Skip to content

Generator Material Purchase Integration Guide

Framework-agnostic xTool material-purchase modal SDK. Drop it into any Vue / React / vanilla JS project via <script src> or import.


Table of contents

  1. Overview
  2. Install and import
  3. Quick start
  4. Public API
  5. Configuration reference
  6. Authentication
  7. Framework integration examples
  8. Internationalization (i18n)
  9. Styling and visual spec
  10. FAQ
  11. Build artifacts
  12. 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-sdk to create a cart → fetches checkoutUrl → defaults to window.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: false to init to turn it off)
  • Bundles en / zh strings

2. Install and import

html
<!-- 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.

bash
# 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-purchase
ts
import 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.

html
<!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:

ts
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>
}
MethodDescription
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 preserves init config and the previous product cache (no duplicate request for the same store). The selectIds / hideIds / quantities of each open apply 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.

ts
// 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, selectIds still applies (pin + select);
  • Matching: compared by accessoryId as a string, so both number and string work.

hideIds: hide specific accessories

ts
// Do not show accessoryId 2001 or 2002 when the modal opens
GeneratorMaterialPurchase.open({ hideIds: [2001, 2002] })

Rules:

  • Filter timing: applied by accessoryId right after the fetch; matched accessories never enter rendering, selection, or checkout;
  • Precedence: for the same accessoryId, hideIds wins over selectIds and quantities;
  • Scope: applies to the current open() only; omit it next time to restore the full list;
  • Matching: same as selectIds, compared by accessoryId as 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:

ts
// 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 ≤ 0 fall back to 1;
  • Independent of selectIds: an accessory not in selectIds can still be pre-filled without affecting selection state;
  • Scope: applies to the current open() only; omit it next time to fall back to 1;
  • Matching: same as selectIds, compared by accessoryId as 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/message with the current store, the variant's variantId, the generatorId passed to init, and the item's supplySkuId (its accessoryId);
  • 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 onRequireLogin callback so the host can launch its own login flow; without the callback the SDK only shows a "Please sign in first." toast.
ts
GeneratorMaterialPurchase.init({
  generatorId: 'light-sign',
  accessoryType: 'default',
  onRequireLogin: () => {
    // Launch the host's own login dialog / redirect to the login page
    openLoginDialog()
  },
})

5. Configuration reference

ts
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-pack endpoint to determine which generator's accessory list to load. Also used as relatedObjectType on the distribution endpoint.
  • accessoryType — Same endpoint's input. Currently 'default' across prod.
  • locale — Current i18n language. To switch dynamically, call init({ locale: ... }) then open() 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, implement location.href = url.
  • onClose — Fires when the user dismisses the modal. Useful for analytics / business glue.
  • enableReplenishNotify — Back-in-stock notification switch. Defaults to true: out-of-stock rows show the "Notify Me" button; pass false to turn it off so out-of-stock rows only show the "Sold out" badge.
  • onRequireLogin — Fires when a signed-out user (no uToken) 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:

HeaderSource
uTokenReads document.cookie.utoken first, then falls back to localStorage.utoken
langReads localStorage.LANG_KEY, defaults to 'en'

For cross-origin deployments:

  1. Make sure the host page has written utoken to cookie or localStorage (usually right after login).
  2. Make sure xcs-api.xtool.com whitelists the caller domain in CORS and allows the custom uToken / lang headers.

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>)

html
<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)

vue
<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)

tsx
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)

vue
<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

ts
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

keyMeaning
shopify_supplies_kitModal title
closeClose button aria-label
current_country_noticeCountry notice, template includes {country}
loadingLoading text
no_items_in_cartEmpty product state
sold_outSold-out badge
notify_meNotify Me button
notify_subscribedSubscribed button state
notify_subscribe_successSubscribe success toast
notify_subscribe_failSubscribe failure toast
notify_login_requiredSign-in required toast
totalTotal label
discount_noticeDiscount code notice
buy_nowBuy Now button
failed_load_product_listLoad failure toast
checkout_failedCheckout failure toast
selected_items_out_of_stockSelected items out of stock toast
no_valid_products_selectedNo purchasable products toast
store_us / store_ca / store_au / store_eu / store_de / store_fr / store_uk / store_jpStorefront dropdown short names
country_usa / country_canada / country_australia / country_eu / country_germany / country_france / country_uk / country_japanLong country names

9. Styling and visual spec

AspectSpec
Mountdocument.body.appendChild(host), host uses Shadow DOM for style isolation
Backdropposition: fixed; inset: 0; background: rgba(0,0,0,0.4);, click to dismiss
Modalposition: fixed; top: 0; right: 0; width: 320px; height: 100vh; — on mobile (≤ 767px): width: 100%; left: 0 (full-screen)
AnimationBackdrop 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 targetsClose button 36×36px, qty buttons 32×32px on mobile (20–24px on desktop)
Touch feedbackAll interactive buttons expose :active press states (in addition to :hover)
FontInter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
Primary colorsBuy button #ff0035 / accent #070b10 / notice orange #ff7c23
CSS namingBEM 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:

  1. In DevTools Network, verify /generator-accessory-pack returns 200 with code: 0.
  2. On 401 / 403, confirm uToken is in cookie or localStorage.
  3. 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:

ts
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:

bash
pnpm build

Output in dist/:

FileUseSize (gzip)
index.es.jsESM entry, for npm / modern bundlers~15 KB
index.umd.jsUMD, browser <script src> or Node require~12 KB
index.d.tsTypeScript declarations (rollup-merged single file)
*.mapsourcemaps for runtime error tracing

Published to npm with files:

  • dist/ (artifacts)
  • README.md

12. Versions and changelog

0.1.6

  • New option: open() accepts hideIds — matched accessories are filtered out of the list entirely and take no part in display / selection / checkout; when it conflicts with selectIds or quantities on the same accessoryId, hideIds wins
  • New option: open() accepts quantities — pre-set the initial quantity per accessoryId when the modal opens (unspecified accessories default to 1); accepts number or numeric string (non-integers round up, NaN / non-numeric / ≤0 fall back to 1); independent of selectIds
  • Scope: both hideIds and quantities apply only to the current open() 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 defaultVariant to configVariantId; the SDK reads the new field first and falls back to the legacy defaultVariant when absent — fully backwards-compatible

0.1.4

  • Feature: back-in-stock notification (Notify Me) for out-of-stock items, on by default (pass enableReplenishNotify: false to init to turn it off); out-of-stock product rows show a "Notify Me" button, clicking calls /community/v1/web/subscribe/product/message (with store + variantId + generatorId + supplySkuId), then switches to a "Subscribed" state with a confirmation toast
  • Config: new init option enableReplenishNotify and optional callback onRequireLogin (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 different generatorId / accessoryType not 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: touch for 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 :active press states for touch feedback

0.1.0 (initial release)

  • First published version
  • Exposes init / open / close / destroy, attaches global GeneratorMaterialPurchase
  • Ships 8 prod Shopify storefronts
  • Modal layout: right-side 320px / full height / backdrop
  • Bundled en + zh strings
  • Auto-injects uToken + lang on 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.

MIT Licensed