@magicstoreai/hydrogen 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CATALOGUE.md +931 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/core.cjs +746 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +252 -0
- package/dist/core.d.ts +252 -0
- package/dist/core.js +706 -0
- package/dist/core.js.map +1 -0
- package/dist/index.cjs +1046 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +790 -0
- package/dist/index.d.ts +790 -0
- package/dist/index.js +1000 -0
- package/dist/index.js.map +1 -0
- package/dist/seo.cjs +101 -0
- package/dist/seo.cjs.map +1 -0
- package/dist/seo.d.cts +83 -0
- package/dist/seo.d.ts +83 -0
- package/dist/seo.js +73 -0
- package/dist/seo.js.map +1 -0
- package/dist/server.cjs +172 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +67 -0
- package/dist/server.d.ts +67 -0
- package/dist/server.js +139 -0
- package/dist/server.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core.ts","../src/analytics.ts","../src/cart.ts","../src/storage.ts","../src/money.ts","../src/session.ts","../src/variants.ts","../src/wishlist.ts"],"sourcesContent":["// The framework-free core: what the React bindings are built on, usable from any UI library.\nexport { AnalyticsController, attributionFrom, visitorSessionId } from './analytics';\nexport type { AnalyticsEvent, AnalyticsEventType, AnalyticsPayload } from './analytics';\nexport { CartController } from './cart';\nexport type { Cart, CartAttribute, CartLine, CartLineInput, CartState } from './cart';\nexport { currencySymbol, formatMoney, groupAmount, moneyAmount } from './money';\nexport type { Money, MoneyFormat } from './money';\nexport { CustomerSessionController } from './session';\nexport type { Customer, CustomerSession, SessionState } from './session';\nexport { browserStorage, memoryStorage } from './storage';\nexport type { KeyValueStorage } from './storage';\nexport {\n initialSelection,\n isOptionValueAvailable,\n optionsOf,\n selectOption,\n variantFor,\n} from './variants';\nexport type { ProductOption, ProductVariant, SelectableProduct, SelectedOptions } from './variants';\nexport { WISHLIST_LIMIT, WishlistController } from './wishlist';\nexport type { WishlistState } from './wishlist';\n","import type { Schema, StorefrontClient, operations } from '@magicstoreai/storefront-client';\nimport type { KeyValueStorage } from './storage';\n\ntype EventsBody = NonNullable<\n operations['analyticsEventsStore']['requestBody']\n>['content']['application/json'];\nexport type AnalyticsEvent = EventsBody['events'][number];\nexport type AnalyticsEventType = Schema<'StorefrontEventType'>;\nexport type AnalyticsPayload = AnalyticsEvent['payload'];\n\n/** The API takes up to this many events per batch. */\nconst BATCH_LIMIT = 50;\n\n/**\n * The visitor's id for the shop's analytics and funnel: a UUID minted once per visitor and kept.\n * The same value goes out as `X-Session-Id`, which ties carts and orders to the visit.\n */\nexport function visitorSessionId(storage: KeyValueStorage, key = 'magicstore.session-id'): string {\n const existing = storage.get(key);\n if (existing !== null && /^[0-9a-f-]{36}$/i.test(existing)) {\n return existing;\n }\n const id = globalThis.crypto.randomUUID();\n storage.set(key, id);\n return id;\n}\n\n/**\n * What the visitor looks at, sent in batches (`POST /analytics/events`). Cart and checkout steps\n * are recorded by the server — never report them. Failures are dropped: analytics must never get\n * in the way of shopping.\n */\nexport class AnalyticsController {\n private queue: AnalyticsEvent[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(\n private readonly client: StorefrontClient,\n private readonly sessionId: string,\n private readonly options: { flushAfterMs?: number; context?: AnalyticsPayload } = {},\n ) {}\n\n /** A page view — starts the session with its first-touch attribution (UTM, click ids, referrer). */\n pageView(path: string, extra: AnalyticsPayload = {}): void {\n this.track('PAGE_VIEW', { path: path.slice(0, 255), ...extra });\n }\n\n productView(productId: string, variantId?: string | null): void {\n this.track('PRODUCT_VIEW', { productId, variantId: variantId ?? null });\n }\n\n collectionView(collectionId: string): void {\n this.track('COLLECTION_VIEW', { collectionId });\n }\n\n search(query: string, resultsCount?: number): void {\n this.track('SEARCH', { query: query.slice(0, 255), resultsCount: resultsCount ?? null });\n }\n\n custom(name: string, properties?: Record<string, unknown>): void {\n this.track('CUSTOM', {\n name: name.slice(0, 64),\n properties: properties ?? null,\n } as AnalyticsPayload);\n }\n\n track(type: AnalyticsEventType, payload: AnalyticsPayload): void {\n this.queue.push({\n type,\n sessionId: this.sessionId,\n occurredAt: new Date().toISOString(),\n payload: { ...this.options.context, ...payload },\n });\n if (this.queue.length >= BATCH_LIMIT) {\n void this.flush();\n return;\n }\n this.timer ??= setTimeout(() => void this.flush(), this.options.flushAfterMs ?? 2000);\n }\n\n /** Sends what is queued now — call it when the page is hidden. */\n async flush(): Promise<void> {\n if (this.timer !== null) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n while (this.queue.length > 0) {\n const events = this.queue.splice(0, BATCH_LIMIT);\n try {\n await this.client.analyticsEventsStore({ body: { events } });\n } catch {\n // Dropped on purpose.\n }\n }\n }\n}\n\n/** The first-touch attribution a landing URL carries, for the first `pageView`. */\nexport function attributionFrom(url: URL, referrer?: string): AnalyticsPayload {\n const param = (name: string): string | null => url.searchParams.get(name)?.slice(0, 255) || null;\n return {\n utmSource: param('utm_source'),\n utmMedium: param('utm_medium'),\n utmCampaign: param('utm_campaign'),\n utmTerm: param('utm_term'),\n utmContent: param('utm_content'),\n utmId: param('utm_id'),\n gclid: param('gclid'),\n fbclid: param('fbclid'),\n referrer: referrer ? referrer.slice(0, 255) : null,\n };\n}\n","import {\n MagicStoreError,\n type Schema,\n type StorefrontClient,\n} from '@magicstoreai/storefront-client';\nimport { Observable, type KeyValueStorage } from './storage';\n\nexport type Cart = Schema<'Cart'>;\nexport type CartLine = Schema<'CartLine'>;\nexport type CartAttribute = { key: string; value: string | null };\n\n/** A line to add: the product, its variant (null for a product without variants) and how many. */\nexport interface CartLineInput {\n productId: string;\n variantId?: string | null;\n quantity?: number;\n attributes?: CartAttribute[];\n}\n\nexport interface CartState {\n cart: Cart | null;\n /** `loading` while the stored cart is fetched; `updating` while a change is on its way. */\n status: 'idle' | 'loading' | 'updating';\n /** The last change that failed — the cart is back to what the server holds. */\n error: MagicStoreError | null;\n}\n\n/** A cart id the server no longer serves: forget it and start a new cart on the next add. */\nconst GONE = new Set(['CART_NOT_FOUND', 'CART_CLOSED']);\n\n/**\n * The visitor's cart. Its id is a capability and lives only in storage; the cart itself always\n * comes from the server, so prices, discounts and availability are the server's. Quantity changes\n * show at once and roll back if the server refuses them. Changes run one after another.\n */\nexport class CartController extends Observable<CartState> {\n private queue: Promise<unknown> = Promise.resolve();\n\n constructor(\n private readonly client: StorefrontClient,\n private readonly storage: KeyValueStorage,\n private readonly key = 'magicstore.cart-id',\n ) {\n // A stored id means a cart is on its way: say so, not \"empty\". The server cannot know.\n super(\n { cart: null, status: storage.get(key) === null ? 'idle' : 'loading', error: null },\n { cart: null, status: 'loading', error: null },\n );\n }\n\n get cart(): Cart | null {\n return this.get().cart;\n }\n\n get cartId(): string | null {\n return this.storage.get(this.key);\n }\n\n /** Fetches the stored cart, if any. A cart that is gone is forgotten. */\n load(): Promise<Cart | null> {\n return this.enqueue(async () => {\n const id = this.cartId;\n if (id === null) {\n this.patch({ status: 'idle' });\n return null;\n }\n this.patch({ status: 'loading' });\n try {\n const { data } = await this.client.cartsShow({ path: { id } });\n return this.adopt(data);\n } catch (error) {\n return this.fail(error, null);\n }\n });\n }\n\n /** Adds lines; the first add creates the cart. */\n addLines(lines: CartLineInput[]): Promise<Cart | null> {\n return this.mutate(null, (id) => {\n const body = { lines: lines.map(toLine) };\n return id === null\n ? this.client.cartsStore({ body })\n : this.client.cartsLinesStore({ path: { id }, body });\n });\n }\n\n /** The line's quantity outright; 0 removes it. Shown at once, rolled back if refused. */\n updateLine(lineId: string, quantity: number): Promise<Cart | null> {\n return this.mutate(\n (cart) => withQuantity(cart, lineId, quantity),\n (id) =>\n this.client.cartsLinesUpdate({ path: { id: required(id), lineId }, body: { quantity } }),\n );\n }\n\n removeLine(lineId: string): Promise<Cart | null> {\n return this.mutate(\n (cart) => withQuantity(cart, lineId, 0),\n (id) => this.client.cartsLinesDestroy({ path: { id: required(id), lineId } }),\n );\n }\n\n /** The cart's codes as a whole — `[]` takes the code off. A code that does not apply is kept, flagged. */\n setDiscountCodes(discountCodes: string[]): Promise<Cart | null> {\n return this.mutate(null, (id) =>\n this.client.cartsDiscountCodes({ path: { id: required(id) }, body: { discountCodes } }),\n );\n }\n\n setNote(note: string | null): Promise<Cart | null> {\n return this.mutate(null, (id) =>\n this.client.cartsNote({ path: { id: required(id) }, body: { note } }),\n );\n }\n\n setAttributes(attributes: CartAttribute[]): Promise<Cart | null> {\n return this.mutate(null, (id) =>\n this.client.cartsAttributes({ path: { id: required(id) }, body: { attributes } }),\n );\n }\n\n /** The gift promotion chosen from `cart.giftOptions`; null takes it off. */\n setGift(promotionId: string | null): Promise<Cart | null> {\n return this.mutate(null, (id) =>\n this.client.cartsGift({ path: { id: required(id) }, body: { promotionId } }),\n );\n }\n\n /** Points to spend on this cart (signed-in customers). */\n setPoints(points: number): Promise<Cart | null> {\n return this.mutate(null, (id) =>\n this.client.cartsPoints({ path: { id: required(id) }, body: { points } }),\n );\n }\n\n /**\n * After sign-in: the cart becomes the customer's. When they already had an open cart, this one\n * merges into it and the answer is THAT cart — its id replaces the stored one.\n */\n attachCustomer(): Promise<Cart | null> {\n return this.enqueue(async () => {\n const id = this.cartId;\n if (id === null) {\n return null;\n }\n this.patch({ status: 'updating' });\n try {\n const { data } = await this.client.cartsBuyerIdentity({ path: { id } });\n return this.adopt(data);\n } catch (error) {\n return this.fail(error, this.cart);\n }\n });\n }\n\n /** After sign-out: the cart is the customer's, not this visitor's any more. */\n forget(): void {\n this.storage.remove(this.key);\n this.set({ cart: null, status: 'idle', error: null });\n }\n\n /** Shows `optimistic` at once (when given), sends the change, and adopts the server's cart or rolls back. */\n private mutate(\n optimistic: ((cart: Cart) => Cart) | null,\n send: (id: string | null) => Promise<{ data: Cart }>,\n ): Promise<Cart | null> {\n return this.enqueue(async () => {\n const before = this.cart;\n this.patch({\n status: 'updating',\n error: null,\n cart: optimistic !== null && before !== null ? optimistic(before) : before,\n });\n try {\n const { data } = await send(this.cartId);\n return this.adopt(data);\n } catch (error) {\n return this.fail(error, before);\n }\n });\n }\n\n private adopt(cart: Cart): Cart {\n this.storage.set(this.key, cart.id);\n this.set({ cart, status: 'idle', error: null });\n return cart;\n }\n\n private fail(error: unknown, rollback: Cart | null): never {\n const failure =\n error instanceof MagicStoreError\n ? error\n : new MagicStoreError({\n status: 0,\n code: 'NETWORK_ERROR',\n message: String(error),\n requestId: '',\n });\n if (GONE.has(failure.code)) {\n this.storage.remove(this.key);\n this.set({ cart: null, status: 'idle', error: failure });\n } else {\n this.set({ cart: rollback, status: 'idle', error: failure });\n }\n throw failure;\n }\n\n private patch(state: Partial<CartState>): void {\n this.set({ ...this.get(), ...state });\n }\n\n private enqueue<T>(task: () => Promise<T>): Promise<T> {\n const run = this.queue.then(task, task);\n this.queue = run.catch(() => undefined);\n return run;\n }\n}\n\nfunction toLine(line: CartLineInput) {\n return {\n productId: line.productId,\n variantId: line.variantId ?? null,\n quantity: line.quantity ?? 1,\n attributes: line.attributes ?? [],\n };\n}\n\nfunction required(id: string | null): string {\n if (id === null) {\n throw new MagicStoreError({\n status: 404,\n code: 'CART_NOT_FOUND',\n message: 'There is no cart yet.',\n requestId: '',\n });\n }\n return id;\n}\n\n/** The cart as it will look once the server has the new quantity (totals stay the server's). */\nfunction withQuantity(cart: Cart, lineId: string, quantity: number): Cart {\n const lines =\n quantity <= 0\n ? cart.lines.filter((line) => line.id !== lineId)\n : cart.lines.map((line) => (line.id === lineId ? { ...line, quantity } : line));\n return { ...cart, lines, totalQuantity: lines.reduce((sum, line) => sum + line.quantity, 0) };\n}\n","/** Where the SDK keeps what outlives a page: the customer session, the cart id, the guest wishlist. */\nexport interface KeyValueStorage {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\n/** In memory — for tests, the server, and when the browser refuses storage. */\nexport function memoryStorage(initial: Record<string, string> = {}): KeyValueStorage {\n const values = new Map(Object.entries(initial));\n return {\n get: (key) => values.get(key) ?? null,\n set: (key, value) => void values.set(key, value),\n remove: (key) => void values.delete(key),\n };\n}\n\n/**\n * `localStorage` when the browser allows it; memory otherwise (server render, private mode,\n * blocked site data). Every access is guarded: storage can throw at any time.\n */\nexport function browserStorage(): KeyValueStorage {\n const fallback = memoryStorage();\n const local = (): Storage | null => {\n try {\n return typeof window === 'undefined' ? null : window.localStorage;\n } catch {\n return null;\n }\n };\n return {\n get(key) {\n try {\n return local()?.getItem(key) ?? fallback.get(key);\n } catch {\n return fallback.get(key);\n }\n },\n set(key, value) {\n try {\n const storage = local();\n if (storage) {\n storage.setItem(key, value);\n return;\n }\n } catch {\n // Quota or a blocked store: keep it for this page at least.\n }\n fallback.set(key, value);\n },\n remove(key) {\n try {\n local()?.removeItem(key);\n } catch {\n // Nothing to do.\n }\n fallback.remove(key);\n },\n };\n}\n\nexport function readJson<T>(storage: KeyValueStorage, key: string): T | null {\n const raw = storage.get(key);\n if (raw === null) {\n return null;\n }\n try {\n return JSON.parse(raw) as T;\n } catch {\n storage.remove(key);\n return null;\n }\n}\n\n/**\n * A tiny observable value — what the React hooks subscribe to. `serverValue` is what a server render\n * and hydration see: storage is browser-only, so both must render the same \"not known yet\" state\n * and let the stored one arrive after hydration (otherwise React reports a hydration mismatch).\n */\nexport class Observable<T> {\n private listeners = new Set<() => void>();\n private readonly serverValue: T;\n\n constructor(\n private value: T,\n serverValue?: T,\n ) {\n this.serverValue = serverValue ?? value;\n }\n\n get(): T {\n return this.value;\n }\n\n protected set(value: T): void {\n this.value = value;\n for (const listener of [...this.listeners]) {\n listener();\n }\n }\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): T => this.value;\n\n getServerSnapshot = (): T => this.serverValue;\n}\n","import type { Schema } from '@magicstoreai/storefront-client';\n\nexport type Money = Schema<'Money'>;\nexport type MoneyFormat = Schema<'Shop'>['moneyFormat'];\n\n/**\n * The currency word per locale — the same templates the MagicStore dashboard renders with.\n * `{amount}` is the grouped number. A currency or locale not listed falls back to its code.\n */\nconst TEMPLATES: Record<string, Record<string, string>> = {\n ru: {\n UZS: '{amount} сум',\n USD: '${amount}',\n KGS: '{amount} сом',\n KZT: '{amount} ₸',\n RUB: '{amount} ₽',\n },\n uz: {\n UZS: '{amount} so‘m',\n USD: '${amount}',\n KGS: '{amount} som',\n KZT: '{amount} ₸',\n RUB: '{amount} ₽',\n },\n en: {\n UZS: '{amount} UZS',\n USD: '${amount}',\n KGS: '{amount} KGS',\n KZT: '{amount} ₸',\n RUB: '{amount} ₽',\n },\n};\n\nfunction template(currencyCode: string, locale: string): string {\n const language = locale.toLowerCase().split(/[-_]/)[0] ?? 'ru';\n return (TEMPLATES[language] ?? TEMPLATES['ru']!)[currencyCode] ?? `{amount} ${currencyCode}`;\n}\n\n/** The currency's symbol or word in a locale: `сум`, `so‘m`, `$`. */\nexport function currencySymbol(currencyCode: string, locale: string): string {\n return template(currencyCode, locale).replace('{amount}', '').trim();\n}\n\n/**\n * `12500.00` → `12 500`: groups of three separated by a space, the fraction kept only when it is\n * not zero. The amount is never rounded here — the API already applied the shop's rounding, and\n * the string is exactly what will be charged.\n */\nexport function groupAmount(amount: string): string {\n const negative = amount.startsWith('-');\n const [integer = '0', fraction = ''] = amount.replace(/^[-+]/, '').split('.');\n const grouped = integer.replace(/^0+(?=\\d)/, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ' ');\n const kept = /^0*$/.test(fraction) ? '' : `.${fraction}`;\n return `${negative ? '-' : ''}${grouped}${kept}`;\n}\n\n/**\n * Money as the shop shows it: `shop.moneyFormat.format` places the symbol or the code, and a shop\n * with no format uses the currency's own convention (`12 500 сум`, `$129.99`).\n */\nexport function formatMoney(\n money: Money,\n options: { locale: string; format?: MoneyFormat | null },\n): string {\n const amount = groupAmount(money.amount);\n const symbol = currencySymbol(money.currencyCode, options.locale);\n\n switch (options.format?.format) {\n case 'SYMBOL_AFTER':\n return `${amount} ${symbol}`.trim();\n case 'SYMBOL_BEFORE':\n return `${symbol} ${amount}`.trim();\n case 'CODE_AFTER':\n return `${amount} ${money.currencyCode}`;\n default:\n return template(money.currencyCode, options.locale).replace('{amount}', amount);\n }\n}\n\n/** The amount as a number, for arithmetic you must do on the client (sorting, a progress bar). */\nexport function moneyAmount(money: Money): number {\n return Number(money.amount);\n}\n","import {\n MagicStoreError,\n type Schema,\n type StorefrontClient,\n} from '@magicstoreai/storefront-client';\nimport { Observable, readJson, type KeyValueStorage } from './storage';\n\nexport type CustomerSession = Schema<'CustomerSession'>;\nexport type Customer = Schema<'Customer'>;\n\nexport interface SessionState {\n session: CustomerSession | null;\n}\n\n/** Refresh this long before the access token expires, so a call never leaves with a dead one. */\nconst REFRESH_AHEAD_MS = 60_000;\n\n/**\n * The signed-in customer and their tokens. The access token lives an hour and is refreshed on\n * demand; the refresh token works once, so concurrent callers share one refresh.\n */\nexport class CustomerSessionController extends Observable<SessionState> {\n private refreshing: Promise<string | null> | null = null;\n private client: StorefrontClient | null = null;\n\n constructor(\n private readonly storage: KeyValueStorage,\n private readonly key = 'magicstore.customer-session',\n private readonly now: () => number = () => Date.now(),\n ) {\n super({ session: readJson<CustomerSession>(storage, key) }, { session: null });\n }\n\n /** The client the session signs in and refreshes through (created with this controller's token). */\n attach(client: StorefrontClient): void {\n this.client = client;\n }\n\n get session(): CustomerSession | null {\n return this.get().session;\n }\n\n get customer(): Customer | null {\n return this.session?.customer ?? null;\n }\n\n /** The bearer for the next call: refreshed when it is about to expire; null when signed out. */\n accessToken = async (): Promise<string | null> => {\n const session = this.session;\n if (session === null) {\n return null;\n }\n if (Date.parse(session.expiresAt) - this.now() > REFRESH_AHEAD_MS) {\n return session.accessToken;\n }\n return this.refresh();\n };\n\n /** One refresh at a time: every caller waiting meanwhile gets its result. */\n refresh(): Promise<string | null> {\n this.refreshing ??= this.doRefresh().finally(() => {\n this.refreshing = null;\n });\n return this.refreshing;\n }\n\n private async doRefresh(): Promise<string | null> {\n const session = this.session;\n if (session === null) {\n return null;\n }\n if (Date.parse(session.refreshTokenExpiresAt) <= this.now()) {\n this.store(null);\n return null;\n }\n try {\n const { data } = await this.requireClient().authTokenRefresh(\n { body: { refreshToken: session.refreshToken } },\n { customerToken: null },\n );\n this.store(data);\n return data.accessToken;\n } catch (error) {\n if (error instanceof MagicStoreError && error.status === 401) {\n // Used, expired or revoked: this sign-in is over.\n this.store(null);\n return null;\n }\n // No answer: keep the session; the call goes out with the token it has.\n return session.accessToken;\n }\n }\n\n /** Texts a sign-in code. */\n async requestOtp(phone: string): Promise<Schema<'OtpChallenge'>> {\n const { data } = await this.requireClient().authOtp(\n { body: { phone } },\n { customerToken: null },\n );\n return data;\n }\n\n async verifyOtp(phone: string, code: string, referralCode?: string): Promise<CustomerSession> {\n const body = referralCode === undefined ? { phone, code } : { phone, code, referralCode };\n return this.signedIn(\n this.requireClient().authOtpVerification({ body }, { customerToken: null }),\n );\n }\n\n async signInWithTelegram(initData: string, referralCode?: string): Promise<CustomerSession> {\n const body = referralCode === undefined ? { initData } : { initData, referralCode };\n return this.signedIn(this.requireClient().authTelegram({ body }, { customerToken: null }));\n }\n\n async signInWithOq(oqToken: string): Promise<CustomerSession> {\n return this.signedIn(\n this.requireClient().authOq({ body: { oqToken } }, { customerToken: null }),\n );\n }\n\n async signInWithClick(webSession: string): Promise<CustomerSession> {\n return this.signedIn(\n this.requireClient().authClick({ body: { webSession } }, { customerToken: null }),\n );\n }\n\n /** Ends this sign-in on the server too; signed out locally whatever the server says. */\n async signOut(): Promise<void> {\n const session = this.session;\n this.store(null);\n if (session === null) {\n return;\n }\n try {\n await this.requireClient().authTokenDestroy(undefined, {\n customerToken: session.accessToken,\n });\n } catch {\n // Already expired or unreachable: the tokens die on their own.\n }\n }\n\n /** The customer as the server has them now (after a profile change). */\n async reloadCustomer(): Promise<Customer | null> {\n if (this.session === null) {\n return null;\n }\n const { data } = await this.requireClient().customerShow();\n const current = this.session;\n if (current !== null) {\n this.store({ ...current, customer: data });\n }\n return data;\n }\n\n private async signedIn(call: Promise<{ data: CustomerSession }>): Promise<CustomerSession> {\n const { data } = await call;\n this.store(data);\n return data;\n }\n\n private store(session: CustomerSession | null): void {\n if (session === null) {\n this.storage.remove(this.key);\n } else {\n this.storage.set(this.key, JSON.stringify(session));\n }\n this.set({ session });\n }\n\n private requireClient(): StorefrontClient {\n if (this.client === null) {\n throw new Error('CustomerSessionController is not attached to a client.');\n }\n return this.client;\n }\n}\n","import type { Schema } from '@magicstoreai/storefront-client';\n\nexport type ProductVariant = Schema<'ProductVariant'>;\nexport type ProductOption = Schema<'ProductOption'>;\n\n/** What variant selection needs of a product — `Product` and `ProductDetail` both fit. */\nexport interface SelectableProduct {\n options: ProductOption[];\n variants: ProductVariant[];\n}\n\nexport type SelectedOptions = Record<string, string>;\n\nexport function optionsOf(variant: ProductVariant): SelectedOptions {\n return Object.fromEntries(variant.selectedOptions.map((option) => [option.name, option.value]));\n}\n\n/** The variant whose options are exactly these, or null while the selection is incomplete. */\nexport function variantFor(\n product: SelectableProduct,\n selected: SelectedOptions,\n): ProductVariant | null {\n return (\n product.variants.find((variant) =>\n variant.selectedOptions.every((option) => selected[option.name] === option.value),\n ) ?? null\n );\n}\n\n/**\n * Where selection starts: the given variant, else the first one for sale, else the first one.\n * A product with a single variant and no options is always \"selected\".\n */\nexport function initialSelection(\n product: SelectableProduct,\n variantId?: string | null,\n): SelectedOptions {\n const start =\n product.variants.find((variant) => variant.id === variantId) ??\n product.variants.find((variant) => variant.availableForSale) ??\n product.variants[0];\n return start ? optionsOf(start) : {};\n}\n\n/**\n * Whether picking `value` for `name` — keeping the other choices — lands on a variant for sale.\n * What a storefront uses to grey out a size that is sold out in the chosen colour.\n */\nexport function isOptionValueAvailable(\n product: SelectableProduct,\n selected: SelectedOptions,\n name: string,\n value: string,\n): boolean {\n const candidate = { ...selected, [name]: value };\n return product.variants.some(\n (variant) =>\n variant.availableForSale &&\n variant.selectedOptions.every(\n (option) => candidate[option.name] === undefined || candidate[option.name] === option.value,\n ),\n );\n}\n\n/**\n * Choosing a value keeps the other choices when that combination exists; otherwise it moves to the\n * closest variant that has the new value (for sale first), so the selection never dead-ends.\n */\nexport function selectOption(\n product: SelectableProduct,\n selected: SelectedOptions,\n name: string,\n value: string,\n): SelectedOptions {\n const candidate = { ...selected, [name]: value };\n if (variantFor(product, candidate) !== null) {\n return candidate;\n }\n const withValue = product.variants.filter((variant) =>\n variant.selectedOptions.some((option) => option.name === name && option.value === value),\n );\n const score = (variant: ProductVariant): number =>\n variant.selectedOptions.filter((option) => selected[option.name] === option.value).length +\n (variant.availableForSale ? 0.5 : 0);\n const best = [...withValue].sort((a, b) => score(b) - score(a))[0];\n return best ? optionsOf(best) : candidate;\n}\n","import type { StorefrontClient } from '@magicstoreai/storefront-client';\nimport { Observable, readJson, type KeyValueStorage } from './storage';\n\nexport interface WishlistState {\n /** Product ids, most recent first. */\n productIds: string[];\n /** Whose list it is: a guest's lives in this browser, a customer's on the server. */\n owner: 'guest' | 'customer';\n status: 'idle' | 'loading';\n}\n\n/** The server keeps up to this many; a guest list is capped the same. */\nexport const WISHLIST_LIMIT = 500;\n\n/**\n * Saved products. A guest's list is kept in this browser; on sign-in it moves into the customer's\n * list on the server and the local copy is cleared. Changes show at once and roll back if refused.\n */\nexport class WishlistController extends Observable<WishlistState> {\n constructor(\n private readonly client: StorefrontClient,\n private readonly storage: KeyValueStorage,\n private readonly key = 'magicstore.guest-wishlist',\n ) {\n super(\n { productIds: readJson<string[]>(storage, key) ?? [], owner: 'guest', status: 'idle' },\n { productIds: [], owner: 'guest', status: 'idle' },\n );\n }\n\n has(productId: string): boolean {\n return this.get().productIds.includes(productId);\n }\n\n async add(productId: string): Promise<void> {\n if (this.has(productId)) {\n return;\n }\n const before = this.get().productIds;\n this.setIds([productId, ...before].slice(0, WISHLIST_LIMIT));\n if (this.get().owner === 'guest') {\n return;\n }\n try {\n await this.client.customerWishlistAdd({ path: { productId } });\n } catch (error) {\n this.setIds(before);\n throw error;\n }\n }\n\n async remove(productId: string): Promise<void> {\n const before = this.get().productIds;\n this.setIds(before.filter((id) => id !== productId));\n if (this.get().owner === 'guest') {\n return;\n }\n try {\n await this.client.customerWishlistRemove({ path: { productId } });\n } catch (error) {\n this.setIds(before);\n throw error;\n }\n }\n\n toggle(productId: string): Promise<void> {\n return this.has(productId) ? this.remove(productId) : this.add(productId);\n }\n\n /** After sign-in: the guest's products join the customer's list, then the list is the server's. */\n async signedIn(): Promise<void> {\n const guest = this.get().owner === 'guest' ? this.get().productIds : [];\n this.set({ ...this.get(), owner: 'customer', status: 'loading' });\n for (const productId of [...guest].reverse()) {\n try {\n await this.client.customerWishlistAdd({ path: { productId } });\n } catch {\n // A product that is gone or hidden is simply not saved.\n }\n }\n this.storage.remove(this.key);\n await this.reload();\n }\n\n /** After sign-out: back to an empty guest list — the customer's list stays on the server. */\n signedOut(): void {\n this.storage.remove(this.key);\n this.set({ productIds: [], owner: 'guest', status: 'idle' });\n }\n\n /** The customer's list as the server has it. */\n async reload(): Promise<void> {\n if (this.get().owner === 'guest') {\n return;\n }\n const ids: string[] = [];\n for await (const product of this.client.paginate('customerWishlistIndex', {\n query: { perPage: 100 },\n })) {\n ids.push(product.id);\n if (ids.length >= WISHLIST_LIMIT) {\n break;\n }\n }\n this.set({ productIds: ids, owner: 'customer', status: 'idle' });\n }\n\n private setIds(productIds: string[]): void {\n if (this.get().owner === 'guest') {\n this.storage.set(this.key, JSON.stringify(productIds));\n }\n this.set({ ...this.get(), productIds });\n }\n}\n"],"mappings":"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","names":["import_storefront_client"]}
|
package/dist/core.d.cts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { StorefrontClient, operations, Schema, MagicStoreError } from '@magicstoreai/storefront-client';
|
|
2
|
+
|
|
3
|
+
/** Where the SDK keeps what outlives a page: the customer session, the cart id, the guest wishlist. */
|
|
4
|
+
interface KeyValueStorage {
|
|
5
|
+
get(key: string): string | null;
|
|
6
|
+
set(key: string, value: string): void;
|
|
7
|
+
remove(key: string): void;
|
|
8
|
+
}
|
|
9
|
+
/** In memory — for tests, the server, and when the browser refuses storage. */
|
|
10
|
+
declare function memoryStorage(initial?: Record<string, string>): KeyValueStorage;
|
|
11
|
+
/**
|
|
12
|
+
* `localStorage` when the browser allows it; memory otherwise (server render, private mode,
|
|
13
|
+
* blocked site data). Every access is guarded: storage can throw at any time.
|
|
14
|
+
*/
|
|
15
|
+
declare function browserStorage(): KeyValueStorage;
|
|
16
|
+
/**
|
|
17
|
+
* A tiny observable value — what the React hooks subscribe to. `serverValue` is what a server render
|
|
18
|
+
* and hydration see: storage is browser-only, so both must render the same "not known yet" state
|
|
19
|
+
* and let the stored one arrive after hydration (otherwise React reports a hydration mismatch).
|
|
20
|
+
*/
|
|
21
|
+
declare class Observable<T> {
|
|
22
|
+
private value;
|
|
23
|
+
private listeners;
|
|
24
|
+
private readonly serverValue;
|
|
25
|
+
constructor(value: T, serverValue?: T);
|
|
26
|
+
get(): T;
|
|
27
|
+
protected set(value: T): void;
|
|
28
|
+
subscribe: (listener: () => void) => (() => void);
|
|
29
|
+
getSnapshot: () => T;
|
|
30
|
+
getServerSnapshot: () => T;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type EventsBody = NonNullable<operations['analyticsEventsStore']['requestBody']>['content']['application/json'];
|
|
34
|
+
type AnalyticsEvent = EventsBody['events'][number];
|
|
35
|
+
type AnalyticsEventType = Schema<'StorefrontEventType'>;
|
|
36
|
+
type AnalyticsPayload = AnalyticsEvent['payload'];
|
|
37
|
+
/**
|
|
38
|
+
* The visitor's id for the shop's analytics and funnel: a UUID minted once per visitor and kept.
|
|
39
|
+
* The same value goes out as `X-Session-Id`, which ties carts and orders to the visit.
|
|
40
|
+
*/
|
|
41
|
+
declare function visitorSessionId(storage: KeyValueStorage, key?: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* What the visitor looks at, sent in batches (`POST /analytics/events`). Cart and checkout steps
|
|
44
|
+
* are recorded by the server — never report them. Failures are dropped: analytics must never get
|
|
45
|
+
* in the way of shopping.
|
|
46
|
+
*/
|
|
47
|
+
declare class AnalyticsController {
|
|
48
|
+
private readonly client;
|
|
49
|
+
private readonly sessionId;
|
|
50
|
+
private readonly options;
|
|
51
|
+
private queue;
|
|
52
|
+
private timer;
|
|
53
|
+
constructor(client: StorefrontClient, sessionId: string, options?: {
|
|
54
|
+
flushAfterMs?: number;
|
|
55
|
+
context?: AnalyticsPayload;
|
|
56
|
+
});
|
|
57
|
+
/** A page view — starts the session with its first-touch attribution (UTM, click ids, referrer). */
|
|
58
|
+
pageView(path: string, extra?: AnalyticsPayload): void;
|
|
59
|
+
productView(productId: string, variantId?: string | null): void;
|
|
60
|
+
collectionView(collectionId: string): void;
|
|
61
|
+
search(query: string, resultsCount?: number): void;
|
|
62
|
+
custom(name: string, properties?: Record<string, unknown>): void;
|
|
63
|
+
track(type: AnalyticsEventType, payload: AnalyticsPayload): void;
|
|
64
|
+
/** Sends what is queued now — call it when the page is hidden. */
|
|
65
|
+
flush(): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
/** The first-touch attribution a landing URL carries, for the first `pageView`. */
|
|
68
|
+
declare function attributionFrom(url: URL, referrer?: string): AnalyticsPayload;
|
|
69
|
+
|
|
70
|
+
type Cart = Schema<'Cart'>;
|
|
71
|
+
type CartLine = Schema<'CartLine'>;
|
|
72
|
+
type CartAttribute = {
|
|
73
|
+
key: string;
|
|
74
|
+
value: string | null;
|
|
75
|
+
};
|
|
76
|
+
/** A line to add: the product, its variant (null for a product without variants) and how many. */
|
|
77
|
+
interface CartLineInput {
|
|
78
|
+
productId: string;
|
|
79
|
+
variantId?: string | null;
|
|
80
|
+
quantity?: number;
|
|
81
|
+
attributes?: CartAttribute[];
|
|
82
|
+
}
|
|
83
|
+
interface CartState {
|
|
84
|
+
cart: Cart | null;
|
|
85
|
+
/** `loading` while the stored cart is fetched; `updating` while a change is on its way. */
|
|
86
|
+
status: 'idle' | 'loading' | 'updating';
|
|
87
|
+
/** The last change that failed — the cart is back to what the server holds. */
|
|
88
|
+
error: MagicStoreError | null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The visitor's cart. Its id is a capability and lives only in storage; the cart itself always
|
|
92
|
+
* comes from the server, so prices, discounts and availability are the server's. Quantity changes
|
|
93
|
+
* show at once and roll back if the server refuses them. Changes run one after another.
|
|
94
|
+
*/
|
|
95
|
+
declare class CartController extends Observable<CartState> {
|
|
96
|
+
private readonly client;
|
|
97
|
+
private readonly storage;
|
|
98
|
+
private readonly key;
|
|
99
|
+
private queue;
|
|
100
|
+
constructor(client: StorefrontClient, storage: KeyValueStorage, key?: string);
|
|
101
|
+
get cart(): Cart | null;
|
|
102
|
+
get cartId(): string | null;
|
|
103
|
+
/** Fetches the stored cart, if any. A cart that is gone is forgotten. */
|
|
104
|
+
load(): Promise<Cart | null>;
|
|
105
|
+
/** Adds lines; the first add creates the cart. */
|
|
106
|
+
addLines(lines: CartLineInput[]): Promise<Cart | null>;
|
|
107
|
+
/** The line's quantity outright; 0 removes it. Shown at once, rolled back if refused. */
|
|
108
|
+
updateLine(lineId: string, quantity: number): Promise<Cart | null>;
|
|
109
|
+
removeLine(lineId: string): Promise<Cart | null>;
|
|
110
|
+
/** The cart's codes as a whole — `[]` takes the code off. A code that does not apply is kept, flagged. */
|
|
111
|
+
setDiscountCodes(discountCodes: string[]): Promise<Cart | null>;
|
|
112
|
+
setNote(note: string | null): Promise<Cart | null>;
|
|
113
|
+
setAttributes(attributes: CartAttribute[]): Promise<Cart | null>;
|
|
114
|
+
/** The gift promotion chosen from `cart.giftOptions`; null takes it off. */
|
|
115
|
+
setGift(promotionId: string | null): Promise<Cart | null>;
|
|
116
|
+
/** Points to spend on this cart (signed-in customers). */
|
|
117
|
+
setPoints(points: number): Promise<Cart | null>;
|
|
118
|
+
/**
|
|
119
|
+
* After sign-in: the cart becomes the customer's. When they already had an open cart, this one
|
|
120
|
+
* merges into it and the answer is THAT cart — its id replaces the stored one.
|
|
121
|
+
*/
|
|
122
|
+
attachCustomer(): Promise<Cart | null>;
|
|
123
|
+
/** After sign-out: the cart is the customer's, not this visitor's any more. */
|
|
124
|
+
forget(): void;
|
|
125
|
+
/** Shows `optimistic` at once (when given), sends the change, and adopts the server's cart or rolls back. */
|
|
126
|
+
private mutate;
|
|
127
|
+
private adopt;
|
|
128
|
+
private fail;
|
|
129
|
+
private patch;
|
|
130
|
+
private enqueue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type Money = Schema<'Money'>;
|
|
134
|
+
type MoneyFormat = Schema<'Shop'>['moneyFormat'];
|
|
135
|
+
/** The currency's symbol or word in a locale: `сум`, `so‘m`, `$`. */
|
|
136
|
+
declare function currencySymbol(currencyCode: string, locale: string): string;
|
|
137
|
+
/**
|
|
138
|
+
* `12500.00` → `12 500`: groups of three separated by a space, the fraction kept only when it is
|
|
139
|
+
* not zero. The amount is never rounded here — the API already applied the shop's rounding, and
|
|
140
|
+
* the string is exactly what will be charged.
|
|
141
|
+
*/
|
|
142
|
+
declare function groupAmount(amount: string): string;
|
|
143
|
+
/**
|
|
144
|
+
* Money as the shop shows it: `shop.moneyFormat.format` places the symbol or the code, and a shop
|
|
145
|
+
* with no format uses the currency's own convention (`12 500 сум`, `$129.99`).
|
|
146
|
+
*/
|
|
147
|
+
declare function formatMoney(money: Money, options: {
|
|
148
|
+
locale: string;
|
|
149
|
+
format?: MoneyFormat | null;
|
|
150
|
+
}): string;
|
|
151
|
+
/** The amount as a number, for arithmetic you must do on the client (sorting, a progress bar). */
|
|
152
|
+
declare function moneyAmount(money: Money): number;
|
|
153
|
+
|
|
154
|
+
type CustomerSession = Schema<'CustomerSession'>;
|
|
155
|
+
type Customer = Schema<'Customer'>;
|
|
156
|
+
interface SessionState {
|
|
157
|
+
session: CustomerSession | null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The signed-in customer and their tokens. The access token lives an hour and is refreshed on
|
|
161
|
+
* demand; the refresh token works once, so concurrent callers share one refresh.
|
|
162
|
+
*/
|
|
163
|
+
declare class CustomerSessionController extends Observable<SessionState> {
|
|
164
|
+
private readonly storage;
|
|
165
|
+
private readonly key;
|
|
166
|
+
private readonly now;
|
|
167
|
+
private refreshing;
|
|
168
|
+
private client;
|
|
169
|
+
constructor(storage: KeyValueStorage, key?: string, now?: () => number);
|
|
170
|
+
/** The client the session signs in and refreshes through (created with this controller's token). */
|
|
171
|
+
attach(client: StorefrontClient): void;
|
|
172
|
+
get session(): CustomerSession | null;
|
|
173
|
+
get customer(): Customer | null;
|
|
174
|
+
/** The bearer for the next call: refreshed when it is about to expire; null when signed out. */
|
|
175
|
+
accessToken: () => Promise<string | null>;
|
|
176
|
+
/** One refresh at a time: every caller waiting meanwhile gets its result. */
|
|
177
|
+
refresh(): Promise<string | null>;
|
|
178
|
+
private doRefresh;
|
|
179
|
+
/** Texts a sign-in code. */
|
|
180
|
+
requestOtp(phone: string): Promise<Schema<'OtpChallenge'>>;
|
|
181
|
+
verifyOtp(phone: string, code: string, referralCode?: string): Promise<CustomerSession>;
|
|
182
|
+
signInWithTelegram(initData: string, referralCode?: string): Promise<CustomerSession>;
|
|
183
|
+
signInWithOq(oqToken: string): Promise<CustomerSession>;
|
|
184
|
+
signInWithClick(webSession: string): Promise<CustomerSession>;
|
|
185
|
+
/** Ends this sign-in on the server too; signed out locally whatever the server says. */
|
|
186
|
+
signOut(): Promise<void>;
|
|
187
|
+
/** The customer as the server has them now (after a profile change). */
|
|
188
|
+
reloadCustomer(): Promise<Customer | null>;
|
|
189
|
+
private signedIn;
|
|
190
|
+
private store;
|
|
191
|
+
private requireClient;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
type ProductVariant = Schema<'ProductVariant'>;
|
|
195
|
+
type ProductOption = Schema<'ProductOption'>;
|
|
196
|
+
/** What variant selection needs of a product — `Product` and `ProductDetail` both fit. */
|
|
197
|
+
interface SelectableProduct {
|
|
198
|
+
options: ProductOption[];
|
|
199
|
+
variants: ProductVariant[];
|
|
200
|
+
}
|
|
201
|
+
type SelectedOptions = Record<string, string>;
|
|
202
|
+
declare function optionsOf(variant: ProductVariant): SelectedOptions;
|
|
203
|
+
/** The variant whose options are exactly these, or null while the selection is incomplete. */
|
|
204
|
+
declare function variantFor(product: SelectableProduct, selected: SelectedOptions): ProductVariant | null;
|
|
205
|
+
/**
|
|
206
|
+
* Where selection starts: the given variant, else the first one for sale, else the first one.
|
|
207
|
+
* A product with a single variant and no options is always "selected".
|
|
208
|
+
*/
|
|
209
|
+
declare function initialSelection(product: SelectableProduct, variantId?: string | null): SelectedOptions;
|
|
210
|
+
/**
|
|
211
|
+
* Whether picking `value` for `name` — keeping the other choices — lands on a variant for sale.
|
|
212
|
+
* What a storefront uses to grey out a size that is sold out in the chosen colour.
|
|
213
|
+
*/
|
|
214
|
+
declare function isOptionValueAvailable(product: SelectableProduct, selected: SelectedOptions, name: string, value: string): boolean;
|
|
215
|
+
/**
|
|
216
|
+
* Choosing a value keeps the other choices when that combination exists; otherwise it moves to the
|
|
217
|
+
* closest variant that has the new value (for sale first), so the selection never dead-ends.
|
|
218
|
+
*/
|
|
219
|
+
declare function selectOption(product: SelectableProduct, selected: SelectedOptions, name: string, value: string): SelectedOptions;
|
|
220
|
+
|
|
221
|
+
interface WishlistState {
|
|
222
|
+
/** Product ids, most recent first. */
|
|
223
|
+
productIds: string[];
|
|
224
|
+
/** Whose list it is: a guest's lives in this browser, a customer's on the server. */
|
|
225
|
+
owner: 'guest' | 'customer';
|
|
226
|
+
status: 'idle' | 'loading';
|
|
227
|
+
}
|
|
228
|
+
/** The server keeps up to this many; a guest list is capped the same. */
|
|
229
|
+
declare const WISHLIST_LIMIT = 500;
|
|
230
|
+
/**
|
|
231
|
+
* Saved products. A guest's list is kept in this browser; on sign-in it moves into the customer's
|
|
232
|
+
* list on the server and the local copy is cleared. Changes show at once and roll back if refused.
|
|
233
|
+
*/
|
|
234
|
+
declare class WishlistController extends Observable<WishlistState> {
|
|
235
|
+
private readonly client;
|
|
236
|
+
private readonly storage;
|
|
237
|
+
private readonly key;
|
|
238
|
+
constructor(client: StorefrontClient, storage: KeyValueStorage, key?: string);
|
|
239
|
+
has(productId: string): boolean;
|
|
240
|
+
add(productId: string): Promise<void>;
|
|
241
|
+
remove(productId: string): Promise<void>;
|
|
242
|
+
toggle(productId: string): Promise<void>;
|
|
243
|
+
/** After sign-in: the guest's products join the customer's list, then the list is the server's. */
|
|
244
|
+
signedIn(): Promise<void>;
|
|
245
|
+
/** After sign-out: back to an empty guest list — the customer's list stays on the server. */
|
|
246
|
+
signedOut(): void;
|
|
247
|
+
/** The customer's list as the server has it. */
|
|
248
|
+
reload(): Promise<void>;
|
|
249
|
+
private setIds;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export { AnalyticsController, type AnalyticsEvent, type AnalyticsEventType, type AnalyticsPayload, type Cart, type CartAttribute, CartController, type CartLine, type CartLineInput, type CartState, type Customer, type CustomerSession, CustomerSessionController, type KeyValueStorage, type Money, type MoneyFormat, type ProductOption, type ProductVariant, type SelectableProduct, type SelectedOptions, type SessionState, WISHLIST_LIMIT, WishlistController, type WishlistState, attributionFrom, browserStorage, currencySymbol, formatMoney, groupAmount, initialSelection, isOptionValueAvailable, memoryStorage, moneyAmount, optionsOf, selectOption, variantFor, visitorSessionId };
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { StorefrontClient, operations, Schema, MagicStoreError } from '@magicstoreai/storefront-client';
|
|
2
|
+
|
|
3
|
+
/** Where the SDK keeps what outlives a page: the customer session, the cart id, the guest wishlist. */
|
|
4
|
+
interface KeyValueStorage {
|
|
5
|
+
get(key: string): string | null;
|
|
6
|
+
set(key: string, value: string): void;
|
|
7
|
+
remove(key: string): void;
|
|
8
|
+
}
|
|
9
|
+
/** In memory — for tests, the server, and when the browser refuses storage. */
|
|
10
|
+
declare function memoryStorage(initial?: Record<string, string>): KeyValueStorage;
|
|
11
|
+
/**
|
|
12
|
+
* `localStorage` when the browser allows it; memory otherwise (server render, private mode,
|
|
13
|
+
* blocked site data). Every access is guarded: storage can throw at any time.
|
|
14
|
+
*/
|
|
15
|
+
declare function browserStorage(): KeyValueStorage;
|
|
16
|
+
/**
|
|
17
|
+
* A tiny observable value — what the React hooks subscribe to. `serverValue` is what a server render
|
|
18
|
+
* and hydration see: storage is browser-only, so both must render the same "not known yet" state
|
|
19
|
+
* and let the stored one arrive after hydration (otherwise React reports a hydration mismatch).
|
|
20
|
+
*/
|
|
21
|
+
declare class Observable<T> {
|
|
22
|
+
private value;
|
|
23
|
+
private listeners;
|
|
24
|
+
private readonly serverValue;
|
|
25
|
+
constructor(value: T, serverValue?: T);
|
|
26
|
+
get(): T;
|
|
27
|
+
protected set(value: T): void;
|
|
28
|
+
subscribe: (listener: () => void) => (() => void);
|
|
29
|
+
getSnapshot: () => T;
|
|
30
|
+
getServerSnapshot: () => T;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type EventsBody = NonNullable<operations['analyticsEventsStore']['requestBody']>['content']['application/json'];
|
|
34
|
+
type AnalyticsEvent = EventsBody['events'][number];
|
|
35
|
+
type AnalyticsEventType = Schema<'StorefrontEventType'>;
|
|
36
|
+
type AnalyticsPayload = AnalyticsEvent['payload'];
|
|
37
|
+
/**
|
|
38
|
+
* The visitor's id for the shop's analytics and funnel: a UUID minted once per visitor and kept.
|
|
39
|
+
* The same value goes out as `X-Session-Id`, which ties carts and orders to the visit.
|
|
40
|
+
*/
|
|
41
|
+
declare function visitorSessionId(storage: KeyValueStorage, key?: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* What the visitor looks at, sent in batches (`POST /analytics/events`). Cart and checkout steps
|
|
44
|
+
* are recorded by the server — never report them. Failures are dropped: analytics must never get
|
|
45
|
+
* in the way of shopping.
|
|
46
|
+
*/
|
|
47
|
+
declare class AnalyticsController {
|
|
48
|
+
private readonly client;
|
|
49
|
+
private readonly sessionId;
|
|
50
|
+
private readonly options;
|
|
51
|
+
private queue;
|
|
52
|
+
private timer;
|
|
53
|
+
constructor(client: StorefrontClient, sessionId: string, options?: {
|
|
54
|
+
flushAfterMs?: number;
|
|
55
|
+
context?: AnalyticsPayload;
|
|
56
|
+
});
|
|
57
|
+
/** A page view — starts the session with its first-touch attribution (UTM, click ids, referrer). */
|
|
58
|
+
pageView(path: string, extra?: AnalyticsPayload): void;
|
|
59
|
+
productView(productId: string, variantId?: string | null): void;
|
|
60
|
+
collectionView(collectionId: string): void;
|
|
61
|
+
search(query: string, resultsCount?: number): void;
|
|
62
|
+
custom(name: string, properties?: Record<string, unknown>): void;
|
|
63
|
+
track(type: AnalyticsEventType, payload: AnalyticsPayload): void;
|
|
64
|
+
/** Sends what is queued now — call it when the page is hidden. */
|
|
65
|
+
flush(): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
/** The first-touch attribution a landing URL carries, for the first `pageView`. */
|
|
68
|
+
declare function attributionFrom(url: URL, referrer?: string): AnalyticsPayload;
|
|
69
|
+
|
|
70
|
+
type Cart = Schema<'Cart'>;
|
|
71
|
+
type CartLine = Schema<'CartLine'>;
|
|
72
|
+
type CartAttribute = {
|
|
73
|
+
key: string;
|
|
74
|
+
value: string | null;
|
|
75
|
+
};
|
|
76
|
+
/** A line to add: the product, its variant (null for a product without variants) and how many. */
|
|
77
|
+
interface CartLineInput {
|
|
78
|
+
productId: string;
|
|
79
|
+
variantId?: string | null;
|
|
80
|
+
quantity?: number;
|
|
81
|
+
attributes?: CartAttribute[];
|
|
82
|
+
}
|
|
83
|
+
interface CartState {
|
|
84
|
+
cart: Cart | null;
|
|
85
|
+
/** `loading` while the stored cart is fetched; `updating` while a change is on its way. */
|
|
86
|
+
status: 'idle' | 'loading' | 'updating';
|
|
87
|
+
/** The last change that failed — the cart is back to what the server holds. */
|
|
88
|
+
error: MagicStoreError | null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The visitor's cart. Its id is a capability and lives only in storage; the cart itself always
|
|
92
|
+
* comes from the server, so prices, discounts and availability are the server's. Quantity changes
|
|
93
|
+
* show at once and roll back if the server refuses them. Changes run one after another.
|
|
94
|
+
*/
|
|
95
|
+
declare class CartController extends Observable<CartState> {
|
|
96
|
+
private readonly client;
|
|
97
|
+
private readonly storage;
|
|
98
|
+
private readonly key;
|
|
99
|
+
private queue;
|
|
100
|
+
constructor(client: StorefrontClient, storage: KeyValueStorage, key?: string);
|
|
101
|
+
get cart(): Cart | null;
|
|
102
|
+
get cartId(): string | null;
|
|
103
|
+
/** Fetches the stored cart, if any. A cart that is gone is forgotten. */
|
|
104
|
+
load(): Promise<Cart | null>;
|
|
105
|
+
/** Adds lines; the first add creates the cart. */
|
|
106
|
+
addLines(lines: CartLineInput[]): Promise<Cart | null>;
|
|
107
|
+
/** The line's quantity outright; 0 removes it. Shown at once, rolled back if refused. */
|
|
108
|
+
updateLine(lineId: string, quantity: number): Promise<Cart | null>;
|
|
109
|
+
removeLine(lineId: string): Promise<Cart | null>;
|
|
110
|
+
/** The cart's codes as a whole — `[]` takes the code off. A code that does not apply is kept, flagged. */
|
|
111
|
+
setDiscountCodes(discountCodes: string[]): Promise<Cart | null>;
|
|
112
|
+
setNote(note: string | null): Promise<Cart | null>;
|
|
113
|
+
setAttributes(attributes: CartAttribute[]): Promise<Cart | null>;
|
|
114
|
+
/** The gift promotion chosen from `cart.giftOptions`; null takes it off. */
|
|
115
|
+
setGift(promotionId: string | null): Promise<Cart | null>;
|
|
116
|
+
/** Points to spend on this cart (signed-in customers). */
|
|
117
|
+
setPoints(points: number): Promise<Cart | null>;
|
|
118
|
+
/**
|
|
119
|
+
* After sign-in: the cart becomes the customer's. When they already had an open cart, this one
|
|
120
|
+
* merges into it and the answer is THAT cart — its id replaces the stored one.
|
|
121
|
+
*/
|
|
122
|
+
attachCustomer(): Promise<Cart | null>;
|
|
123
|
+
/** After sign-out: the cart is the customer's, not this visitor's any more. */
|
|
124
|
+
forget(): void;
|
|
125
|
+
/** Shows `optimistic` at once (when given), sends the change, and adopts the server's cart or rolls back. */
|
|
126
|
+
private mutate;
|
|
127
|
+
private adopt;
|
|
128
|
+
private fail;
|
|
129
|
+
private patch;
|
|
130
|
+
private enqueue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type Money = Schema<'Money'>;
|
|
134
|
+
type MoneyFormat = Schema<'Shop'>['moneyFormat'];
|
|
135
|
+
/** The currency's symbol or word in a locale: `сум`, `so‘m`, `$`. */
|
|
136
|
+
declare function currencySymbol(currencyCode: string, locale: string): string;
|
|
137
|
+
/**
|
|
138
|
+
* `12500.00` → `12 500`: groups of three separated by a space, the fraction kept only when it is
|
|
139
|
+
* not zero. The amount is never rounded here — the API already applied the shop's rounding, and
|
|
140
|
+
* the string is exactly what will be charged.
|
|
141
|
+
*/
|
|
142
|
+
declare function groupAmount(amount: string): string;
|
|
143
|
+
/**
|
|
144
|
+
* Money as the shop shows it: `shop.moneyFormat.format` places the symbol or the code, and a shop
|
|
145
|
+
* with no format uses the currency's own convention (`12 500 сум`, `$129.99`).
|
|
146
|
+
*/
|
|
147
|
+
declare function formatMoney(money: Money, options: {
|
|
148
|
+
locale: string;
|
|
149
|
+
format?: MoneyFormat | null;
|
|
150
|
+
}): string;
|
|
151
|
+
/** The amount as a number, for arithmetic you must do on the client (sorting, a progress bar). */
|
|
152
|
+
declare function moneyAmount(money: Money): number;
|
|
153
|
+
|
|
154
|
+
type CustomerSession = Schema<'CustomerSession'>;
|
|
155
|
+
type Customer = Schema<'Customer'>;
|
|
156
|
+
interface SessionState {
|
|
157
|
+
session: CustomerSession | null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The signed-in customer and their tokens. The access token lives an hour and is refreshed on
|
|
161
|
+
* demand; the refresh token works once, so concurrent callers share one refresh.
|
|
162
|
+
*/
|
|
163
|
+
declare class CustomerSessionController extends Observable<SessionState> {
|
|
164
|
+
private readonly storage;
|
|
165
|
+
private readonly key;
|
|
166
|
+
private readonly now;
|
|
167
|
+
private refreshing;
|
|
168
|
+
private client;
|
|
169
|
+
constructor(storage: KeyValueStorage, key?: string, now?: () => number);
|
|
170
|
+
/** The client the session signs in and refreshes through (created with this controller's token). */
|
|
171
|
+
attach(client: StorefrontClient): void;
|
|
172
|
+
get session(): CustomerSession | null;
|
|
173
|
+
get customer(): Customer | null;
|
|
174
|
+
/** The bearer for the next call: refreshed when it is about to expire; null when signed out. */
|
|
175
|
+
accessToken: () => Promise<string | null>;
|
|
176
|
+
/** One refresh at a time: every caller waiting meanwhile gets its result. */
|
|
177
|
+
refresh(): Promise<string | null>;
|
|
178
|
+
private doRefresh;
|
|
179
|
+
/** Texts a sign-in code. */
|
|
180
|
+
requestOtp(phone: string): Promise<Schema<'OtpChallenge'>>;
|
|
181
|
+
verifyOtp(phone: string, code: string, referralCode?: string): Promise<CustomerSession>;
|
|
182
|
+
signInWithTelegram(initData: string, referralCode?: string): Promise<CustomerSession>;
|
|
183
|
+
signInWithOq(oqToken: string): Promise<CustomerSession>;
|
|
184
|
+
signInWithClick(webSession: string): Promise<CustomerSession>;
|
|
185
|
+
/** Ends this sign-in on the server too; signed out locally whatever the server says. */
|
|
186
|
+
signOut(): Promise<void>;
|
|
187
|
+
/** The customer as the server has them now (after a profile change). */
|
|
188
|
+
reloadCustomer(): Promise<Customer | null>;
|
|
189
|
+
private signedIn;
|
|
190
|
+
private store;
|
|
191
|
+
private requireClient;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
type ProductVariant = Schema<'ProductVariant'>;
|
|
195
|
+
type ProductOption = Schema<'ProductOption'>;
|
|
196
|
+
/** What variant selection needs of a product — `Product` and `ProductDetail` both fit. */
|
|
197
|
+
interface SelectableProduct {
|
|
198
|
+
options: ProductOption[];
|
|
199
|
+
variants: ProductVariant[];
|
|
200
|
+
}
|
|
201
|
+
type SelectedOptions = Record<string, string>;
|
|
202
|
+
declare function optionsOf(variant: ProductVariant): SelectedOptions;
|
|
203
|
+
/** The variant whose options are exactly these, or null while the selection is incomplete. */
|
|
204
|
+
declare function variantFor(product: SelectableProduct, selected: SelectedOptions): ProductVariant | null;
|
|
205
|
+
/**
|
|
206
|
+
* Where selection starts: the given variant, else the first one for sale, else the first one.
|
|
207
|
+
* A product with a single variant and no options is always "selected".
|
|
208
|
+
*/
|
|
209
|
+
declare function initialSelection(product: SelectableProduct, variantId?: string | null): SelectedOptions;
|
|
210
|
+
/**
|
|
211
|
+
* Whether picking `value` for `name` — keeping the other choices — lands on a variant for sale.
|
|
212
|
+
* What a storefront uses to grey out a size that is sold out in the chosen colour.
|
|
213
|
+
*/
|
|
214
|
+
declare function isOptionValueAvailable(product: SelectableProduct, selected: SelectedOptions, name: string, value: string): boolean;
|
|
215
|
+
/**
|
|
216
|
+
* Choosing a value keeps the other choices when that combination exists; otherwise it moves to the
|
|
217
|
+
* closest variant that has the new value (for sale first), so the selection never dead-ends.
|
|
218
|
+
*/
|
|
219
|
+
declare function selectOption(product: SelectableProduct, selected: SelectedOptions, name: string, value: string): SelectedOptions;
|
|
220
|
+
|
|
221
|
+
interface WishlistState {
|
|
222
|
+
/** Product ids, most recent first. */
|
|
223
|
+
productIds: string[];
|
|
224
|
+
/** Whose list it is: a guest's lives in this browser, a customer's on the server. */
|
|
225
|
+
owner: 'guest' | 'customer';
|
|
226
|
+
status: 'idle' | 'loading';
|
|
227
|
+
}
|
|
228
|
+
/** The server keeps up to this many; a guest list is capped the same. */
|
|
229
|
+
declare const WISHLIST_LIMIT = 500;
|
|
230
|
+
/**
|
|
231
|
+
* Saved products. A guest's list is kept in this browser; on sign-in it moves into the customer's
|
|
232
|
+
* list on the server and the local copy is cleared. Changes show at once and roll back if refused.
|
|
233
|
+
*/
|
|
234
|
+
declare class WishlistController extends Observable<WishlistState> {
|
|
235
|
+
private readonly client;
|
|
236
|
+
private readonly storage;
|
|
237
|
+
private readonly key;
|
|
238
|
+
constructor(client: StorefrontClient, storage: KeyValueStorage, key?: string);
|
|
239
|
+
has(productId: string): boolean;
|
|
240
|
+
add(productId: string): Promise<void>;
|
|
241
|
+
remove(productId: string): Promise<void>;
|
|
242
|
+
toggle(productId: string): Promise<void>;
|
|
243
|
+
/** After sign-in: the guest's products join the customer's list, then the list is the server's. */
|
|
244
|
+
signedIn(): Promise<void>;
|
|
245
|
+
/** After sign-out: back to an empty guest list — the customer's list stays on the server. */
|
|
246
|
+
signedOut(): void;
|
|
247
|
+
/** The customer's list as the server has it. */
|
|
248
|
+
reload(): Promise<void>;
|
|
249
|
+
private setIds;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export { AnalyticsController, type AnalyticsEvent, type AnalyticsEventType, type AnalyticsPayload, type Cart, type CartAttribute, CartController, type CartLine, type CartLineInput, type CartState, type Customer, type CustomerSession, CustomerSessionController, type KeyValueStorage, type Money, type MoneyFormat, type ProductOption, type ProductVariant, type SelectableProduct, type SelectedOptions, type SessionState, WISHLIST_LIMIT, WishlistController, type WishlistState, attributionFrom, browserStorage, currencySymbol, formatMoney, groupAmount, initialSelection, isOptionValueAvailable, memoryStorage, moneyAmount, optionsOf, selectOption, variantFor, visitorSessionId };
|