Generator Material Purchase Integration Guide
Framework-agnostic xTool material-purchase modal SDK. Drop it into any Vue / React / vanilla JS project via
<script src>orimport.
Table of contents
- Overview
- Install and import
- Quick start
- Public API
- Configuration reference
- Authentication
- Framework integration examples
- Internationalization (i18n)
- Styling and visual spec
- FAQ
- Build artifacts
- Versions and changelog
1. Overview
After calling GeneratorMaterialPurchase.init(...) followed by GeneratorMaterialPurchase.open(), the SDK inserts a modal under the current page's document.body:
position: fixed, slides in from the right edge- 320px wide on desktop; full-screen (100% width) on mobile (≤ 767px), 100vh tall (full height)
- Semi-transparent backdrop (
rgba(0, 0, 0, 0.4)), click to dismiss - Contents: title bar / store switcher / product list (variant dropdown, quantity adjust, checkboxes) / total + Buy Now
Business capabilities:
- Picks a Shopify storefront by IP (US / CA / EU / UK / FR / DE / JP / AU), with manual override
- Uses
@xtool/shopify-sdkto create a cart → fetchescheckoutUrl→ defaults towindow.open(_, '_blank') - Calls the atomm backend for the accessory pack list / IP geolocation / distribution
trackId - Out-of-stock items support back-in-stock notification subscription (Notify Me, on by default; pass
enableReplenishNotify: falsetoinitto turn it off) - Bundles en / zh strings
2. Install and import
2.1 Via CDN (recommended for non-Vue/React projects)
<!-- UMD: legacy-friendly, exposes window.GeneratorMaterialPurchase -->
<script src="https://static-res.atomm.com/scripts/js/generator-sdk/generator-material-purchase/index.umd.js"></script>Once loaded, window.GeneratorMaterialPurchase is your public API.
2.2 Via npm (recommended for Vue / React projects)
# Internal registry requires .npmrc → http://repository.makeblock.com/repository/npm-group/
pnpm add @atomm-developer/generator-material-purchase
# or
npm install @atomm-developer/generator-material-purchaseimport GeneratorMaterialPurchase from '@atomm-developer/generator-material-purchase'
// or named import
import { GeneratorMaterialPurchase } from '@atomm-developer/generator-material-purchase'The package ships both ESM (
index.es.js) and UMD (index.umd.js). Modern bundlers pick the right entry automatically.
3. Quick start
Minimal working example — two steps: init + open.
<!doctype html>
<html>
<head>
<script src="https://static-res.atomm.com/scripts/js/generator-sdk/generator-material-purchase/index.umd.js"></script>
</head>
<body>
<button id="buyBtn">Buy accessories</button>
<script>
// Step 1: initialize with required business params
GeneratorMaterialPurchase.init({
generatorId: 'light-sign',
accessoryType: 'default',
})
// Step 2: open the modal on click
document.getElementById('buyBtn').addEventListener('click', () => {
GeneratorMaterialPurchase.open()
})
</script>
</body>
</html>4. Public API
Four methods are exposed:
interface GeneratorMaterialPurchaseApi {
init(options: PurchaseModalInitOptions): void
open(options?: OpenModalOptions): Promise<void>
close(): void
destroy(): void
}
interface OpenModalOptions {
/**
* Optional: accessoryId list to pre-select.
* - Provided and non-empty: only these accessories are selected, and they are pinned to the top of the list.
* - Omitted or empty array: all purchasable accessories are selected by default (original behavior).
*/
selectIds?: Array<string | number>
/**
* Optional: accessoryId list to hide.
* - Matched accessories are filtered out of the list entirely — no rendering, selection, or checkout.
* - Takes precedence over `selectIds` and `quantities` for the same accessoryId.
*/
hideIds?: Array<string | number>
/**
* Optional: initial quantity per accessoryId. Unlisted accessories keep the default of 1.
* Independent of `selectIds` — an accessory not in `selectIds` can still be pre-filled.
*
* Rules:
* - Accepts numbers or numeric strings (e.g. 3, "3");
* - Non-integers are rounded up (2.1 → 3, "2.1" → 3);
* - Non-numeric values, NaN, and values ≤ 0 fall back to the default of 1.
*/
quantities?: Record<string | number, number | string>
}| Method | Description |
|---|---|
init(options) | Must be called first. Sets business params, locale, and callbacks. Can be called multiple times to update config. If generatorId or accessoryType changes, the internal product cache is automatically invalidated so the next open() re-fetches. |
open(options?) | Mounts the modal DOM into document.body and kicks off the first product load (accessory pack + IP geo). Returns a Promise; init failures fire onError. Options: selectIds (pre-select & pin), hideIds (hide specific accessories), quantities (pre-fill per-accessory initial quantity). |
close() | Plays exit animation, then unmounts the modal DOM. Also triggered by the user clicking the backdrop / close icon. |
destroy() | Force-unmounts and clears internal caches (e.g. product cache). To use the SDK again, call init first. |
open()can be called repeatedly. Each open resets UI state (checkboxes, variant selection, scroll position) but preservesinitconfig and the previous product cache (no duplicate request for the same store). TheselectIds/hideIds/quantitiesof eachopenapply to that call only; omit them next time to fall back to defaults.
selectIds: pre-select and pin specific accessories
accessoryId is the accessoryId field returned per accessory by the /community/v1/web/generator-accessory-pack endpoint.
// Select and pin only accessories with accessoryId 1001 and 1002
GeneratorMaterialPurchase.open({ selectIds: [1001, 1002] })
// Omitted or empty array → select all purchasable accessories by default
GeneratorMaterialPurchase.open()Rules:
- Pinning: matched accessories are moved to the top of the list in the given order (regardless of stock);
- Selection: only matched and purchasable accessories are checked; an out-of-stock match is not checked (to avoid checkout being blocked);
- Store switch: after switching country/store and reloading,
selectIdsstill applies (pin + select); - Matching: compared by
accessoryIdas a string, so bothnumberandstringwork.
hideIds: hide specific accessories
// Do not show accessoryId 2001 or 2002 when the modal opens
GeneratorMaterialPurchase.open({ hideIds: [2001, 2002] })Rules:
- Filter timing: applied by
accessoryIdright after the fetch; matched accessories never enter rendering, selection, or checkout; - Precedence: for the same
accessoryId,hideIdswins overselectIdsandquantities; - Scope: applies to the current
open()only; omit it next time to restore the full list; - Matching: same as
selectIds, compared byaccessoryIdas a string.
quantities: pre-fill initial quantity per accessory
Set the initial quantity for specific accessories when the modal opens; unlisted ones keep the default of 1:
// Open with accessoryId=1001 quantity=3, 1002 quantity=5; others stay at 1
GeneratorMaterialPurchase.open({
selectIds: [1001, 1002],
quantities: { 1001: 3, 1002: 5 },
})
// Numeric strings are also accepted
GeneratorMaterialPurchase.open({ quantities: { 1001: '3', 1002: '2.1' } })Rules:
- Normalization: accepts numbers or numeric strings; non-integers are rounded up (
2.1 → 3);NaN, non-numeric values, and values≤ 0fall back to1; - Independent of
selectIds: an accessory not inselectIdscan still be pre-filled without affecting selection state; - Scope: applies to the current
open()only; omit it next time to fall back to1; - Matching: same as
selectIds, compared byaccessoryIdas a string.
Back-in-stock notification (Notify Me)
On by default: out-of-stock (outOfStock === true) product rows show a "Notify Me" button; pass enableReplenishNotify: false to init to turn it off:
- Clicking it calls
/community/v1/web/subscribe/product/messagewith the currentstore, the variant'svariantId, thegeneratorIdpassed toinit, and the item'ssupplySkuId(itsaccessoryId); - On success the button switches to a "Subscribed" state (check icon + muted color) and a toast confirms an email will be sent when the item is back in stock;
- The "Subscribed" state lasts for the current browser session only and resets on page refresh (it is a front-end duplicate-click guard, independent of the backend subscription record);
- The endpoint requires a signed-in user: when not signed in, the SDK does not send the request and fires the
onRequireLogincallback so the host can launch its own login flow; without the callback the SDK only shows a "Please sign in first." toast.
GeneratorMaterialPurchase.init({
generatorId: 'light-sign',
accessoryType: 'default',
onRequireLogin: () => {
// Launch the host's own login dialog / redirect to the login page
openLoginDialog()
},
})5. Configuration reference
interface PurchaseModalInitOptions {
/** Required: generator code, e.g. 'light-sign', 'flower-generator' */
generatorId: string
/** Required: accessory type. Currently 'default' is used across prod. */
accessoryType: string
/** Optional: default locale, 'en' | 'zh'. Defaults to 'en'. */
locale?: 'en' | 'zh' | string
/** Optional: modal z-index. Defaults to 9999. */
zIndex?: number
/** Optional: override the default prod apiBaseUrl. Defaults to https://xcs-api.xtool.com */
apiBaseUrl?: string
/** Optional: extend or override locale strings */
messages?: Partial<Record<string, Record<string, string>>>
/** Optional: back-in-stock notification (Notify Me) switch. Defaults to true (on); pass false to turn it off. */
enableReplenishNotify?: boolean
/** Optional: checkout success callback. Defaults to window.open(checkoutUrl, '_blank') */
onCheckoutSuccess?: (checkoutUrl: string) => void
/** Optional: modal close callback */
onClose?: () => void
/** Optional: fired when a signed-out user clicks "Notify Me"; the host launches its own login flow. Without it the SDK only shows a sign-in toast. */
onRequireLogin?: () => void
/** Optional: error callback (API failure / checkout failure / etc.) */
onError?: (err: unknown) => void
}Field semantics:
generatorId— Sent to the backend/generator-accessory-packendpoint to determine which generator's accessory list to load. Also used asrelatedObjectTypeon the distribution endpoint.accessoryType— Same endpoint's input. Currently'default'across prod.locale— Current i18n language. To switch dynamically, callinit({ locale: ... })thenopen()again.messages— Overrides individual strings or adds non-bundled languages, e.g.{ en: { buy_now: 'Checkout' }, ja: { ... } }.zIndex— Raise when the host page has higher-stacked elements (e.g. global toasts) that would otherwise overlay the modal.apiBaseUrl— Defaults to the prod atomm service. Point to other environments for local dev or staging.onCheckoutSuccess— Default behavior opens a new tab on the Shopify checkout page. To redirect the current page, implementlocation.href = url.onClose— Fires when the user dismisses the modal. Useful for analytics / business glue.enableReplenishNotify— Back-in-stock notification switch. Defaults totrue: out-of-stock rows show the "Notify Me" button; passfalseto turn it off so out-of-stock rows only show the "Sold out" badge.onRequireLogin— Fires when a signed-out user (nouToken) clicks "Notify Me". The SDK sends no request; the host should launch its login flow. Without it the SDK only shows a "Please sign in first." toast.onError— Fires on product-load failure, checkout failure, IP geolocation failure, etc. The SDK already shows a toast internally; you don't need extra UI.
6. Authentication
The SDK automatically injects two headers into every request to the atomm backend:
| Header | Source |
|---|---|
uToken | Reads document.cookie.utoken first, then falls back to localStorage.utoken |
lang | Reads localStorage.LANG_KEY, defaults to 'en' |
For cross-origin deployments:
- Make sure the host page has written
utokento cookie or localStorage (usually right after login).- Make sure
xcs-api.xtool.comwhitelists the caller domain in CORS and allows the customuToken / langheaders.
If you see 401 / 403, check whether uToken is present, expired, or stripped by cross-origin policy.
7. Framework integration examples
7.1 Vanilla JS (<script src>)
<script src="https://static-res.atomm.com/scripts/js/generator-sdk/generator-material-purchase/index.umd.js"></script>
<script>
GeneratorMaterialPurchase.init({
generatorId: 'light-sign',
accessoryType: 'default',
locale: 'en',
onCheckoutSuccess: (url) => (location.href = url), // redirect current tab instead of new tab
onClose: () => console.log('modal closed'),
})
document.querySelector('#buyBtn').onclick = () => GeneratorMaterialPurchase.open()
</script>7.2 Vue 3 (Composition API)
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue'
import GeneratorMaterialPurchase from '@atomm-developer/generator-material-purchase'
onMounted(() => {
GeneratorMaterialPurchase.init({
generatorId: 'light-sign',
accessoryType: 'default',
})
})
onBeforeUnmount(() => {
GeneratorMaterialPurchase.destroy()
})
function handleClick() {
GeneratorMaterialPurchase.open()
}
</script>
<template>
<button @click="handleClick">Buy accessories</button>
</template>7.3 React (Hooks)
import { useEffect } from 'react'
import GeneratorMaterialPurchase from '@atomm-developer/generator-material-purchase'
export function BuyButton() {
useEffect(() => {
GeneratorMaterialPurchase.init({
generatorId: 'flower-generator',
accessoryType: 'default',
locale: 'en',
})
return () => GeneratorMaterialPurchase.destroy()
}, [])
return <button onClick={() => GeneratorMaterialPurchase.open()}>Buy accessories</button>
}7.4 Vue 2 (Options API)
<script>
import GeneratorMaterialPurchase from '@atomm-developer/generator-material-purchase'
export default {
mounted() {
GeneratorMaterialPurchase.init({
generatorId: 'light-sign',
accessoryType: 'default',
})
},
beforeDestroy() {
GeneratorMaterialPurchase.destroy()
},
methods: {
openModal() {
GeneratorMaterialPurchase.open()
},
},
}
</script>
<template>
<button @click="openModal">Buy accessories</button>
</template>8. Internationalization (i18n)
8.1 Bundled languages
The SDK ships with full en and zh string sets, covering:
- Modal title / close button / country notice / loading / empty state / sold-out badge
- Total / discount notice / Buy Now
- Various error toasts (load failure, checkout failure, out of stock, etc.)
- Country names for all 8 storefronts (USA / Canada / Australia / EU / Germany / France / UK / Japan)
Switch via init({ locale: 'zh' }).
8.2 Extending or overriding strings
GeneratorMaterialPurchase.init({
generatorId: 'light-sign',
accessoryType: 'default',
locale: 'en',
messages: {
en: {
buy_now: 'Checkout securely', // override one entry
},
ja: {
// add Japanese
shopify_supplies_kit: '消耗品キット',
buy_now: '今すぐ購入',
// ...missing entries fall back to en
},
},
})8.3 String key reference
| key | Meaning |
|---|---|
shopify_supplies_kit | Modal title |
close | Close button aria-label |
current_country_notice | Country notice, template includes {country} |
loading | Loading text |
no_items_in_cart | Empty product state |
sold_out | Sold-out badge |
notify_me | Notify Me button |
notify_subscribed | Subscribed button state |
notify_subscribe_success | Subscribe success toast |
notify_subscribe_fail | Subscribe failure toast |
notify_login_required | Sign-in required toast |
total | Total label |
discount_notice | Discount code notice |
buy_now | Buy Now button |
failed_load_product_list | Load failure toast |
checkout_failed | Checkout failure toast |
selected_items_out_of_stock | Selected items out of stock toast |
no_valid_products_selected | No purchasable products toast |
store_us / store_ca / store_au / store_eu / store_de / store_fr / store_uk / store_jp | Storefront dropdown short names |
country_usa / country_canada / country_australia / country_eu / country_germany / country_france / country_uk / country_japan | Long country names |
9. Styling and visual spec
| Aspect | Spec |
|---|---|
| Mount | document.body.appendChild(host), host uses Shadow DOM for style isolation |
| Backdrop | position: fixed; inset: 0; background: rgba(0,0,0,0.4);, click to dismiss |
| Modal | position: fixed; top: 0; right: 0; width: 320px; height: 100vh; — on mobile (≤ 767px): width: 100%; left: 0 (full-screen) |
| Animation | Backdrop fades over 0.2s, modal slides over 0.25s translateX(100% → 0) |
| Mobile scrolling | -webkit-overflow-scrolling: touch for native iOS momentum scrolling in the product list |
| Mobile tap targets | Close button 36×36px, qty buttons 32×32px on mobile (20–24px on desktop) |
| Touch feedback | All interactive buttons expose :active press states (in addition to :hover) |
| Font | Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif |
| Primary colors | Buy button #ff0035 / accent #070b10 / notice orange #ff7c23 |
| CSS naming | BEM style with unified xpm- prefix (Xtool Purchase Modal) |
Because the modal uses Shadow DOM, host-page CSS resets, Tailwind, or * { box-sizing: ... } won't bleed into the modal. Likewise, internal modal styles don't leak out to the host page.
10. FAQ
Q1: Calling open() throws "call init() before open()"
You called open() before init(). Call init once during mount (onMounted in Vue / useEffect in React).
Q2: Modal opens but product list spins forever / shows empty
Step through:
- In DevTools Network, verify
/generator-accessory-packreturns 200 withcode: 0. - On 401 / 403, confirm
uTokenis in cookie or localStorage. - On CORS errors, ask the backend to add your domain to the whitelist.
Q3: Checkout button is greyed out, what now?
When every checked item is detected as out-of-stock (outOfStock === true or availableForSale === false), the button greys out. Switch to a different variant or storefront.
Q4: How do I redirect the current tab instead of opening a new tab?
Pass onCheckoutSuccess:
GeneratorMaterialPurchase.init({
generatorId: '...',
accessoryType: 'default',
onCheckoutSuccess: (url) => (window.location.href = url),
})Q5: Locale switched but the modal didn't refresh
Re-call init({ locale: 'xx' }) after switching. If the modal is currently open, close() then open().
Q6: Can I open multiple modals at once?
No. The SDK is a singleton; a second open() waits for the first one's exit animation before mounting. Need concurrent modals? Let us know.
Q7: Does it pollute globals?
Only attaches window.GeneratorMaterialPurchase (UMD mode). Doesn't modify body styles; only appends a host element while the modal exists, removed after close/destroy.
Q8: DevTools is awkward inside Shadow DOM. Any workaround?
In Chrome DevTools → Settings → Preferences → Elements, enable "Show user agent shadow DOM" to expand it. Or call methods directly via window.GeneratorMaterialPurchase in the Console.
Q9: An item is out of stock but the "Notify Me" button doesn't show
Check whether init was given enableReplenishNotify: false (the switch is on by default; with false out-of-stock rows only show the "Sold out" badge).
Q10: Clicking "Notify Me" does nothing / shows a sign-in toast
The endpoint requires a signed-in user. Without a uToken the SDK sends no request: it fires onRequireLogin if configured, otherwise toasts "Please sign in first.". Also note the button is disabled in the "Subscribed" and in-flight states — that is the duplicate-click guard, not a bug.
Q11: After a page refresh the "Subscribed" button reverts to "Notify Me" — is that a bug?
No. The "Subscribed" state lasts for the current browser session only (matching the 3d_generator behavior) and exists solely to prevent duplicate clicks; the backend subscription record is unaffected, and re-subscribing the same variant does not produce duplicate emails.
11. Build artifacts
Build command:
pnpm buildOutput in dist/:
| File | Use | Size (gzip) |
|---|---|---|
index.es.js | ESM entry, for npm / modern bundlers | ~15 KB |
index.umd.js | UMD, browser <script src> or Node require | ~12 KB |
index.d.ts | TypeScript declarations (rollup-merged single file) | — |
*.map | sourcemaps for runtime error tracing | — |
Published to npm with files:
dist/(artifacts)README.md
12. Versions and changelog
0.1.6
- New option:
open()acceptshideIds— matched accessories are filtered out of the list entirely and take no part in display / selection / checkout; when it conflicts withselectIdsorquantitieson the sameaccessoryId,hideIdswins - New option:
open()acceptsquantities— pre-set the initial quantity peraccessoryIdwhen the modal opens (unspecified accessories default to 1); accepts number or numeric string (non-integers round up,NaN/ non-numeric /≤0fall back to 1); independent ofselectIds - Scope: both
hideIdsandquantitiesapply only to the currentopen()call; the next call without them reverts to defaults
0.1.5
- Default variant binding: on first render, on variant fallback, and when computing the default checked variant, the SDK now honors the default variant id returned by the API without any host-side configuration
- API field rename: the default variant id field is renamed from
defaultVarianttoconfigVariantId; the SDK reads the new field first and falls back to the legacydefaultVariantwhen absent — fully backwards-compatible
0.1.4
- Feature: back-in-stock notification (Notify Me) for out-of-stock items, on by default (pass
enableReplenishNotify: falsetoinitto turn it off); out-of-stock product rows show a "Notify Me" button, clicking calls/community/v1/web/subscribe/product/message(withstore+variantId+generatorId+supplySkuId), then switches to a "Subscribed" state with a confirmation toast - Config: new
initoptionenableReplenishNotifyand optional callbackonRequireLogin(fired when a signed-out user clicks "Notify Me" so the host can launch its login flow; without it the SDK shows a sign-in toast) - Behavior: out-of-stock rows no longer show a disabled quantity control (matching 3d_generator)
- Strings:
notify_me/notify_subscribed/notify_subscribe_success/notify_subscribe_fail/notify_login_required
0.1.3
- Bug fix: variant selection resetting the product list scroll position (scroll position is now preserved across re-renders)
- Bug fix: re-calling
init()with a differentgeneratorId/accessoryTypenot refreshing the product list (cache is now invalidated when either field changes) - Mobile: panel is full-screen (100% width) on viewports ≤ 767px; 320px fixed width is preserved on desktop
- Mobile: product list gains
-webkit-overflow-scrolling: touchfor native iOS momentum scrolling - Mobile: close button enlarged to 36×36px, qty buttons to 32×32px for better touch targets
- UX: all interactive buttons now have
:activepress states for touch feedback
0.1.0 (initial release)
- First published version
- Exposes
init / open / close / destroy, attaches globalGeneratorMaterialPurchase - Ships 8 prod Shopify storefronts
- Modal layout: right-side 320px / full height / backdrop
- Bundled en + zh strings
- Auto-injects
uToken+langon every request - BEM CSS + Shadow DOM style isolation
Feedback
For bugs or feature requests, file an issue on the internal GitLab repo, or ping the maintainers in the team Feishu group.