@shoppexio/storefront-react 0.2.1 → 0.4.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/dist/account.css +4 -8
  3. package/dist/account.js +2387 -942
  4. package/dist/account.js.map +1 -1
  5. package/dist/{chunk-ZEFZPUAW.js → chunk-NBOIS53R.js} +2 -1
  6. package/dist/chunk-NBOIS53R.js.map +1 -0
  7. package/dist/index.d.ts +243 -29
  8. package/dist/index.js +221 -77
  9. package/dist/index.js.map +1 -1
  10. package/eject/account/account-availability.ts +4 -1
  11. package/eject/account/account-downloads.ts +89 -1
  12. package/eject/account/account-referrals.ts +249 -7
  13. package/eject/account/account-reseller.ts +6 -140
  14. package/eject/account/dashboard.tsx +1 -0
  15. package/eject/account/downloads.tsx +73 -4
  16. package/eject/account/hosted-page.ts +45 -0
  17. package/eject/account/messages/ar.json +94 -31
  18. package/eject/account/messages/de.json +94 -31
  19. package/eject/account/messages/en.json +94 -31
  20. package/eject/account/messages/es.json +94 -31
  21. package/eject/account/messages/fi.json +94 -31
  22. package/eject/account/messages/fr.json +94 -31
  23. package/eject/account/messages/hi.json +94 -31
  24. package/eject/account/messages/it.json +94 -31
  25. package/eject/account/messages/lt.json +94 -31
  26. package/eject/account/messages/nl.json +94 -31
  27. package/eject/account/messages/pl.json +94 -31
  28. package/eject/account/messages/pt.json +94 -31
  29. package/eject/account/messages/ru.json +94 -31
  30. package/eject/account/messages/tr.json +94 -31
  31. package/eject/account/messages/uk.json +94 -31
  32. package/eject/account/messages/zh.json +94 -31
  33. package/eject/account/orders.tsx +141 -0
  34. package/eject/account/overview.tsx +6 -0
  35. package/eject/account/referrals.tsx +444 -156
  36. package/eject/account/reseller.tsx +59 -306
  37. package/eject/account/rewards.tsx +43 -6
  38. package/eject/account/settings.tsx +98 -2
  39. package/eject/account/shared.tsx +42 -3
  40. package/eject/account/subscriptions.tsx +75 -16
  41. package/package.json +7 -4
  42. package/dist/chunk-ZEFZPUAW.js.map +0 -1
@@ -1,7 +1,8 @@
1
1
  /**
2
- * The buyer's own details, their marketing consent, and their signed-in devices.
2
+ * The buyer's own details, their marketing consent, their order mail and their
3
+ * signed-in devices.
3
4
  *
4
- * Three producers, three sections, and nothing pretending to be a fourth. The
5
+ * Four producers, four sections, and nothing pretending to be a fifth. The
5
6
  * email address is not editable — changing it is gated behind a fresh code and
6
7
  * has no allowlisted path — there is no password field, because this account
7
8
  * signs in with a code, and there is no "delete account", because no endpoint
@@ -30,6 +31,7 @@ import {
30
31
  safeExternalHref,
31
32
  type AccountPanelState,
32
33
  } from './customer-account';
34
+ import type { SdkCustomerNotificationPreferences } from '@shoppexio/storefront-react';
33
35
  import { t } from './i18n';
34
36
  import { useAccountDashboard } from './dashboard';
35
37
  import {
@@ -67,6 +69,7 @@ export function AccountSettings() {
67
69
  onSaved={reload}
68
70
  />
69
71
  <EmailPreferencesSection />
72
+ <OrderEmailsSection />
70
73
  <DevicesSection />
71
74
  </div>
72
75
  );
@@ -474,6 +477,99 @@ function EmailPreferencesSection() {
474
477
  );
475
478
  }
476
479
 
480
+ type OrderEmailKey = keyof SdkCustomerNotificationPreferences;
481
+
482
+ /**
483
+ * Order mail: placed, completed, payment needs attention.
484
+ *
485
+ * A different producer from the marketing lists above, and unlike that one it
486
+ * answers the saved set on every write — so the boxes are drawn from the answer
487
+ * rather than from a second read. A read that failed draws no boxes at all:
488
+ * three ticks resting at "on" would be a claim about the buyer's mail that
489
+ * nothing has made.
490
+ */
491
+ function OrderEmailsSection() {
492
+ const { state, reload } = useAccountResource('notification-preferences', () =>
493
+ callCustomerApi((sdk) => sdk.notificationPreferences()));
494
+ /** The set the last write answered with; it replaces the read it followed. */
495
+ const [saved, setSaved] = useState<SdkCustomerNotificationPreferences | null>(null);
496
+ const [pendingKey, setPendingKey] = useState<OrderEmailKey | null>(null);
497
+ const [error, setError] = useState<string | null>(null);
498
+
499
+ const heading = <AccountSectionTitle>{t('settings.orderEmailsTitle')}</AccountSectionTitle>;
500
+
501
+ if (state.status === 'error') {
502
+ return (
503
+ <section>
504
+ {heading}
505
+ <div className="sxa:mt-4">
506
+ <AccountError message={state.message} onRetry={reload} />
507
+ </div>
508
+ </section>
509
+ );
510
+ }
511
+
512
+ if (state.status === 'loading') {
513
+ return (
514
+ <section>
515
+ {heading}
516
+ <p role="status" className="sxa:mt-4 sxa:text-small sxa:text-muted">
517
+ {t('settings.orderEmailsLoading')}
518
+ </p>
519
+ </section>
520
+ );
521
+ }
522
+
523
+ const shown = saved ?? state.data;
524
+
525
+ const write = async (key: OrderEmailKey, enabled: boolean) => {
526
+ setPendingKey(key);
527
+ setError(null);
528
+ // The one box that changed travels alone: the producer upserts exactly the
529
+ // keys it is sent, and a full set built from this panel's last read would
530
+ // overwrite a change made on another device.
531
+ const response = await callCustomerApi((sdk) => sdk.updateNotificationPreferences({ [key]: enabled }));
532
+ setPendingKey(null);
533
+
534
+ if (!response.success || response.data === undefined) {
535
+ setError(response.message?.trim() || t('settings.emailsSaveFailed'));
536
+ return;
537
+ }
538
+ setSaved(response.data);
539
+ };
540
+
541
+ const rows: Array<{ key: OrderEmailKey; label: string; note: string }> = [
542
+ { key: 'order_created_emails', label: t('settings.orderCreated'), note: t('settings.orderCreatedNote') },
543
+ { key: 'order_completed_emails', label: t('settings.orderCompleted'), note: t('settings.orderCompletedNote') },
544
+ { key: 'payment_issue_emails', label: t('settings.paymentIssue'), note: t('settings.paymentIssueNote') },
545
+ ];
546
+
547
+ return (
548
+ <section>
549
+ {heading}
550
+ <AccountCard className="sxa:mt-4 sxa:space-y-4 sxa:p-6">
551
+ {error ? <AccountFieldError message={error} /> : null}
552
+
553
+ {rows.map((row) => (
554
+ <label key={row.key} className="sxa:flex sxa:items-start sxa:gap-3">
555
+ <input
556
+ type="checkbox"
557
+ checked={shown[row.key]}
558
+ disabled={pendingKey !== null}
559
+ className="sxa:mt-1 sxa:h-4 sxa:w-4 sxa:shrink-0"
560
+ onChange={(event) => void write(row.key, event.target.checked)}
561
+ />
562
+ <span className="sxa:min-w-0">
563
+ <span className="sxa:block sxa:text-small sxa:font-medium">{row.label}</span>
564
+ <span className="sxa:mt-1 sxa:block sxa:text-micro sxa:text-muted">{row.note}</span>
565
+ </span>
566
+ </label>
567
+ ))}
568
+ </AccountCard>
569
+ </section>
570
+ );
571
+ }
572
+
477
573
  function DevicesSection() {
478
574
  const { state, reload } = useAccountResource('sessions', () =>
479
575
  callCustomerApi((sdk) => sdk.sessions()));
@@ -14,14 +14,16 @@
14
14
 
15
15
  import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
16
16
  import { Link } from 'react-router-dom';
17
- import { toPanelState, type AccountPanelState } from './customer-account';
17
+ import { safeExternalHref, toPanelState, type AccountPanelState } from './customer-account';
18
18
  import type { SdkResponse } from '@shoppexio/storefront-react';
19
+ import { hostedPageHref, type HostedPage } from './hosted-page';
19
20
  import { getActiveLocale, t, tOptional } from './i18n';
20
21
  import {
21
22
  ArrowLeftIcon,
22
23
  CheckIcon,
23
24
  CircleAlertIcon,
24
25
  CopyIcon,
26
+ ExternalLinkIcon,
25
27
  LoaderIcon,
26
28
  } from './icons';
27
29
 
@@ -245,10 +247,11 @@ const STATUS_TONES: Record<string, string> = {
245
247
  * the tone never applied.
246
248
  */
247
249
 
248
- export function StatusChip({ status }: { status: string }) {
250
+ /** `label` replaces the status word when the caller has a better one; the tone still follows `status`. */
251
+ export function StatusChip({ status, label }: { status: string; label?: string }) {
249
252
  const tone = STATUS_TONES[status.trim().toUpperCase()] ?? 'sxa:meta-chip--neutral';
250
253
  return (
251
- <span className={`sx-account-status sx-account-chip sxa:meta-chip sxa:meta-chip--status ${tone} sxa:shrink-0`}>{statusLabel(status)}</span>
254
+ <span className={`sx-account-status sx-account-chip sxa:meta-chip sxa:meta-chip--status ${tone} sxa:shrink-0`}>{label ?? statusLabel(status)}</span>
252
255
  );
253
256
  }
254
257
 
@@ -392,6 +395,42 @@ export function CopyButton({ value, className = '' }: { value: string; className
392
395
  );
393
396
  }
394
397
 
398
+ /**
399
+ * A way out to a hosted buyer page on the checkout host (design Batch 7, A1):
400
+ * a plate with the external-link glyph, never a solid, because the account
401
+ * itself moves no money and mints no credential. Same tab, so the hosted page
402
+ * can send the buyer back. Renders nothing when the page has no shop or
403
+ * checkout host to link to (`hostedPageHref`).
404
+ */
405
+ export function HostedPageLink({
406
+ page,
407
+ hints,
408
+ label,
409
+ hint = t('account.secureLink'),
410
+ className = '',
411
+ }: {
412
+ page: HostedPage;
413
+ hints: Record<string, string>;
414
+ label: string;
415
+ /** The line beside the link; null where the card has no room for it. */
416
+ hint?: string | null;
417
+ className?: string;
418
+ }) {
419
+ // The checkout host is a page global, so it is held to the same http(s)
420
+ // rule as any other href this account renders.
421
+ const href = safeExternalHref(hostedPageHref(page, hints));
422
+ if (!href) return null;
423
+ return (
424
+ <div className={`sx-account-hosted-link sxa:flex sxa:flex-wrap sxa:items-center sxa:justify-between sxa:gap-2 ${className}`}>
425
+ {hint ? <p className="sxa:text-small sxa:text-muted">{hint}</p> : null}
426
+ <a href={href} className="sx-account-button sxa:card-button sxa:card-button--add">
427
+ {label}
428
+ <ExternalLinkIcon className="sxa:ml-1.5 sxa:h-3.5 sxa:w-3.5 sxa:shrink-0" />
429
+ </a>
430
+ </div>
431
+ );
432
+ }
433
+
395
434
  /** The spinner a control shows while its own request is in flight. */
396
435
  export function BusySpinner({ className = '' }: { className?: string }) {
397
436
  return <LoaderIcon className={`sxa:h-4 sxa:w-4 sxa:shrink-0 sxa:animate-spin ${className}`} />;
@@ -9,10 +9,10 @@
9
9
  *
10
10
  * The payment-method portal is NOT one of them. That route answers with a
11
11
  * signed redirect into the provider's own portal, and any script sharing this
12
- * origin could fetch the link and forward it, so the lane renders no control
13
- * for it whatsoever — not a disabled one, which would promise that pressing it
14
- * would work. The buyer reaches the portal from the store's own emails, and the
15
- * panel says so rather than pretending the capability does not exist.
12
+ * origin could fetch the link and forward it. So "Update card" is a link to the
13
+ * hosted billing page on the checkout host (design Batch 7, 5b), which asks for
14
+ * its own email code before it mints the portal session; the producer's
15
+ * `can_manage_payment_method` decides whether it shows.
16
16
  *
17
17
  * List and detail are two views of one panel. The platform defines a detail
18
18
  * path for orders and for nothing else, so a `/dashboard/subscriptions/<id>`
@@ -29,10 +29,11 @@ import {
29
29
  subscriptionTiming,
30
30
  subscriptionTimingKey,
31
31
  } from './account-subscriptions';
32
- import { callCustomerApi } from './customer-account';
32
+ import { callCustomerApi, safeExternalHref } from './customer-account';
33
+ import { hostedPageHref } from './hosted-page';
33
34
  import type { SdkCustomerSubscription } from '@shoppexio/storefront-react';
34
35
  import { t } from './i18n';
35
- import { LockIcon } from './icons';
36
+ import { CircleAlertIcon } from './icons';
36
37
  import { useAccountDashboard } from './dashboard';
37
38
  import {
38
39
  AccountBackButton,
@@ -43,6 +44,7 @@ import {
43
44
  AccountRowsSkeleton,
44
45
  AccountSectionTitle,
45
46
  BusySpinner,
47
+ HostedPageLink,
46
48
  StatusChip,
47
49
  formatAccountAmount,
48
50
  formatAccountDate,
@@ -127,6 +129,7 @@ export function AccountSubscriptions() {
127
129
  ) : null}
128
130
  <StatusChip status={subscription.status} />
129
131
  </button>
132
+ <RenewalDue subscription={subscription} className="sxa:mt-2" />
130
133
  </li>
131
134
  ))}
132
135
  </ul>
@@ -203,6 +206,12 @@ function SubscriptionDetail({
203
206
  {notice ? <AccountNotice message={notice} /> : null}
204
207
  {actionError ? <div className="sxa:mt-3"><AccountError message={actionError} /></div> : null}
205
208
 
209
+ <RenewalDue subscription={subscription} className="sxa:mt-3" />
210
+
211
+ {subscription.status === 'PAST_DUE' && subscription.actions.can_manage_payment_method ? (
212
+ <PastDueCard subscription={subscription} />
213
+ ) : null}
214
+
206
215
  <AccountCard className="sxa:mt-5 sxa:p-6">
207
216
  {subscription.plan ? (
208
217
  <>
@@ -232,17 +241,13 @@ function SubscriptionDetail({
232
241
  </dl>
233
242
  </AccountCard>
234
243
 
235
- {/*
236
- Where a "Manage payment method" button would have gone. The flag is true
237
- for every Stripe plan, cancelled ones included, and the route behind it
238
- answers with a signed provider URL — so this lane renders the sentence
239
- and no control at all.
240
- */}
241
244
  {subscription.actions.can_manage_payment_method ? (
242
- <p className="sxa:mt-4 sxa:flex sxa:items-start sxa:gap-2 sxa:text-small sxa:text-muted">
243
- <LockIcon className="sxa:mt-0.5 sxa:h-4 sxa:w-4 sxa:shrink-0" />
244
- {t('subscriptions.paymentMethodElsewhere')}
245
- </p>
245
+ <HostedPageLink
246
+ page="billing"
247
+ hints={{ subscription: subscription.uniqid }}
248
+ label={t('subscriptions.updateCard')}
249
+ className="sxa:mt-4"
250
+ />
246
251
  ) : null}
247
252
 
248
253
  {/*
@@ -347,6 +352,60 @@ function SubscriptionDetail({
347
352
  );
348
353
  }
349
354
 
355
+ /**
356
+ * A renewal payment that is due, and the way to pay it.
357
+ *
358
+ * `renewal_checkout_url` is the hosted checkout of the renewal invoice the
359
+ * server already opened, so this is a plain link: following it creates
360
+ * nothing, and no call is made from here. No URL means nothing is due and
361
+ * nothing is shown; a URL that fails the scheme check gets no anchor, only the
362
+ * sentence, because the payment is still due.
363
+ */
364
+ function RenewalDue({ subscription, className }: { subscription: SdkCustomerSubscription; className: string }) {
365
+ const url = subscription.renewal_checkout_url ?? null;
366
+ if (url === null) return null;
367
+ const href = safeExternalHref(url);
368
+
369
+ return (
370
+ <AccountCard className={`${className} sxa:flex sxa:flex-wrap sxa:items-center sxa:justify-between sxa:gap-3 sxa:px-5 sxa:py-3`}>
371
+ <p className="sxa:min-w-0 sxa:text-small">{t('subscriptions.renewalDue')}</p>
372
+ {href === null ? null : (
373
+ <a
374
+ href={href}
375
+ className="sx-account-cta sxa:cta-button sxa:inline-flex sxa:h-9 sxa:shrink-0 sxa:items-center sxa:px-4 sxa:text-small sxa:font-semibold"
376
+ >
377
+ {t('subscriptions.payNow')}
378
+ </a>
379
+ )}
380
+ </AccountCard>
381
+ );
382
+ }
383
+
384
+ /**
385
+ * A past-due Stripe plan: the last charge failed, and the card is what to
386
+ * change before the plan ends. The same hosted billing link as "Update card".
387
+ */
388
+ function PastDueCard({ subscription }: { subscription: SdkCustomerSubscription }) {
389
+ // No shop or checkout host to link to: the notice would point at nothing.
390
+ if (hostedPageHref('billing', { subscription: subscription.uniqid }) === null) return null;
391
+ return (
392
+ <AccountCard className="sx-account-past-due sxa:mt-3 sxa:flex sxa:items-start sxa:gap-3 sxa:px-5 sxa:py-4">
393
+ <CircleAlertIcon className="sxa:mt-0.5 sxa:h-4 sxa:w-4 sxa:shrink-0 sxa:text-error" />
394
+ <div className="sxa:min-w-0 sxa:flex-1">
395
+ <p className="sxa:text-small sxa:font-semibold">{t('subscriptions.pastDueCardTitle')}</p>
396
+ <p className="sxa:mt-1 sxa:text-small sxa:text-muted">{t('subscriptions.pastDueCardBody')}</p>
397
+ <HostedPageLink
398
+ page="billing"
399
+ hints={{ subscription: subscription.uniqid }}
400
+ label={t('subscriptions.updateCard')}
401
+ hint={null}
402
+ className="sxa:mt-3"
403
+ />
404
+ </div>
405
+ </AccountCard>
406
+ );
407
+ }
408
+
350
409
  function DetailRow({ label, children }: { label: string; children: ReactNode }) {
351
410
  return (
352
411
  <div className="sxa:flex sxa:items-center sxa:justify-between sxa:gap-4">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shoppexio/storefront-react",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "React hooks, models and a customer account for Shoppex code storefronts",
5
5
  "type": "module",
6
6
  "homepage": "https://docs.shoppex.io/storefront/code-storefront-development",
@@ -37,7 +37,7 @@
37
37
  "build": "tsup && bun run build:css && bun run build:eject",
38
38
  "build:css": "tailwindcss --input src/account/account.css --output dist/account.css --optimize",
39
39
  "lint": "biome lint src tests scripts",
40
- "typecheck": "tsc --noEmit",
40
+ "typecheck": "tsrs --noEmit",
41
41
  "test": "bun run build:css && bun test ./tests",
42
42
  "build:eject": "bun scripts/build-eject.ts"
43
43
  },
@@ -65,12 +65,15 @@
65
65
  "react-router-dom": "^7.18.1"
66
66
  },
67
67
  "devDependencies": {
68
+ "@shoppex/contracts": "0.1.5",
68
69
  "@tailwindcss/cli": "4.2.1",
69
70
  "@types/bun": "^1.4.0",
71
+ "@types/jsdom-cjs": "npm:@types/jsdom@^28.0.0",
70
72
  "@types/react": "19.2.14",
71
73
  "@types/react-dom": "19.2.3",
72
- "react": "19.2.7",
73
- "react-dom": "19.2.7",
74
+ "jsdom-cjs": "npm:jsdom@26.1.0",
75
+ "react": "19.3.1",
76
+ "react-dom": "19.3.1",
74
77
  "react-router-dom": "7.18.1",
75
78
  "tailwindcss": "4.2.1",
76
79
  "tsup": "^8.5.0",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/account-route.ts","../src/shoppex.ts","../src/favorites.tsx","../src/account/account-favorites.ts"],"sourcesContent":["/**\n * Which account surface a URL names.\n *\n * The account lives under `/dashboard`, and the panel is a PATH SEGMENT:\n * `/dashboard/orders`, `/dashboard/orders/<uniqid>`, `/dashboard/settings`.\n * `/dashboard` itself is the overview. This is the platform's route shape and it\n * is identical on both storefront lanes (ADR-0064), so a theme that invents its\n * own account paths breaks links it does not own.\n *\n * The older query spelling — `?tab=<tab>` and `?order=<id>` — is still ACCEPTED,\n * because it sits in order emails already sent and in the platform's post-login\n * redirect. It is read as input and never written: a URL carrying it is\n * canonicalized to the path form, by replacement rather than a push, so the back\n * button does not bounce the buyer between the two spellings. The path always\n * wins when a URL carries both.\n *\n * `/customer-portal` is the oldest spelling of the same route and folds in with\n * its tail and query intact.\n *\n * A locale prefix (`/de/dashboard/orders`) is part of the served URL on a\n * localized storefront. The router's basename normally strips it before any of\n * this sees it, so the parser accepts a pathname with or without one and\n * canonicalization keeps whatever prefix it was given.\n *\n * Everything here is pure string work, so `tests/account-route.test.ts` can pin\n * the parts email links depend on.\n */\n\nexport const ACCOUNT_PATH = '/dashboard';\n\n/** The older spelling of the same route. Folded, never served separately. */\nexport const ACCOUNT_PATH_ALIAS = '/customer-portal';\n\nconst ACCOUNT_SEGMENT = 'dashboard';\nconst ACCOUNT_ALIAS_SEGMENT = 'customer-portal';\n\n/**\n * Every tab the Shoppex account surface defines, in navigation order.\n *\n * Listed in full so a link to a real platform tab can be told apart from a typo\n * and answered differently.\n */\nexport const ACCOUNT_TABS = [\n 'overview',\n 'orders',\n 'downloads',\n 'subscriptions',\n 'rewards',\n 'referrals',\n 'favorites',\n 'support',\n 'settings',\n 'reseller',\n] as const;\n\nexport type AccountTab = (typeof ACCOUNT_TABS)[number];\n\n/**\n * The catalog key a tab's label lives under.\n *\n * A key rather than a string: this file is imported by the router, the\n * navigation and the \"not part of this store\" notice, and each of them needs\n * the label in the locale the edge served — see `account/i18n.ts`.\n */\nexport function accountTabLabelKey(tab: AccountTab): string {\n return `account.tabs.${tab}`;\n}\n\n/**\n * The tabs the account renders.\n *\n * All ten platform tabs. Adding a panel means adding its tab here AND\n * rendering it, in the same change: a tab listed without a panel would navigate\n * to the \"not built\" notice, which is worse than not offering it.\n *\n * Reseller payloads are locally narrowed because shared contracts do not cover\n * them, but the worker now allowlists their SDK routes.\n */\nexport const IMPLEMENTED_ACCOUNT_TABS = [\n 'overview',\n 'orders',\n 'downloads',\n 'subscriptions',\n 'rewards',\n 'referrals',\n 'favorites',\n 'support',\n 'settings',\n 'reseller',\n] as const;\n\nexport type ImplementedAccountTab = (typeof IMPLEMENTED_ACCOUNT_TABS)[number];\n\nexport const DEFAULT_ACCOUNT_TAB: ImplementedAccountTab = 'overview';\n\n/**\n * What the account page should show.\n *\n * `not-built` and `unknown-tab` are separate on purpose. The first is a real\n * platform tab this storefront chose not to render, the second is a value that\n * names no tab at all. Collapsing them into \"show the overview instead\" would\n * hide a broken link behind a page that looks like it worked.\n */\nexport type AccountView =\n | { kind: 'panel'; tab: ImplementedAccountTab }\n | { kind: 'order'; orderId: string }\n | { kind: 'not-built'; tab: AccountTab }\n | { kind: 'unknown-tab' };\n\nexport function isAccountTab(value: string): value is AccountTab {\n return (ACCOUNT_TABS as readonly string[]).includes(value);\n}\n\nexport function isImplementedAccountTab(value: string): value is ImplementedAccountTab {\n return (IMPLEMENTED_ACCOUNT_TABS as readonly string[]).includes(value);\n}\n\n/** A two-letter locale segment, the only prefix the storefront edge serves. */\nconst LOCALE_SEGMENT = /^[a-z]{2}$/i;\n\nfunction isAccountSegment(segment: string | undefined): boolean {\n return segment === ACCOUNT_SEGMENT || segment === ACCOUNT_ALIAS_SEGMENT;\n}\n\nexport interface AccountRouteParts {\n /** What preceded `/dashboard`: '' or a locale prefix such as '/de'. */\n prefix: string;\n /** Whether the URL used the retired `/customer-portal` spelling. */\n alias: boolean;\n /** The segments after `/dashboard`, decoded. */\n tail: string[];\n}\n\n/**\n * Split an account URL into its prefix and the segments below `/dashboard`.\n *\n * Null when the path is not the account route at all. The account segment is\n * only recognised first or directly after a locale segment — searching for it\n * anywhere would make `/product/dashboard-pro` an account URL.\n */\nexport function parseAccountPath(pathname: string): AccountRouteParts | null {\n const segments = pathname.split('/').filter((segment) => segment.length > 0);\n\n let index = -1;\n if (isAccountSegment(segments[0])) index = 0;\n else if (segments[0] && LOCALE_SEGMENT.test(segments[0]) && isAccountSegment(segments[1])) index = 1;\n if (index === -1) return null;\n\n return {\n prefix: index === 0 ? '' : `/${segments[0]}`,\n alias: segments[index] === ACCOUNT_ALIAS_SEGMENT,\n tail: segments.slice(index + 1).map(decodeSegment),\n };\n}\n\nfunction decodeSegment(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n // A malformed escape is not a segment this route can name; keeping it raw\n // lets it fall through to `unknown-tab` instead of throwing on a link a\n // buyer merely mistyped.\n return segment;\n }\n}\n\n/**\n * What to render for a URL.\n *\n * The path is read first and the query only when the path names no panel, so a\n * canonical URL is never reinterpreted by a stale parameter riding along on it.\n */\nexport function resolveAccountView(pathname: string, search: URLSearchParams): AccountView {\n const parts = parseAccountPath(pathname);\n if (!parts) return { kind: 'unknown-tab' };\n\n if (parts.tail.length === 0) return resolveLegacyQueryView(search);\n\n const [first, second, ...rest] = parts.tail;\n if (rest.length > 0) return { kind: 'unknown-tab' };\n if (!first) return resolveLegacyQueryView(search);\n\n if (second !== undefined) {\n // The only two-segment surface: one order inside the orders panel.\n if (first !== 'orders' || second.length === 0) return { kind: 'unknown-tab' };\n return { kind: 'order', orderId: second };\n }\n\n return viewForTab(first);\n}\n\n/**\n * The retired spelling, read as input only.\n *\n * `?order=` belongs to the orders panel, so it is honoured when the query asks\n * for the orders tab or names no tab at all. An explicit different tab wins:\n * `?tab=settings&order=x` is a request for settings, and answering it with an\n * order would be the page deciding it knows better.\n */\nfunction resolveLegacyQueryView(search: URLSearchParams): AccountView {\n const rawTab = (search.get('tab') ?? '').trim();\n // `invoice` is the portal's spelling and it is already out in the world:\n // notification mails carry `?tab=orders&invoice=<uniqid>` and cannot be\n // edited after the fact. Reading only `order` would open the list and\n // canonicalize the invoice away.\n const orderId = (search.get('order') ?? search.get('invoice') ?? '').trim();\n\n if (rawTab && !isAccountTab(rawTab)) return { kind: 'unknown-tab' };\n if (orderId && (rawTab === '' || rawTab === 'orders')) return { kind: 'order', orderId };\n if (rawTab === '') return { kind: 'panel', tab: DEFAULT_ACCOUNT_TAB };\n\n return viewForTab(rawTab);\n}\n\nfunction viewForTab(value: string): AccountView {\n if (!isAccountTab(value)) return { kind: 'unknown-tab' };\n if (!isImplementedAccountTab(value)) return { kind: 'not-built', tab: value };\n return { kind: 'panel', tab: value };\n}\n\n/** `/dashboard/<tab>`. The overview is the bare path, not `/dashboard/overview`. */\nexport function accountTabHref(tab: AccountTab): string {\n if (tab === DEFAULT_ACCOUNT_TAB) return ACCOUNT_PATH;\n return `${ACCOUNT_PATH}/${tab}`;\n}\n\n/** `/dashboard/orders/<id>`, the one order-detail spelling. */\nexport function accountOrderHref(orderId: string): string {\n return `${ACCOUNT_PATH}/orders/${encodeURIComponent(orderId)}`;\n}\n\n/**\n * The canonical path for a URL that is not written the canonical way, or null\n * when it already is.\n *\n * Three URLs need rewriting: the `/customer-portal` alias, a `?tab=`/`?order=`\n * query, and the redundant `/dashboard/overview`. The caller replaces rather\n * than pushes — a push would put the retired spelling in the history and send\n * the back button straight back into it.\n *\n * A URL naming nothing real (`?tab=orderz`, `/dashboard/nope`) has no canonical\n * form and keeps the address it came with: rewriting it to the overview would\n * turn a broken link into one that looks like it worked.\n *\n * The prefix the URL came with is preserved, so a buyer on `/de/dashboard` is\n * not quietly moved out of their locale.\n */\nexport function canonicalAccountPath(pathname: string, search: URLSearchParams): string | null {\n const parts = parseAccountPath(pathname);\n if (!parts) return null;\n\n const view = resolveAccountView(pathname, search);\n const canonical = canonicalPathForView(view);\n if (canonical === null) return null;\n\n const current = `${parts.prefix}${ACCOUNT_PATH}${parts.tail.map((segment) => `/${encodeURIComponent(segment)}`).join('')}`;\n const target = `${parts.prefix}${canonical}`;\n\n // Already canonical AND carrying nothing to strip: leave the URL alone.\n if (!parts.alias && current === target && search.get('tab') === null && search.get('order') === null && search.get('invoice') === null) {\n return null;\n }\n\n return target;\n}\n\nfunction canonicalPathForView(view: AccountView): string | null {\n switch (view.kind) {\n case 'panel':\n return accountTabHref(view.tab);\n case 'order':\n return accountOrderHref(view.orderId);\n case 'not-built':\n return accountTabHref(view.tab);\n case 'unknown-tab':\n return null;\n }\n}\n\n\n/**\n * The storefront routes the account links to outside itself: the catalog\n * behind every \"browse the store\" action, and a product page per favorite.\n * React Router does not check a `to`, so a host that does not route one of\n * these answers the account's own buttons with its not-found page. A host\n * checks its router against this list (ForkMe: `tests/account-routes.test.ts`).\n */\nexport const ACCOUNT_STOREFRONT_ROUTES = ['/all-products', '/product/:slug'] as const;\n\n/**\n * The artifact capabilities the account relies on. A storefront that mounts\n * the account lists each of them in its `public/shoppex.capabilities.json`;\n * the storefront worker reads that file from the artifact and only then hands\n * the account what the capability promises. `account-reseller-invite-cookie`\n * is pinned to `RESELLER_INVITE_ARTIFACT_CAPABILITY` in\n * `workers/storefront/src/reseller-invite-link.ts`: without it the worker\n * leaves a reseller invitation in the URL for that build.\n */\nexport const ACCOUNT_ARTIFACT_CAPABILITIES = ['account-reseller-invite-cookie'] as const;\n","import type { ShoppexSdk } from './sdk';\n\n/**\n * Access to the Shoppex Storefront SDK global (loaded in index.html from\n * cdn.shoppex.io) plus its one-time initialization from the bootstrap globals\n * the Shoppex worker injects next to __SHOPPEX_INITIAL__:\n * shopSlug, shopId, apiBaseUrl, defaultCurrency (+ optional checkoutBaseUrl).\n *\n * Both the script and the globals are hard requirements — a storefront without\n * them cannot price, cart, or check out, so we fail fast instead of rendering\n * a broken shop.\n */\n\nlet initialized = false;\n\n/**\n * The sentence that names the CAUSE rather than the symptom.\n *\n * A missing bootstrap global and a missing `__SHOPPEX_INITIAL__` payload are\n * the same fault seen from two sides: the page is not being served by the\n * worker that injects both. `App.tsx`'s configuration screen shows this to the\n * merchant, so it lives here once instead of being written twice.\n */\nexport const SERVING_CONTEXT_HINT =\n 'This storefront must be served by the Shoppex storefront (or preview) worker.';\n\nfunction requiredGlobal(name: 'shopSlug' | 'shopId' | 'apiBaseUrl' | 'defaultCurrency'): string {\n const value = window[name];\n if (typeof value !== 'string' || !value.trim()) {\n throw new Error(`Shoppex bootstrap global \"${name}\" is missing. ${SERVING_CONTEXT_HINT}`);\n }\n return value.trim();\n}\n\nexport function getShoppex(): ShoppexSdk {\n const sdk = window.shoppex;\n if (!sdk) {\n throw new Error(\n 'The Shoppex SDK script did not load (window.shoppex is missing). Check the <script src=\"https://cdn.shoppex.io/sdk/...\"> tag in index.html.',\n );\n }\n if (!initialized) {\n const storeSlug = requiredGlobal('shopSlug');\n if (!sdk.isInitialized() || sdk.getConfig().storeSlug !== storeSlug) {\n const checkoutBaseUrl = typeof window.checkoutBaseUrl === 'string' && window.checkoutBaseUrl.trim()\n ? window.checkoutBaseUrl.trim()\n : undefined;\n sdk.init(storeSlug, {\n shopId: requiredGlobal('shopId'),\n apiBaseUrl: requiredGlobal('apiBaseUrl'),\n currency: requiredGlobal('defaultCurrency'),\n ...(checkoutBaseUrl ? { checkoutBaseUrl } : {}),\n });\n }\n // Last-click referral capture. checkout() reads this stored code by itself\n // whenever the cart does not carry an affiliate code of its own, so this\n // single call is what makes `?ref=<code>` links actually pay out. A code the\n // buyer TYPED and the quote classified as a referral takes precedence — see\n // `cart.tsx` → `checkout`.\n // A failed capture must never block the storefront from rendering.\n void sdk.captureAffiliateFromUrl().catch(() => undefined);\n initialized = true;\n }\n return sdk;\n}\n\n/**\n * The currency the prices on this page are in.\n *\n * The edge injected it alongside the catalog, from the same payload the prices\n * came from, so it is right whether the buyer asked for a currency, whether\n * their country selected one, or neither.\n *\n * Pass it to every quote and checkout. Left out, the SDK falls back to reading\n * `?currency=` off `window.location` — which survives the first page load and\n * nothing after it, because client-side navigation drops the query. The cart\n * then quoted in the shop's geo-resolved currency while the page went on\n * showing the one the buyer asked for.\n */\nexport function getBuyerCurrency(): string {\n return requiredGlobal('defaultCurrency');\n}\n","/**\n * Favorites outside the account: the heart on product cards and pages.\n *\n * Same backend as the account's Favorites panel (`/v1/customer/favorites`,\n * reached through the SDK on the shop's own origin, where the Shoppex worker\n * turns the HttpOnly session cookie into the buyer's session). There is no\n * guest wishlist: a signed-out visitor who presses a heart is sent to sign in,\n * and nothing is stored in the browser.\n *\n * The saved set is read once per page load. A heart only changes after the\n * server confirmed the write, like the account panel's remove: flipping it\n * first and flipping it back on failure makes the buyer watch their own action\n * undo itself.\n *\n * This module stays out of the account entry on purpose (no account messages,\n * no account UI), so a card grid does not load the account bundle.\n */\n\nimport { createContext, useContext, useEffect, useMemo, useSyncExternalStore, type ReactNode } from 'react';\nimport { narrowFavorites } from './account/account-favorites';\nimport { getShoppex } from './shoppex';\nimport type { SdkResponse, ShoppexSdk } from './sdk';\n\n/**\n * - `loading`: the saved set has not been read yet (render the heart idle).\n * - `signed_out`: the read answered 401; a press asks the buyer to sign in.\n * - `ready`: the saved set is known.\n * - `unavailable`: the read failed for another reason (5xx, no SDK support, a\n * changed contract, a buyer with no customer profile in this shop). The\n * feature cannot work on this page, so render no heart.\n */\nexport type FavoritesStatus = 'loading' | 'signed_out' | 'ready' | 'unavailable';\n\n/** What one press did. `signed_out` means: send the buyer to sign in. */\nexport type FavoriteToggleResult = 'added' | 'removed' | 'signed_out' | 'failed';\n\nexport interface FavoritesSnapshot {\n status: FavoritesStatus;\n saved: ReadonlySet<string>;\n pending: ReadonlySet<string>;\n}\n\nexport interface FavoritesApi {\n status: FavoritesStatus;\n isFavorite(productUniqid: string): boolean;\n /** A write for this product is in flight; the heart should say so and ignore presses. */\n isPending(productUniqid: string): boolean;\n toggle(productUniqid: string): Promise<FavoriteToggleResult>;\n}\n\n/** Fired by the account panel after it removed a favorite, so hearts on the page follow. */\nexport const FAVORITES_CHANGED_EVENT = 'sx:favorites-changed';\n\nexport function notifyFavoritesChanged(): void {\n if (typeof window !== 'undefined') window.dispatchEvent(new Event(FAVORITES_CHANGED_EVENT));\n}\n\n/** The three SDK calls the store needs; injectable so the logic is testable without a page. */\nexport interface FavoritesTransport {\n list(): Promise<SdkResponse<unknown>>;\n add(productUniqid: string): Promise<SdkResponse<unknown>>;\n remove(productUniqid: string): Promise<SdkResponse<unknown>>;\n}\n\nasync function callSdk<T>(operation: (sdk: ShoppexSdk) => Promise<SdkResponse<T>>): Promise<SdkResponse<T>> {\n try {\n return await operation(getShoppex());\n } catch {\n // No SDK, or an SDK older than the customer module: the feature is not available here.\n return { success: false };\n }\n}\n\nexport const sdkFavoritesTransport: FavoritesTransport = {\n list: () => callSdk((sdk) => sdk.favorites()),\n add: (productUniqid) => callSdk((sdk) => sdk.addFavorite(productUniqid)),\n remove: (productUniqid) => callSdk((sdk) => sdk.removeFavorite(productUniqid)),\n};\n\nexport interface FavoritesStore {\n getSnapshot(): FavoritesSnapshot;\n subscribe(listener: () => void): () => void;\n load(): Promise<void>;\n toggle(productUniqid: string): Promise<FavoriteToggleResult>;\n}\n\nexport function createFavoritesStore(transport: FavoritesTransport): FavoritesStore {\n let snapshot: FavoritesSnapshot = { status: 'loading', saved: new Set(), pending: new Set() };\n const listeners = new Set<() => void>();\n const update = (next: Partial<FavoritesSnapshot>) => {\n snapshot = { ...snapshot, ...next };\n for (const listener of listeners) listener();\n };\n\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n async load() {\n const result = await transport.list();\n if (!result.success) {\n update({ status: result.status === 401 ? 'signed_out' : 'unavailable', saved: new Set() });\n return;\n }\n const narrowed = narrowFavorites(result.data);\n if (!narrowed.ok) {\n update({ status: 'unavailable', saved: new Set() });\n return;\n }\n update({ status: 'ready', saved: new Set(narrowed.favorites.map((favorite) => favorite.product_uniqid)) });\n },\n async toggle(productUniqid) {\n if (snapshot.status === 'signed_out') return 'signed_out';\n if (snapshot.status !== 'ready' || snapshot.pending.has(productUniqid)) return 'failed';\n\n const adding = !snapshot.saved.has(productUniqid);\n update({ pending: new Set(snapshot.pending).add(productUniqid) });\n const result = adding ? await transport.add(productUniqid) : await transport.remove(productUniqid);\n const pending = new Set(snapshot.pending);\n pending.delete(productUniqid);\n\n if (!result.success) {\n if (result.status === 401) {\n // The session ended since the page loaded.\n update({ pending, status: 'signed_out', saved: new Set() });\n return 'signed_out';\n }\n update({ pending });\n return 'failed';\n }\n const saved = new Set(snapshot.saved);\n if (adding) saved.add(productUniqid);\n else saved.delete(productUniqid);\n update({ pending, saved });\n return adding ? 'added' : 'removed';\n },\n };\n}\n\nconst FavoritesContext = createContext<FavoritesStore | null>(null);\n\nexport function FavoritesProvider({\n children,\n transport = sdkFavoritesTransport,\n}: {\n children: ReactNode;\n transport?: FavoritesTransport;\n}) {\n const store = useMemo(() => createFavoritesStore(transport), [transport]);\n\n useEffect(() => {\n void store.load();\n const onChanged = () => void store.load();\n window.addEventListener(FAVORITES_CHANGED_EVENT, onChanged);\n return () => window.removeEventListener(FAVORITES_CHANGED_EVENT, onChanged);\n }, [store]);\n\n return <FavoritesContext.Provider value={store}>{children}</FavoritesContext.Provider>;\n}\n\nexport function useFavorites(): FavoritesApi {\n const store = useContext(FavoritesContext);\n if (!store) throw new Error('useFavorites must be used inside FavoritesProvider.');\n const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);\n return {\n status: snapshot.status,\n isFavorite: (productUniqid) => snapshot.saved.has(productUniqid),\n isPending: (productUniqid) => snapshot.pending.has(productUniqid),\n toggle: store.toggle,\n };\n}\n","/**\n * The favorites panel's pure half.\n *\n * `GET /favorites` is one of the endpoints `@shoppex/contracts` does not\n * describe, so the SDK answers `unknown` and the narrowing is here, written\n * against what `customer-favorites.ts` actually selects. Every field it sends\n * is required: a saved product with no title and no price is not a favorite the\n * buyer can act on, and rendering it as an empty card would hide the fault.\n */\n\nexport interface CustomerFavorite {\n product_uniqid: string;\n title: string;\n /** Null for a product with no slug — such a product has no page to open. */\n slug: string | null;\n image_url: string | null;\n price_display: number;\n /** True once the merchant deletes the product. The row stays, the link goes. */\n unavailable: boolean;\n created_at: string;\n}\n\nexport type FavoritesResult =\n | { ok: true; favorites: CustomerFavorite[] }\n | { ok: false };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction readNullableString(record: Record<string, unknown>, key: string): string | null | undefined {\n const value = record[key];\n if (value === null || typeof value === 'string') return value;\n return undefined;\n}\n\nexport function narrowFavorites(payload: unknown): FavoritesResult {\n if (!isRecord(payload) || !Array.isArray(payload.favorites)) return { ok: false };\n\n const favorites: CustomerFavorite[] = [];\n for (const entry of payload.favorites) {\n if (!isRecord(entry)) return { ok: false };\n\n const productUniqid = entry.product_uniqid;\n const title = entry.title;\n const slug = readNullableString(entry, 'slug');\n const imageUrl = readNullableString(entry, 'image_url');\n const priceDisplay = entry.price_display;\n const unavailable = entry.unavailable;\n const createdAt = entry.created_at;\n\n if (\n typeof productUniqid !== 'string'\n || typeof title !== 'string'\n || slug === undefined\n || imageUrl === undefined\n || typeof priceDisplay !== 'number'\n || !Number.isFinite(priceDisplay)\n || typeof unavailable !== 'boolean'\n || typeof createdAt !== 'string'\n ) {\n return { ok: false };\n }\n\n favorites.push({\n product_uniqid: productUniqid,\n title,\n slug,\n image_url: imageUrl,\n price_display: priceDisplay,\n unavailable,\n created_at: createdAt,\n });\n }\n\n return { ok: true, favorites };\n}\n\n/**\n * Where a saved product lives in this storefront, or null when it has nowhere\n * to go.\n *\n * A product the merchant deleted keeps its row so the buyer can clear it, but\n * it must not keep its link: `/product/<slug>` would answer with the storefront's\n * not-found page, which reads as a broken shop rather than a removed product.\n */\nexport function favoriteProductHref(favorite: CustomerFavorite): string | null {\n if (favorite.unavailable || !favorite.slug) return null;\n return `/product/${encodeURIComponent(favorite.slug)}`;\n}\n\n/** Drops one product from a list, used after the server confirms the removal. */\nexport function withoutFavorite(\n favorites: readonly CustomerFavorite[],\n productUniqid: string,\n): CustomerFavorite[] {\n return favorites.filter((favorite) => favorite.product_uniqid !== productUniqid);\n}\n\n"],"mappings":"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","names":[]}