@shoppexio/storefront-react 0.1.5 → 0.2.1

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.
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState, type ComponentType, type ReactElement, type ReactNode } from 'react';
2
+ import { notifyFavoritesChanged } from '@shoppexio/storefront-react';
2
3
  import { Link, Navigate, useLocation } from 'react-router-dom';
3
4
  import {
4
5
  isAccountTabOffered,
@@ -119,6 +120,26 @@ export interface AccountPageProps {
119
120
  notFound: ReactNode;
120
121
  }
121
122
 
123
+ /**
124
+ * The customer session changed inside this page (signed in or out) without a
125
+ * reload. Everything else on the storefront that reads the session must hear
126
+ * it: the favorites hearts reload, so a buyer who just signed in can save
127
+ * products and a signed-out page shows no one's saved set.
128
+ */
129
+ export function announceCustomerSessionChange(): void {
130
+ notifyFavoritesChanged();
131
+ }
132
+
133
+ /**
134
+ * Whether a sign-out request ended the session. The worker clears the session
135
+ * cookie on every response it produces, even a 500 from the backend, so an
136
+ * ANSWERED request means the buyer really is signed out. Only a result with no
137
+ * status at all means nothing reached the edge and the session is still live.
138
+ */
139
+ export function signOutEndedSession(result: { success: boolean; status?: number }): boolean {
140
+ return result.success || result.status !== undefined;
141
+ }
142
+
122
143
  export function AccountPage({ frame: Frame, notFound }: AccountPageProps) {
123
144
  const location = useLocation();
124
145
  const search = new URLSearchParams(location.search);
@@ -184,16 +205,14 @@ export function AccountPage({ frame: Frame, notFound }: AccountPageProps) {
184
205
  const result = await callCustomerApi((sdk) => sdk.logout());
185
206
  setSigningOut(false);
186
207
 
187
- // The worker clears the session cookie on every response it produces, even
188
- // a 500 from the backend — so an ANSWERED request means the buyer really is
189
- // signed out, whatever the status says. Only a result with no status at all
190
- // means nothing reached the edge: the cookie is still there, the session is
191
- // still live, and a signed-out page would be a lie the next reload exposes.
192
- if (!result.success && result.status === undefined) {
208
+ // A signed-out page for a session that is still live would be a lie the
209
+ // next reload exposes (see signOutEndedSession).
210
+ if (!signOutEndedSession(result)) {
193
211
  setSignOutError(result.message?.trim() || t('account.signOutFailed'));
194
212
  return;
195
213
  }
196
214
  setSession({ status: 'signed-out' });
215
+ announceCustomerSessionChange();
197
216
  };
198
217
 
199
218
  /*
@@ -255,6 +274,7 @@ export function AccountPage({ frame: Frame, notFound }: AccountPageProps) {
255
274
  // and their already-used code.
256
275
  setPrefill(spendSignInPrefill());
257
276
  setSession({ status: 'signed-in' });
277
+ announceCustomerSessionChange();
258
278
  }}
259
279
  />
260
280
  ) : null}
@@ -12,6 +12,7 @@
12
12
  * where the two differ, which is worse than not saying.
13
13
  */
14
14
 
15
+ import { notifyFavoritesChanged } from '@shoppexio/storefront-react';
15
16
  import { useEffect, useState } from 'react';
16
17
  import { Link } from 'react-router-dom';
17
18
  import {
@@ -96,6 +97,8 @@ export function AccountFavorites() {
96
97
  return;
97
98
  }
98
99
 
100
+ // Hearts elsewhere on the page follow the account.
101
+ notifyFavoritesChanged();
99
102
  // Dropped only once the server confirmed it. Removing the card first and
100
103
  // putting it back on failure makes the buyer watch their own action undo
101
104
  // itself, and leaves the two lists disagreeing whenever a retry is slow.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shoppexio/storefront-react",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
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",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/account-route.ts","../src/shoppex.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"],"mappings":"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","names":[]}