@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 ADDED
@@ -0,0 +1,931 @@
1
+ # `@magicstoreai/hydrogen` catalogue
2
+
3
+ <!-- Generated by `pnpm --filter @magicstoreai/hydrogen catalogue` from the TypeScript sources — do not edit. -->
4
+
5
+ Every public export, by entry point: what it is, how to call it, an example, and the Storefront
6
+ API operations it calls (`operationId`s of `@magicstoreai/storefront-client`). Import only from
7
+ these entry points — anything else is internal and may change in any release.
8
+
9
+ ## `@magicstoreai/hydrogen`
10
+
11
+ React bindings (a `"use client"` module). Also re-exports everything in `/core` and `MagicStoreError`.
12
+
13
+ ### Components
14
+
15
+ #### `<Image>`
16
+
17
+ An API image with lazy loading by default. Its intrinsic size is set (no layout shift) unless
18
+ you pass `width` / `height` to draw it at another size, e.g. a 64×64 cart thumbnail.
19
+
20
+ | Prop | Type | Description |
21
+ | --- | --- | --- |
22
+ | `data` | `Schema<'Image'> \| null \| undefined` | |
23
+ | `alt?` | `string` | Used when the image has no alt text of its own. |
24
+ | `fallback?` | `ReactNode` | |
25
+
26
+ Any other prop is passed to the rendered element.
27
+
28
+ ```tsx
29
+ <Image data={product.featuredImage} alt={product.title} fallback={<div className="noimg" />} />
30
+ ```
31
+
32
+ #### `<MagicStoreProvider>`
33
+
34
+ The root of a storefront: one client, and the customer, cart, wishlist and analytics built on it.
35
+ On sign-in the cart becomes the customer's and the guest wishlist moves into their list; on
36
+ sign-out both are left behind with the customer.
37
+
38
+ | Prop | Type | Description |
39
+ | --- | --- | --- |
40
+ | `locale?` | `string` | The language the storefront renders in; the shop's default when omitted. |
41
+ | `shop?` | `Shop` | The shop, when the page already has it (server-rendered) — saves a `/shop` call. |
42
+ | `storage?` | `KeyValueStorage` | Where the session, cart id and guest wishlist are kept; `localStorage` by default. |
43
+ | `children` | `ReactNode` | |
44
+ | `shopDomain?` | `string` | The shop's storefront domain, e.g. `shop.example.uz` → `https://shop.example.uz/api/v2/storefront`. |
45
+ | `baseUrl?` | `string` | The full API base URL, instead of `shopDomain`. |
46
+ | `storefrontToken?` | `string` | `X-Storefront-Token` — identifies the shop when the host is not one of its domains. |
47
+ | `fetch?` | `typeof globalThis.fetch` | A `fetch` implementation; the global one by default. |
48
+ | `retry?` | `RetryOptions \| false` | Retry policy for `GET`s answered `429` or `5xx`, or not answered at all. Writes are never retried by the client. `false` turns retries off. |
49
+
50
+ API operations: `shop`.
51
+
52
+ ```tsx
53
+ // app/providers.tsx ('use client'); `shop` comes from a server-side `api.shop()`.
54
+ <MagicStoreProvider shopDomain="shop.example.uz" shop={shop} locale="ru">
55
+ {children}
56
+ </MagicStoreProvider>
57
+ ```
58
+
59
+ #### `<Money>`
60
+
61
+ A price as the shop shows it (`shop.moneyFormat`, the storefront's locale).
62
+
63
+ | Prop | Type | Description |
64
+ | --- | --- | --- |
65
+ | `data` | `MoneyValue \| null \| undefined` | |
66
+ | `as?` | `As` | |
67
+ | `fallback?` | `ReactNode` | What to render for a missing price (the API sends null for "unknown", never 0). |
68
+
69
+ ```tsx
70
+ <Money data={product.price} fallback={<span>—</span>} />
71
+ ```
72
+
73
+ #### `<Pagination>`
74
+
75
+ Headless pagination over `meta.pagination`: you render the links, it does the arithmetic.
76
+
77
+ | Prop | Type | Description |
78
+ | --- | --- | --- |
79
+ | `meta` | `Pagination` | |
80
+ | `around?` | `number` | |
81
+ | `children` | `(state: PaginationState) => ReactNode` | |
82
+
83
+ ```tsx
84
+ <Pagination meta={products.meta.pagination}>
85
+ {({ pages }) =>
86
+ pages.map((page, i) =>
87
+ page === null ? <span key={i}>…</span> : <a key={i} href={`?page=${page}`}>{page}</a>,
88
+ )
89
+ }
90
+ </Pagination>
91
+ ```
92
+
93
+ #### `<ProductProvider>`
94
+
95
+ Shares one product's variant selection with everything under it (options, price, add-to-cart).
96
+
97
+ | Prop | Type | Description |
98
+ | --- | --- | --- |
99
+ | `product` | `P` | |
100
+ | `initialVariantId?` | `string \| null` | |
101
+ | `children` | `ReactNode` | |
102
+
103
+ ```tsx
104
+ <ProductProvider product={product}>
105
+ <VariantPicker />
106
+ <AddToCart />
107
+ </ProductProvider>
108
+ ```
109
+
110
+ ### Hooks
111
+
112
+ #### `useAnalytics`
113
+
114
+ Reports what the visitor looks at. Stable across renders.
115
+
116
+ ```ts
117
+ function useAnalytics()
118
+ ```
119
+
120
+ | Returns | Type | Description |
121
+ | --- | --- | --- |
122
+ | `queue` | `AnalyticsEvent[]` | |
123
+ | `timer` | `ReturnType<typeof setTimeout> \| null` | |
124
+ | `client` | `StorefrontClient` | |
125
+ | `sessionId` | `string` | |
126
+ | `options` | `{ flushAfterMs?: number; context?: AnalyticsPayload }` | |
127
+ | `pageView` | `void` | A page view — starts the session with its first-touch attribution (UTM, click ids, referrer). |
128
+ | `productView` | `void` | |
129
+ | `collectionView` | `void` | |
130
+ | `search` | `void` | |
131
+ | `custom` | `void` | |
132
+ | `track` | `void` | |
133
+ | `flush` | `Promise<void>` | Sends what is queued now — call it when the page is hidden. |
134
+
135
+ API operations: `analyticsEventsStore`.
136
+
137
+ ```tsx
138
+ const analytics = useAnalytics();
139
+ useEffect(() => analytics.productView(product.id), [analytics, product.id]);
140
+ ```
141
+
142
+ #### `useCart`
143
+
144
+ The visitor's cart and every change to it. The first `addLines` creates the cart.
145
+
146
+ ```ts
147
+ function useCart()
148
+ ```
149
+
150
+ | Returns | Type | Description |
151
+ | --- | --- | --- |
152
+ | `totalQuantity` | `number` | |
153
+ | `addLines` | `(lines: CartLineInput[]) => Promise<Cart \| null>` | |
154
+ | `addLine` | `(line: CartLineInput) => Promise<Cart \| null>` | |
155
+ | `updateLine` | `(lineId: string, quantity: number) => Promise<Cart \| null>` | |
156
+ | `removeLine` | `(lineId: string) => Promise<Cart \| null>` | |
157
+ | `setDiscountCodes` | `(codes: string[]) => Promise<Cart \| null>` | |
158
+ | `setNote` | `(note: string \| null) => Promise<Cart \| null>` | |
159
+ | `setAttributes` | `(attributes: CartAttribute[]) => Promise<Cart \| null>` | |
160
+ | `setGift` | `(promotionId: string \| null) => Promise<Cart \| null>` | |
161
+ | `setPoints` | `(points: number) => Promise<Cart \| null>` | |
162
+ | `cart` | `Cart \| null` | |
163
+ | `status` | `'idle' \| 'loading' \| 'updating'` | `loading` while the stored cart is fetched; `updating` while a change is on its way. |
164
+ | `error` | `MagicStoreError \| null` | The last change that failed — the cart is back to what the server holds. |
165
+
166
+ API operations: `cartsAttributes`, `cartsBuyerIdentity`, `cartsDiscountCodes`, `cartsGift`, `cartsLinesDestroy`, `cartsLinesStore`, `cartsLinesUpdate`, `cartsNote`, `cartsPoints`, `cartsShow`, `cartsStore`.
167
+
168
+ ```tsx
169
+ const { cart, totalQuantity, status, error, addLine } = useCart();
170
+ await addLine({ productId: product.id, variantId: selectedVariant?.id ?? null, quantity: 1 });
171
+ ```
172
+
173
+ #### `useCustomer`
174
+
175
+ The signed-in customer and every way to sign in and out.
176
+
177
+ ```ts
178
+ function useCustomer()
179
+ ```
180
+
181
+ | Returns | Type | Description |
182
+ | --- | --- | --- |
183
+ | `customer` | `{ createdAt: string \| null; deletionRequestedAt: string \| null; gender: "MALE" \| "FEMALE" \| null; id: string; locale: string \| null; name: string \| null; phone: string \| null; referralCode: string \| null; } \| null` | |
184
+ | `isSignedIn` | `boolean` | |
185
+ | `requestOtp` | `(phone: string) => Promise<Schema<'OtpChallenge'>>` | |
186
+ | `verifyOtp` | `(phone: string, code: string, referralCode?: string) => Promise<CustomerSession>` | |
187
+ | `signInWithTelegram` | `(initData: string, referralCode?: string) => Promise<CustomerSession>` | |
188
+ | `signInWithOq` | `(oqToken: string) => Promise<CustomerSession>` | |
189
+ | `signInWithClick` | `(webSession: string) => Promise<CustomerSession>` | |
190
+ | `signOut` | `() => Promise<void>` | |
191
+ | `reload` | `() => Promise<Customer \| null>` | |
192
+
193
+ API operations: `authClick`, `authOq`, `authOtp`, `authOtpVerification`, `authTelegram`, `authTokenDestroy`, `authTokenRefresh`, `customerShow`.
194
+
195
+ ```tsx
196
+ const { customer, isSignedIn, requestOtp, verifyOtp, signOut } = useCustomer();
197
+ await requestOtp('+998901234567');
198
+ await verifyOtp('+998901234567', code);
199
+ ```
200
+
201
+ #### `useMagicStore`
202
+
203
+ Everything the provider holds: client, shop, locale and the controllers. Throws outside
204
+ `<MagicStoreProvider>`.
205
+
206
+ ```ts
207
+ function useMagicStore(): MagicStore
208
+ ```
209
+
210
+ ```tsx
211
+ const { locale, sessionId } = useMagicStore();
212
+ ```
213
+
214
+ #### `useProduct`
215
+
216
+ The product and variant selection of the nearest `<ProductProvider>`; throws outside one.
217
+
218
+ ```ts
219
+ function useProduct(): ProductContextValue
220
+ ```
221
+
222
+ ```tsx
223
+ const { product, selectedVariant } = useProduct();
224
+ return <Money data={selectedVariant?.price ?? product.price} />;
225
+ ```
226
+
227
+ #### `useShop`
228
+
229
+ The shop (`GET /shop`): name, currency, money format, locales, features, branding…
230
+
231
+ ```ts
232
+ function useShop(): Shop | null
233
+ ```
234
+
235
+ ```tsx
236
+ const shop = useShop();
237
+ return <span>{shop?.name}</span>;
238
+ ```
239
+
240
+ #### `useStorefrontClient`
241
+
242
+ The typed API client, for anything the hooks do not cover.
243
+
244
+ ```ts
245
+ function useStorefrontClient(): StorefrontClient
246
+ ```
247
+
248
+ ```tsx
249
+ const client = useStorefrontClient();
250
+ const { data } = await client.searchSuggestions({ query: { q } });
251
+ ```
252
+
253
+ #### `useVariantSelection`
254
+
255
+ Options → variant. `selectedVariant` is null only while a choice is missing; `isAvailable` says
256
+ whether a value leads to a variant for sale given the other choices.
257
+
258
+ ```ts
259
+ function useVariantSelection(product: SelectableProduct, initialVariantId?: string | null)
260
+ ```
261
+
262
+ | Returns | Type | Description |
263
+ | --- | --- | --- |
264
+ | `selectedOptions` | `SelectedOptions` | |
265
+ | `selectedVariant` | `{ availableForSale: boolean; barcode: string \| null; compareAtPrice: { amount: string; currencyCode: string; } \| null; gtin: string \| null; id: string; image: { altText: string \| null; height: number \| null; url: string; width: number \| null; } \| null; ... 7 more ...; weight: { ...; } \| null; } \| null` | |
266
+ | `setOption` | `(name: string, value: string) => void` | |
267
+ | `isAvailable` | `(name: string, value: string) => boolean` | |
268
+
269
+ ```tsx
270
+ const { selectedOptions, selectedVariant, setOption, isAvailable } = useVariantSelection(product);
271
+ <button disabled={!isAvailable('Size', 'M')} onClick={() => setOption('Size', 'M')}>M</button>
272
+ ```
273
+
274
+ #### `useWishlist`
275
+
276
+ Saved products: a guest's in this browser, a customer's on the server (merged at sign-in).
277
+
278
+ ```ts
279
+ function useWishlist()
280
+ ```
281
+
282
+ | Returns | Type | Description |
283
+ | --- | --- | --- |
284
+ | `has` | `(productId: string) => boolean` | |
285
+ | `add` | `(productId: string) => Promise<void>` | |
286
+ | `remove` | `(productId: string) => Promise<void>` | |
287
+ | `toggle` | `(productId: string) => Promise<void>` | |
288
+ | `productIds` | `string[]` | Product ids, most recent first. |
289
+ | `owner` | `'guest' \| 'customer'` | Whose list it is: a guest's lives in this browser, a customer's on the server. |
290
+ | `status` | `'idle' \| 'loading'` | |
291
+
292
+ API operations: `customerWishlistAdd`, `customerWishlistRemove`.
293
+
294
+ ```tsx
295
+ const wishlist = useWishlist();
296
+ <button aria-pressed={wishlist.has(product.id)} onClick={() => wishlist.toggle(product.id)}>♥</button>
297
+ ```
298
+
299
+ ### Classes
300
+
301
+ #### `MagicStoreError`
302
+
303
+ Every failed call. `code` is what to branch on — never the message, which is localized.
304
+
305
+ ```ts
306
+ try {
307
+ await client.checkoutsCompletion({ path: { id } });
308
+ } catch (error) {
309
+ if (error instanceof MagicStoreError && error.code === 'CHECKOUT_NOT_READY') { … }
310
+ }
311
+ ```
312
+
313
+ Re-exported from `@magicstoreai/storefront-client`.
314
+
315
+ ### Functions
316
+
317
+ #### `paginationState`
318
+
319
+ ```ts
320
+ function paginationState(meta: Pagination, around = 1): PaginationState
321
+ ```
322
+
323
+ ### Types
324
+
325
+ #### `MagicStore`
326
+
327
+ ```ts
328
+ export interface MagicStore {
329
+ client: StorefrontClient;
330
+ shop: Shop | null;
331
+ locale: string;
332
+ session: CustomerSessionController;
333
+ cart: CartController;
334
+ wishlist: WishlistController;
335
+ analytics: AnalyticsController;
336
+ sessionId: string;
337
+ }
338
+ ```
339
+
340
+ #### `MagicStoreProviderProps`
341
+
342
+ ```ts
343
+ export interface MagicStoreProviderProps extends Pick<StorefrontClientOptions, 'shopDomain' | 'baseUrl' | 'storefrontToken' | 'fetch' | 'retry'> {
344
+ locale?: string;
345
+ shop?: Shop;
346
+ storage?: KeyValueStorage;
347
+ children: ReactNode;
348
+ }
349
+ ```
350
+
351
+ #### `PaginationState`
352
+
353
+ ```ts
354
+ export interface PaginationState {
355
+ page: number;
356
+ totalPages: number;
357
+ hasPreviousPage: boolean;
358
+ hasNextPage: boolean;
359
+ previousPage: number | null;
360
+ nextPage: number | null;
361
+ pages: Array<number | null>;
362
+ }
363
+ ```
364
+
365
+ #### `Shop`
366
+
367
+ ```ts
368
+ export type Shop = Schema<'Shop'>;
369
+ ```
370
+
371
+ ## `@magicstoreai/hydrogen/core`
372
+
373
+ Framework-free controllers and helpers, safe on the server and in any UI library.
374
+
375
+ ### Classes
376
+
377
+ #### `AnalyticsController`
378
+
379
+ What the visitor looks at, sent in batches (`POST /analytics/events`). Cart and checkout steps
380
+ are recorded by the server — never report them. Failures are dropped: analytics must never get
381
+ in the way of shopping.
382
+
383
+ ```ts
384
+ class AnalyticsController {
385
+ constructor(private readonly client: StorefrontClient, private readonly sessionId: string, private readonly options: {
386
+ flushAfterMs?: number;
387
+ context?: AnalyticsPayload;
388
+ } = {});
389
+ pageView(path: string, extra: AnalyticsPayload = {}): void;
390
+ productView(productId: string, variantId?: string | null): void;
391
+ collectionView(collectionId: string): void;
392
+ search(query: string, resultsCount?: number): void;
393
+ custom(name: string, properties?: Record<string, unknown>): void;
394
+ track(type: AnalyticsEventType, payload: AnalyticsPayload): void;
395
+ async flush(): Promise<void>;
396
+ }
397
+ ```
398
+
399
+ API operations: `analyticsEventsStore`.
400
+
401
+ #### `CartController`
402
+
403
+ The visitor's cart. Its id is a capability and lives only in storage; the cart itself always
404
+ comes from the server, so prices, discounts and availability are the server's. Quantity changes
405
+ show at once and roll back if the server refuses them. Changes run one after another.
406
+
407
+ ```ts
408
+ class CartController {
409
+ constructor(private readonly client: StorefrontClient, private readonly storage: KeyValueStorage, private readonly key = 'magicstore.cart-id');
410
+ get cart(): Cart | null;
411
+ get cartId(): string | null;
412
+ load(): Promise<Cart | null>;
413
+ addLines(lines: CartLineInput[]): Promise<Cart | null>;
414
+ updateLine(lineId: string, quantity: number): Promise<Cart | null>;
415
+ removeLine(lineId: string): Promise<Cart | null>;
416
+ setDiscountCodes(discountCodes: string[]): Promise<Cart | null>;
417
+ setNote(note: string | null): Promise<Cart | null>;
418
+ setAttributes(attributes: CartAttribute[]): Promise<Cart | null>;
419
+ setGift(promotionId: string | null): Promise<Cart | null>;
420
+ setPoints(points: number): Promise<Cart | null>;
421
+ attachCustomer(): Promise<Cart | null>;
422
+ forget(): void;
423
+ }
424
+ ```
425
+
426
+ API operations: `cartsAttributes`, `cartsBuyerIdentity`, `cartsDiscountCodes`, `cartsGift`, `cartsLinesDestroy`, `cartsLinesStore`, `cartsLinesUpdate`, `cartsNote`, `cartsPoints`, `cartsShow`, `cartsStore`.
427
+
428
+ #### `CustomerSessionController`
429
+
430
+ The signed-in customer and their tokens. The access token lives an hour and is refreshed on
431
+ demand; the refresh token works once, so concurrent callers share one refresh.
432
+
433
+ ```ts
434
+ class CustomerSessionController {
435
+ constructor(private readonly storage: KeyValueStorage, private readonly key = 'magicstore.customer-session', private readonly now: () => number = () => Date.now());
436
+ attach(client: StorefrontClient): void;
437
+ get session(): CustomerSession | null;
438
+ get customer(): Customer | null;
439
+ accessToken;
440
+ refresh(): Promise<string | null>;
441
+ async requestOtp(phone: string): Promise<Schema<'OtpChallenge'>>;
442
+ async verifyOtp(phone: string, code: string, referralCode?: string): Promise<CustomerSession>;
443
+ async signInWithTelegram(initData: string, referralCode?: string): Promise<CustomerSession>;
444
+ async signInWithOq(oqToken: string): Promise<CustomerSession>;
445
+ async signInWithClick(webSession: string): Promise<CustomerSession>;
446
+ async signOut(): Promise<void>;
447
+ async reloadCustomer(): Promise<Customer | null>;
448
+ }
449
+ ```
450
+
451
+ API operations: `authClick`, `authOq`, `authOtp`, `authOtpVerification`, `authTelegram`, `authTokenDestroy`, `authTokenRefresh`, `customerShow`.
452
+
453
+ #### `WishlistController`
454
+
455
+ Saved products. A guest's list is kept in this browser; on sign-in it moves into the customer's
456
+ list on the server and the local copy is cleared. Changes show at once and roll back if refused.
457
+
458
+ ```ts
459
+ class WishlistController {
460
+ constructor(private readonly client: StorefrontClient, private readonly storage: KeyValueStorage, private readonly key = 'magicstore.guest-wishlist');
461
+ has(productId: string): boolean;
462
+ async add(productId: string): Promise<void>;
463
+ async remove(productId: string): Promise<void>;
464
+ toggle(productId: string): Promise<void>;
465
+ async signedIn(): Promise<void>;
466
+ signedOut(): void;
467
+ async reload(): Promise<void>;
468
+ }
469
+ ```
470
+
471
+ API operations: `customerWishlistAdd`, `customerWishlistRemove`.
472
+
473
+ ### Functions
474
+
475
+ #### `attributionFrom`
476
+
477
+ The first-touch attribution a landing URL carries, for the first `pageView`.
478
+
479
+ ```ts
480
+ function attributionFrom(url: URL, referrer?: string): AnalyticsPayload
481
+ ```
482
+
483
+ #### `browserStorage`
484
+
485
+ `localStorage` when the browser allows it; memory otherwise (server render, private mode,
486
+ blocked site data). Every access is guarded: storage can throw at any time.
487
+
488
+ ```ts
489
+ function browserStorage(): KeyValueStorage
490
+ ```
491
+
492
+ #### `currencySymbol`
493
+
494
+ The currency's symbol or word in a locale: `сум`, `so‘m`, `$`.
495
+
496
+ ```ts
497
+ function currencySymbol(currencyCode: string, locale: string): string
498
+ ```
499
+
500
+ #### `formatMoney`
501
+
502
+ Money as the shop shows it: `shop.moneyFormat.format` places the symbol or the code, and a shop
503
+ with no format uses the currency's own convention (`12 500 сум`, `$129.99`).
504
+
505
+ ```ts
506
+ function formatMoney(money: Money, options: {
507
+ locale: string;
508
+ format?: MoneyFormat | null;
509
+ }): string
510
+ ```
511
+
512
+ #### `groupAmount`
513
+
514
+ `12500.00` → `12 500`: groups of three separated by a space, the fraction kept only when it is
515
+ not zero. The amount is never rounded here — the API already applied the shop's rounding, and
516
+ the string is exactly what will be charged.
517
+
518
+ ```ts
519
+ function groupAmount(amount: string): string
520
+ ```
521
+
522
+ #### `initialSelection`
523
+
524
+ Where selection starts: the given variant, else the first one for sale, else the first one.
525
+ A product with a single variant and no options is always "selected".
526
+
527
+ ```ts
528
+ function initialSelection(product: SelectableProduct, variantId?: string | null): SelectedOptions
529
+ ```
530
+
531
+ #### `isOptionValueAvailable`
532
+
533
+ Whether picking `value` for `name` — keeping the other choices — lands on a variant for sale.
534
+ What a storefront uses to grey out a size that is sold out in the chosen colour.
535
+
536
+ ```ts
537
+ function isOptionValueAvailable(product: SelectableProduct, selected: SelectedOptions, name: string, value: string): boolean
538
+ ```
539
+
540
+ #### `memoryStorage`
541
+
542
+ In memory — for tests, the server, and when the browser refuses storage.
543
+
544
+ ```ts
545
+ function memoryStorage(initial: Record<string, string> = {}): KeyValueStorage
546
+ ```
547
+
548
+ #### `moneyAmount`
549
+
550
+ The amount as a number, for arithmetic you must do on the client (sorting, a progress bar).
551
+
552
+ ```ts
553
+ function moneyAmount(money: Money): number
554
+ ```
555
+
556
+ #### `optionsOf`
557
+
558
+ ```ts
559
+ function optionsOf(variant: ProductVariant): SelectedOptions
560
+ ```
561
+
562
+ #### `selectOption`
563
+
564
+ Choosing a value keeps the other choices when that combination exists; otherwise it moves to the
565
+ closest variant that has the new value (for sale first), so the selection never dead-ends.
566
+
567
+ ```ts
568
+ function selectOption(product: SelectableProduct, selected: SelectedOptions, name: string, value: string): SelectedOptions
569
+ ```
570
+
571
+ #### `variantFor`
572
+
573
+ The variant whose options are exactly these, or null while the selection is incomplete.
574
+
575
+ ```ts
576
+ function variantFor(product: SelectableProduct, selected: SelectedOptions): ProductVariant | null
577
+ ```
578
+
579
+ #### `visitorSessionId`
580
+
581
+ The visitor's id for the shop's analytics and funnel: a UUID minted once per visitor and kept.
582
+ The same value goes out as `X-Session-Id`, which ties carts and orders to the visit.
583
+
584
+ ```ts
585
+ function visitorSessionId(storage: KeyValueStorage, key = 'magicstore.session-id'): string
586
+ ```
587
+
588
+ ### Constants
589
+
590
+ #### `WISHLIST_LIMIT`
591
+
592
+ The server keeps up to this many; a guest list is capped the same.
593
+
594
+ ```ts
595
+ const WISHLIST_LIMIT = 500
596
+ ```
597
+
598
+ ### Types
599
+
600
+ #### `AnalyticsEvent`
601
+
602
+ ```ts
603
+ export type AnalyticsEvent = EventsBody['events'][number];
604
+ ```
605
+
606
+ #### `AnalyticsEventType`
607
+
608
+ ```ts
609
+ export type AnalyticsEventType = Schema<'StorefrontEventType'>;
610
+ ```
611
+
612
+ #### `AnalyticsPayload`
613
+
614
+ ```ts
615
+ export type AnalyticsPayload = AnalyticsEvent['payload'];
616
+ ```
617
+
618
+ #### `Cart`
619
+
620
+ ```ts
621
+ export type Cart = Schema<'Cart'>;
622
+ ```
623
+
624
+ #### `CartAttribute`
625
+
626
+ ```ts
627
+ export type CartAttribute = {
628
+ key: string;
629
+ value: string | null;
630
+ };
631
+ ```
632
+
633
+ #### `CartLine`
634
+
635
+ ```ts
636
+ export type CartLine = Schema<'CartLine'>;
637
+ ```
638
+
639
+ #### `CartLineInput`
640
+
641
+ A line to add: the product, its variant (null for a product without variants) and how many.
642
+
643
+ ```ts
644
+ export interface CartLineInput {
645
+ productId: string;
646
+ variantId?: string | null;
647
+ quantity?: number;
648
+ attributes?: CartAttribute[];
649
+ }
650
+ ```
651
+
652
+ #### `CartState`
653
+
654
+ ```ts
655
+ export interface CartState {
656
+ cart: Cart | null;
657
+ status: 'idle' | 'loading' | 'updating';
658
+ error: MagicStoreError | null;
659
+ }
660
+ ```
661
+
662
+ #### `Customer`
663
+
664
+ ```ts
665
+ export type Customer = Schema<'Customer'>;
666
+ ```
667
+
668
+ #### `CustomerSession`
669
+
670
+ ```ts
671
+ export type CustomerSession = Schema<'CustomerSession'>;
672
+ ```
673
+
674
+ #### `KeyValueStorage`
675
+
676
+ Where the SDK keeps what outlives a page: the customer session, the cart id, the guest wishlist.
677
+
678
+ ```ts
679
+ export interface KeyValueStorage {
680
+ get(key: string): string | null;
681
+ set(key: string, value: string): void;
682
+ remove(key: string): void;
683
+ }
684
+ ```
685
+
686
+ #### `Money`
687
+
688
+ ```ts
689
+ export type Money = Schema<'Money'>;
690
+ ```
691
+
692
+ #### `MoneyFormat`
693
+
694
+ ```ts
695
+ export type MoneyFormat = Schema<'Shop'>['moneyFormat'];
696
+ ```
697
+
698
+ #### `ProductOption`
699
+
700
+ ```ts
701
+ export type ProductOption = Schema<'ProductOption'>;
702
+ ```
703
+
704
+ #### `ProductVariant`
705
+
706
+ ```ts
707
+ export type ProductVariant = Schema<'ProductVariant'>;
708
+ ```
709
+
710
+ #### `SelectableProduct`
711
+
712
+ What variant selection needs of a product — `Product` and `ProductDetail` both fit.
713
+
714
+ ```ts
715
+ export interface SelectableProduct {
716
+ options: ProductOption[];
717
+ variants: ProductVariant[];
718
+ }
719
+ ```
720
+
721
+ #### `SelectedOptions`
722
+
723
+ ```ts
724
+ export type SelectedOptions = Record<string, string>;
725
+ ```
726
+
727
+ #### `SessionState`
728
+
729
+ ```ts
730
+ export interface SessionState {
731
+ session: CustomerSession | null;
732
+ }
733
+ ```
734
+
735
+ #### `WishlistState`
736
+
737
+ ```ts
738
+ export interface WishlistState {
739
+ productIds: string[];
740
+ owner: 'guest' | 'customer';
741
+ status: 'idle' | 'loading';
742
+ }
743
+ ```
744
+
745
+ ## `@magicstoreai/hydrogen/server`
746
+
747
+ Server only: webhook verification and Next.js cache tags.
748
+
749
+ ### Functions
750
+
751
+ #### `createWebhookHandler`
752
+
753
+ A route handler for the platform's webhook: `export const POST = createWebhookHandler({ secret,
754
+ revalidateTag })` in `app/api/magicstore/webhook/route.ts`. Answers 401 on a bad signature, 400
755
+ on a body that is not an event, 200 otherwise — including for a topic it does not know. Events
756
+ seen before (same id, this instance) are acknowledged and skipped.
757
+
758
+ ```ts
759
+ function createWebhookHandler(options: WebhookHandlerOptions): (request: Request) => Promise<Response>
760
+ ```
761
+
762
+ #### `nextCacheFetch`
763
+
764
+ A `fetch` for `createStorefrontClient` on a Next.js server: public GETs are cached under the tag
765
+ a webhook revalidates (and at most `revalidate` seconds); everything else is `no-store`.
766
+
767
+ ```ts
768
+ const client = createStorefrontClient({ shopDomain, fetch: nextCacheFetch() });
769
+ ```
770
+
771
+ ```ts
772
+ function nextCacheFetch(options: {
773
+ revalidate?: number | false;
774
+ fetch?: typeof globalThis.fetch;
775
+ } = {}): typeof globalThis.fetch
776
+ ```
777
+
778
+ #### `tagForPath`
779
+
780
+ The cache tag of an API path — what a webhook topic will invalidate. Personal and live data
781
+ (customer, cart, checkout, orders, search, stock-bearing listings are still catalog) get none:
782
+ they must never be cached across visitors.
783
+
784
+ ```ts
785
+ function tagForPath(pathname: string): string | null
786
+ ```
787
+
788
+ #### `tagsForTopic`
789
+
790
+ ```ts
791
+ function tagsForTopic(topic: string): string[]
792
+ ```
793
+
794
+ #### `verifyWebhookSignature`
795
+
796
+ Verifies `X-MagicStore-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>` over
797
+ the RAW body, in constant time, with Web Crypto — so it runs in Node, edge runtimes and Workers.
798
+
799
+ ```ts
800
+ async function verifyWebhookSignature(secret: string, header: string | null, rawBody: string, nowSeconds: number = Math.floor(Date.now() / 1000)): Promise<boolean>
801
+ ```
802
+
803
+ ### Constants
804
+
805
+ #### `CACHE_TAGS`
806
+
807
+ The cache tags a storefront gives what each topic invalidates.
808
+
809
+ ```ts
810
+ const CACHE_TAGS = {
811
+ shop: 'magicstore:shop',
812
+ catalog: 'magicstore:catalog',
813
+ pages: 'magicstore:pages',
814
+ home: 'magicstore:home',
815
+ } as const
816
+ ```
817
+
818
+ #### `SIGNATURE_TOLERANCE_SECONDS`
819
+
820
+ How far the signature's timestamp may be from this clock (replay window).
821
+
822
+ ```ts
823
+ const SIGNATURE_TOLERANCE_SECONDS = 300
824
+ ```
825
+
826
+ ### Types
827
+
828
+ #### `WebhookEvent`
829
+
830
+ ```ts
831
+ export interface WebhookEvent {
832
+ id: string;
833
+ topic: WebhookTopic | (string & {});
834
+ shop: string;
835
+ occurredAt: string;
836
+ data: Record<string, unknown>;
837
+ }
838
+ ```
839
+
840
+ #### `WebhookHandlerOptions`
841
+
842
+ ```ts
843
+ export interface WebhookHandlerOptions {
844
+ secret: string;
845
+ revalidateTag?: (tag: string, profile: 'max') => void | Promise<void>;
846
+ onEvent?: (event: WebhookEvent) => void | Promise<void>;
847
+ now?: () => number;
848
+ }
849
+ ```
850
+
851
+ #### `WebhookTopic`
852
+
853
+ Topics the platform sends today. New ones may be added — ignore what you do not know.
854
+
855
+ ```ts
856
+ export type WebhookTopic = 'SHOP_UPDATED' | 'CATALOG_UPDATED' | 'PAGES_UPDATED' | 'HOME_UPDATED';
857
+ ```
858
+
859
+ ## `@magicstoreai/hydrogen/seo`
860
+
861
+ Page meta and JSON-LD from API resources; server-safe.
862
+
863
+ ### Functions
864
+
865
+ #### `breadcrumbJsonLd`
866
+
867
+ schema.org `BreadcrumbList` from `[{ name, url }]`.
868
+
869
+ ```ts
870
+ function breadcrumbJsonLd(items: Array<{
871
+ name: string;
872
+ url: string;
873
+ }>): { '@context': string; '@type': string; itemListElement: { '@type': string; position: number; name: string; item: string; }[]; }
874
+ ```
875
+
876
+ #### `jsonLdScript`
877
+
878
+ A JSON-LD object as a string safe inside `<script>` (no `</script>` break-out).
879
+
880
+ ```ts
881
+ function jsonLdScript(data: unknown): string
882
+ ```
883
+
884
+ #### `pageMeta`
885
+
886
+ Title and description for a page: the merchant's SEO fields first, then the page's own. Shaped so
887
+ a Next.js `generateMetadata` can return most of it as is.
888
+
889
+ ```ts
890
+ function pageMeta(input: {
891
+ seo?: Seo | null;
892
+ title: string;
893
+ description?: string | null;
894
+ url?: string | null;
895
+ images?: Array<{
896
+ url: string;
897
+ } | null | undefined>;
898
+ type?: string;
899
+ shop?: Pick<Shop, 'name'> | null;
900
+ }): PageMeta
901
+ ```
902
+
903
+ #### `productJsonLd`
904
+
905
+ schema.org `Product` with its offers — for a `<script type="application/ld+json">`.
906
+
907
+ ```ts
908
+ function productJsonLd(product: Product, options: {
909
+ url: string;
910
+ shop?: Pick<Shop, 'name'> | null;
911
+ }): { '@context': string; '@type': string; name: string; description: string | undefined; image: string[]; sku: string | undefined; brand: { '@type': string; name: string; } | undefined; offers: { '@type': string; ... 4 more ...; url: string; }[] | undefined; aggregateRating: { ...; } | undefined; seller: { ...; } | und...
912
+ ```
913
+
914
+ ### Types
915
+
916
+ #### `PageMeta`
917
+
918
+ ```ts
919
+ export interface PageMeta {
920
+ title: string;
921
+ description: string | null;
922
+ canonical: string | null;
923
+ openGraph: {
924
+ title: string;
925
+ description: string | null;
926
+ url: string | null;
927
+ images: string[];
928
+ type: string;
929
+ };
930
+ }
931
+ ```