@shoppexio/storefront-react 0.2.0 → 0.3.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/CHANGELOG.md +106 -0
- package/dist/account.js +1170 -196
- package/dist/account.js.map +1 -1
- package/dist/{chunk-ZEFZPUAW.js → chunk-NBOIS53R.js} +2 -1
- package/dist/chunk-NBOIS53R.js.map +1 -0
- package/dist/index.d.ts +151 -21
- package/dist/index.js +206 -61
- package/dist/index.js.map +1 -1
- package/eject/account/account-availability.ts +4 -1
- package/eject/account/account-page.tsx +26 -6
- package/eject/account/account-referrals.ts +179 -6
- package/eject/account/dashboard.tsx +1 -0
- package/eject/account/messages/ar.json +40 -3
- package/eject/account/messages/de.json +40 -3
- package/eject/account/messages/en.json +40 -3
- package/eject/account/messages/es.json +40 -3
- package/eject/account/messages/fi.json +40 -3
- package/eject/account/messages/fr.json +40 -3
- package/eject/account/messages/hi.json +40 -3
- package/eject/account/messages/it.json +40 -3
- package/eject/account/messages/lt.json +40 -3
- package/eject/account/messages/nl.json +40 -3
- package/eject/account/messages/pl.json +40 -3
- package/eject/account/messages/pt.json +40 -3
- package/eject/account/messages/ru.json +40 -3
- package/eject/account/messages/tr.json +40 -3
- package/eject/account/messages/uk.json +40 -3
- package/eject/account/messages/zh.json +40 -3
- package/eject/account/orders.tsx +53 -0
- package/eject/account/referrals.tsx +399 -153
- package/eject/account/rewards.tsx +38 -6
- package/eject/account/settings.tsx +98 -2
- package/eject/account/shared.tsx +3 -2
- package/package.json +4 -1
- package/dist/chunk-ZEFZPUAW.js.map +0 -1
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* depends on that answer must not hold up the coupons above it.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { useState, type ReactNode } from 'react';
|
|
20
|
+
import { useRef, useState, type ReactNode } from 'react';
|
|
21
21
|
import { Link } from 'react-router-dom';
|
|
22
22
|
import {
|
|
23
23
|
loyaltyDate,
|
|
@@ -218,6 +218,14 @@ function LoyaltyPoints({
|
|
|
218
218
|
}) {
|
|
219
219
|
const { state, reload } = resource;
|
|
220
220
|
const [notice, setNotice] = useState<string | null>(null);
|
|
221
|
+
/*
|
|
222
|
+
* The loyalty answer on screen when a conversion's outcome became unknown.
|
|
223
|
+
* Held by identity: only a re-read that ANSWERS replaces `state.data`, and
|
|
224
|
+
* the reload discards any read started before it. So the form stays locked
|
|
225
|
+
* until a fresh read succeeded; a failed one keeps this object and the lock,
|
|
226
|
+
* and offers the read again through the stale notice below.
|
|
227
|
+
*/
|
|
228
|
+
const [unconfirmedOn, setUnconfirmedOn] = useState<SdkCustomerLoyalty | null>(null);
|
|
221
229
|
|
|
222
230
|
/*
|
|
223
231
|
* A conversion re-reads the route so the balance and the activity list come
|
|
@@ -229,6 +237,10 @@ function LoyaltyPoints({
|
|
|
229
237
|
* the buyer pressed the button. Nothing is kept a second time here.
|
|
230
238
|
*/
|
|
231
239
|
const loyalty = state.status === 'ready' ? state.data : null;
|
|
240
|
+
// Read at the moment the answer arrives: the callback below was created by
|
|
241
|
+
// the render that started the request, and a re-read may have landed since.
|
|
242
|
+
const onScreen = useRef(loyalty);
|
|
243
|
+
onScreen.current = loyalty;
|
|
232
244
|
|
|
233
245
|
if (loyalty === null) {
|
|
234
246
|
return state.status === 'error' ? (
|
|
@@ -249,6 +261,7 @@ function LoyaltyPoints({
|
|
|
249
261
|
const stale = state.status === 'ready' ? state.staleMessage : undefined;
|
|
250
262
|
|
|
251
263
|
const offer = redeemOffer(loyalty);
|
|
264
|
+
const unconfirmed = unconfirmedOn === loyalty;
|
|
252
265
|
|
|
253
266
|
return (
|
|
254
267
|
<PointsSection>
|
|
@@ -271,7 +284,9 @@ function LoyaltyPoints({
|
|
|
271
284
|
|
|
272
285
|
{stale ? <AccountError message={stale} onRetry={reload} /> : null}
|
|
273
286
|
|
|
274
|
-
{
|
|
287
|
+
{unconfirmed ? (
|
|
288
|
+
<p role="status" className="sxa:mt-3 sxa:text-small sxa:text-muted">{t('rewards.loading')}</p>
|
|
289
|
+
) : notice ? <AccountNotice message={notice} /> : null}
|
|
275
290
|
|
|
276
291
|
{/* The minimum, for a balance that cannot clear it yet. The form is gone
|
|
277
292
|
because the route would refuse it, but the figure it refuses below is
|
|
@@ -286,8 +301,11 @@ function LoyaltyPoints({
|
|
|
286
301
|
<RedeemPoints
|
|
287
302
|
offer={offer}
|
|
288
303
|
currency={currency}
|
|
304
|
+
locked={unconfirmed}
|
|
289
305
|
onRedeemed={(message) => {
|
|
290
306
|
setNotice(message);
|
|
307
|
+
// No message is the unknown outcome; the two re-reads answer it.
|
|
308
|
+
setUnconfirmedOn(message === null ? onScreen.current : null);
|
|
291
309
|
reload();
|
|
292
310
|
onCredited();
|
|
293
311
|
}}
|
|
@@ -331,11 +349,15 @@ function PointsTile({ label, points }: { label: string; points: number }) {
|
|
|
331
349
|
function RedeemPoints({
|
|
332
350
|
offer,
|
|
333
351
|
currency,
|
|
352
|
+
locked,
|
|
334
353
|
onRedeemed,
|
|
335
354
|
}: {
|
|
336
355
|
offer: Extract<RedeemOffer, { kind: 'offered' }>;
|
|
337
356
|
currency: string;
|
|
338
|
-
|
|
357
|
+
/** An earlier conversion is unconfirmed until the points were re-read: nothing may be sent. */
|
|
358
|
+
locked: boolean;
|
|
359
|
+
/** The credited notice, or null when the server withheld its answer. */
|
|
360
|
+
onRedeemed: (message: string | null) => void;
|
|
339
361
|
}) {
|
|
340
362
|
const [typed, setTyped] = useState('');
|
|
341
363
|
const [confirming, setConfirming] = useState(false);
|
|
@@ -368,7 +390,7 @@ function RedeemPoints({
|
|
|
368
390
|
};
|
|
369
391
|
|
|
370
392
|
const submit = async () => {
|
|
371
|
-
if (preview.kind !== 'ready') return;
|
|
393
|
+
if (preview.kind !== 'ready' || locked) return;
|
|
372
394
|
|
|
373
395
|
setSubmitting(true);
|
|
374
396
|
setError(null);
|
|
@@ -383,6 +405,16 @@ function RedeemPoints({
|
|
|
383
405
|
idempotencyKey,
|
|
384
406
|
}));
|
|
385
407
|
|
|
408
|
+
if (response.code === 'RESPONSE_CONTRACT_VIOLATION') {
|
|
409
|
+
// The server ran the conversion and withheld only its answer: there is
|
|
410
|
+
// no failure to show and nothing to send again. The amount's key stays
|
|
411
|
+
// remembered, so a later send of it replays instead of debiting twice.
|
|
412
|
+
setTyped('');
|
|
413
|
+
setConfirming(false);
|
|
414
|
+
onRedeemed(null);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
386
418
|
if (!response.success || response.data === undefined) {
|
|
387
419
|
// The server's own sentence: "below the minimum", "not enough points"
|
|
388
420
|
// and "the programme was switched off" are all 400s, and only it knows
|
|
@@ -425,7 +457,7 @@ function RedeemPoints({
|
|
|
425
457
|
max={offer.balance}
|
|
426
458
|
step={1}
|
|
427
459
|
value={typed}
|
|
428
|
-
disabled={submitting}
|
|
460
|
+
disabled={submitting || locked}
|
|
429
461
|
placeholder={String(offer.minimumPoints)}
|
|
430
462
|
aria-invalid={preview.kind === 'not-a-number' || preview.kind === 'zero-value' ? true : undefined}
|
|
431
463
|
onChange={(event) => retype(event.target.value)}
|
|
@@ -434,7 +466,7 @@ function RedeemPoints({
|
|
|
434
466
|
{!confirming ? (
|
|
435
467
|
<button
|
|
436
468
|
type="button"
|
|
437
|
-
disabled={preview.kind !== 'ready' || submitting}
|
|
469
|
+
disabled={preview.kind !== 'ready' || submitting || locked}
|
|
438
470
|
className="sx-account-button sxa:card-button sxa:card-button--add sxa:disabled:cursor-not-allowed sxa:disabled:opacity-50"
|
|
439
471
|
onClick={() => setConfirming(true)}
|
|
440
472
|
>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The buyer's own details, their marketing consent, and their
|
|
2
|
+
* The buyer's own details, their marketing consent, their order mail and their
|
|
3
|
+
* signed-in devices.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
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()));
|
package/eject/account/shared.tsx
CHANGED
|
@@ -245,10 +245,11 @@ const STATUS_TONES: Record<string, string> = {
|
|
|
245
245
|
* the tone never applied.
|
|
246
246
|
*/
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
/** `label` replaces the status word when the caller has a better one; the tone still follows `status`. */
|
|
249
|
+
export function StatusChip({ status, label }: { status: string; label?: string }) {
|
|
249
250
|
const tone = STATUS_TONES[status.trim().toUpperCase()] ?? 'sxa:meta-chip--neutral';
|
|
250
251
|
return (
|
|
251
|
-
<span className={`sx-account-status sx-account-chip sxa:meta-chip sxa:meta-chip--status ${tone} sxa:shrink-0`}>{statusLabel(status)}</span>
|
|
252
|
+
<span className={`sx-account-status sx-account-chip sxa:meta-chip sxa:meta-chip--status ${tone} sxa:shrink-0`}>{label ?? statusLabel(status)}</span>
|
|
252
253
|
);
|
|
253
254
|
}
|
|
254
255
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shoppexio/storefront-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -65,10 +65,13 @@
|
|
|
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",
|
|
74
|
+
"jsdom-cjs": "npm:jsdom@26.1.0",
|
|
72
75
|
"react": "19.2.7",
|
|
73
76
|
"react-dom": "19.2.7",
|
|
74
77
|
"react-router-dom": "7.18.1",
|
|
@@ -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":[]}
|