@doswiftly/storefront-sdk 17.0.0 → 18.0.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 +919 -0
- package/README.md +16 -4
- package/dist/core/auth/auth-client.d.ts +39 -3
- package/dist/core/auth/auth-client.d.ts.map +1 -1
- package/dist/core/auth/auth-client.js +51 -3
- package/dist/core/auth/cookie-config.d.ts +52 -3
- package/dist/core/auth/cookie-config.d.ts.map +1 -1
- package/dist/core/auth/cookie-config.js +60 -6
- package/dist/core/auth/handlers.d.ts +46 -0
- package/dist/core/auth/handlers.d.ts.map +1 -1
- package/dist/core/auth/handlers.js +9 -2
- package/dist/core/auth/session-events.d.ts +38 -0
- package/dist/core/auth/session-events.d.ts.map +1 -0
- package/dist/core/auth/session-events.js +35 -0
- package/dist/core/cart/cart-recovery.d.ts +23 -0
- package/dist/core/cart/cart-recovery.d.ts.map +1 -1
- package/dist/core/cart/cart-recovery.js +20 -3
- package/dist/core/cart/types.d.ts +2 -1
- package/dist/core/cart/types.d.ts.map +1 -1
- package/dist/core/cart/types.js +7 -1
- package/dist/core/client/create-client.d.ts.map +1 -1
- package/dist/core/client/create-client.js +7 -3
- package/dist/core/client/execute.d.ts +29 -3
- package/dist/core/client/execute.d.ts.map +1 -1
- package/dist/core/client/execute.js +174 -3
- package/dist/core/client/types.d.ts +50 -2
- package/dist/core/client/types.d.ts.map +1 -1
- package/dist/core/errors.d.ts +6 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +6 -0
- package/dist/core/generated/operation-types.d.ts +838 -221
- package/dist/core/generated/operation-types.d.ts.map +1 -1
- package/dist/core/generated/operation-types.js +560 -1
- package/dist/core/index.d.ts +6 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +12 -2
- package/dist/core/middleware/session-retry.d.ts +47 -0
- package/dist/core/middleware/session-retry.d.ts.map +1 -0
- package/dist/core/middleware/session-retry.js +71 -0
- package/dist/core/operations/auth.d.ts.map +1 -1
- package/dist/core/operations/auth.js +1 -0
- package/dist/core/operations/cart.d.ts.map +1 -1
- package/dist/core/operations/cart.js +15 -11
- package/dist/react/components/PaymentInstrumentSection.d.ts.map +1 -1
- package/dist/react/components/PaymentInstrumentSection.js +4 -4
- package/dist/react/components/PaymentInstrumentTile.d.ts +7 -7
- package/dist/react/components/PaymentInstrumentTile.d.ts.map +1 -1
- package/dist/react/components/PaymentInstrumentTile.js +4 -3
- package/dist/react/hooks/use-cart-manager.d.ts +104 -13
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +144 -12
- package/dist/react/hooks/use-login.d.ts.map +1 -1
- package/dist/react/hooks/use-login.js +3 -3
- package/dist/react/hooks/use-refresh-token.d.ts.map +1 -1
- package/dist/react/hooks/use-refresh-token.js +6 -4
- package/dist/react/hooks/use-session-expired.d.ts +16 -0
- package/dist/react/hooks/use-session-expired.d.ts.map +1 -0
- package/dist/react/hooks/use-session-expired.js +26 -0
- package/dist/react/hooks/use-session-refresh.d.ts +32 -0
- package/dist/react/hooks/use-session-refresh.d.ts.map +1 -0
- package/dist/react/hooks/use-session-refresh.js +147 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/providers/storefront-client-provider.d.ts +10 -1
- package/dist/react/providers/storefront-client-provider.d.ts.map +1 -1
- package/dist/react/providers/storefront-client-provider.js +38 -3
- package/dist/react/providers/storefront-provider.d.ts +51 -3
- package/dist/react/providers/storefront-provider.d.ts.map +1 -1
- package/dist/react/providers/storefront-provider.js +22 -5
- package/dist/react/server/create-storefront-auth-route.d.ts +63 -0
- package/dist/react/server/create-storefront-auth-route.d.ts.map +1 -0
- package/dist/react/server/create-storefront-auth-route.js +239 -0
- package/dist/react/server/get-initial-auth.d.ts +57 -0
- package/dist/react/server/get-initial-auth.d.ts.map +1 -0
- package/dist/react/server/get-initial-auth.js +55 -0
- package/dist/react/server/index.d.ts +3 -0
- package/dist/react/server/index.d.ts.map +1 -1
- package/dist/react/server/index.js +6 -0
- package/dist/react/stores/auth.store.d.ts +46 -2
- package/dist/react/stores/auth.store.d.ts.map +1 -1
- package/dist/react/stores/auth.store.js +19 -7
- package/package.json +4 -2
|
@@ -7,23 +7,53 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Per-operation strategy:
|
|
9
9
|
*
|
|
10
|
-
* | Operation
|
|
11
|
-
* |
|
|
12
|
-
* | `addItem`
|
|
13
|
-
* | `updateBuyerIdentity`
|
|
14
|
-
* | `setShippingAddress`
|
|
15
|
-
* | `updateDiscountCodes`
|
|
16
|
-
* | `updateNote`
|
|
17
|
-
* | `
|
|
18
|
-
* | `
|
|
10
|
+
* | Operation | Strategy | Why |
|
|
11
|
+
* | -------------------------- | ------------------------------------- | --------------------------------------------- |
|
|
12
|
+
* | `addItem` | Auto-replay (atomic `cartCreate`) | Storefront expects "add to cart" always works |
|
|
13
|
+
* | `updateBuyerIdentity` | Auto-replay | User just typed email/phone — keep it |
|
|
14
|
+
* | `setShippingAddress` | Auto-replay | User just typed address — keep it |
|
|
15
|
+
* | `updateDiscountCodes` | Auto-replay | Coupon valid independently of cart |
|
|
16
|
+
* | `updateNote` | Auto-replay | Stateless / idempotent |
|
|
17
|
+
* | `updateAttributes` | Auto-replay | Stateless metadata — atomic re-create OK |
|
|
18
|
+
* | `updateItem` | Bail + `cart-expired` event | `lineId` refers to a line in the dead cart |
|
|
19
|
+
* | `removeItem` | Bail + `cart-expired` event | jw. |
|
|
20
|
+
* | `setBillingAddress` | Bail + `cart-expired` event | Cart must exist (separate from shipping) |
|
|
21
|
+
* | `selectShippingMethod` | Bail + `cart-expired` event | Method tied to cart contents + address |
|
|
22
|
+
* | `selectPaymentMethod` | Bail + `cart-expired` event | Payment selection on existing cart state |
|
|
23
|
+
* | `clearPaymentSelection` | Bail + `cart-expired` event | Operates on existing cart payment fields |
|
|
24
|
+
* | `applyGiftCard` | Bail + `cart-expired` event | Balance allocation tied to cart total |
|
|
25
|
+
* | `removeGiftCard` | Bail + `cart-expired` event | `giftCardId` refers to row on dead cart |
|
|
26
|
+
* | `updateGiftCardRecipient` | Bail + `cart-expired` event | `lineId` refers to a line in the dead cart |
|
|
27
|
+
* | `complete` | Bail + `cart-expired` event | Finalised cart cannot be auto-recreated |
|
|
28
|
+
* | `createPayment` | Out of recovery scope | Operates on `orderId` (post-complete) |
|
|
19
29
|
*
|
|
20
30
|
* On bail the runner clears the cookie and calls every `onExpired` listener
|
|
21
31
|
* with a `CartExpiredEvent`. UI subscribes once globally and shows a toast /
|
|
22
32
|
* banner — caller code never writes `try / catch` per mutation.
|
|
23
33
|
*
|
|
34
|
+
* After `complete` success the hook auto-clears the `cart-id` cookie and
|
|
35
|
+
* resets status to `idle` — buyer returning to `/checkout` (back from the
|
|
36
|
+
* payment gateway, deep link, new tab) gets a fresh empty cart instead of the
|
|
37
|
+
* CONVERTED cart. Manual `clearCart()` remains available as an escape hatch.
|
|
38
|
+
*
|
|
24
39
|
* Auto-creates cart on first add. Cart id persisted in `cart-id` cookie
|
|
25
40
|
* (SSR/edge visible, 30 days, samesite=lax).
|
|
26
41
|
*
|
|
42
|
+
* ### Server-known cart-id (env seed, magic-link, iframe, customer service)
|
|
43
|
+
*
|
|
44
|
+
* Pass `{ initialCartId }` to seed the hook with a cart-id resolved server-side
|
|
45
|
+
* (read from URL params in a Route Handler, env var for dev fixtures, parent
|
|
46
|
+
* `postMessage` for embedded iframe, admin "view this cart" lookup). The hook
|
|
47
|
+
* applies priority `cookie wins → seed → auto-create`. The seed is eagerly
|
|
48
|
+
* written to the cart-id cookie so cross-tab tabs and standard recovery
|
|
49
|
+
* semantics operate on a canonical value. A stale seed goes through the same
|
|
50
|
+
* recovery flow as a stale cookie — `addItem` auto-replays through
|
|
51
|
+
* `cartCreate({ lines })`, state-dependent ops bail with `cart-expired`.
|
|
52
|
+
*
|
|
53
|
+
* Mirrors the `<StorefrontProvider initialAccessToken>` pattern for the
|
|
54
|
+
* cart-id half of the checkout state — both are seeds for the first render
|
|
55
|
+
* when the client cannot read the canonical source itself.
|
|
56
|
+
*
|
|
27
57
|
* @example
|
|
28
58
|
* ```tsx
|
|
29
59
|
* function CartUI() {
|
|
@@ -34,21 +64,46 @@
|
|
|
34
64
|
* return <button onClick={() => addItem([{ variantId, quantity: 1 }])}>Add</button>;
|
|
35
65
|
* }
|
|
36
66
|
* ```
|
|
67
|
+
*
|
|
68
|
+
* @example Server-known cart-id
|
|
69
|
+
* ```tsx
|
|
70
|
+
* // app/checkout/page.tsx — Server Component
|
|
71
|
+
* import { cookies } from 'next/headers';
|
|
72
|
+
* import { CART_COOKIE_NAME } from '@doswiftly/storefront-sdk';
|
|
73
|
+
*
|
|
74
|
+
* export default async function CheckoutPage() {
|
|
75
|
+
* const cookieJar = await cookies();
|
|
76
|
+
* const initialCartId =
|
|
77
|
+
* cookieJar.get(CART_COOKIE_NAME)?.value ?? process.env.NEXT_PUBLIC_DEV_CART_ID ?? null;
|
|
78
|
+
* return <CheckoutClient initialCartId={initialCartId} />;
|
|
79
|
+
* }
|
|
80
|
+
*
|
|
81
|
+
* // CheckoutClient.tsx — 'use client'
|
|
82
|
+
* function CheckoutClient({ initialCartId }: { initialCartId: string | null }) {
|
|
83
|
+
* const { complete, selectPaymentMethod, addItem } = useCartManager({ initialCartId });
|
|
84
|
+
* // ... rest unchanged — hook handles seed → cookie → recovery transparently
|
|
85
|
+
* }
|
|
86
|
+
* ```
|
|
37
87
|
*/
|
|
38
88
|
'use client';
|
|
39
89
|
import { useCallback, useMemo, useState } from 'react';
|
|
40
90
|
import { useStorefrontClientContext } from '../providers/storefront-client-provider';
|
|
41
91
|
import { createCartRecoveryRunner, recreateWithInput, } from '../../core/cart/cart-recovery';
|
|
42
92
|
import { createBrowserCartCookieStore } from '../cookies';
|
|
43
|
-
export function useCartManager() {
|
|
93
|
+
export function useCartManager(options) {
|
|
44
94
|
const { cartClient } = useStorefrontClientContext();
|
|
45
95
|
const [status, setStatus] = useState({ type: 'idle' });
|
|
46
96
|
// Cookie store is stateless — keep one instance per hook mount.
|
|
47
97
|
const cookieStore = useMemo(() => createBrowserCartCookieStore(), []);
|
|
98
|
+
// Normalize seed so the runner identity is stable across `undefined` /
|
|
99
|
+
// `null` props and only flips when the storefront actually swaps the seed.
|
|
100
|
+
const initialCartId = options?.initialCartId ?? null;
|
|
48
101
|
// Recovery runner is bound to cartClient identity (changes only on provider
|
|
49
102
|
// re-mount). Sharing one runner per hook gives concurrent operations a single
|
|
50
|
-
// recovery coordinator (Phase 0 mutex).
|
|
51
|
-
|
|
103
|
+
// recovery coordinator (Phase 0 mutex). `initialCartId` is part of the
|
|
104
|
+
// identity so a switch (e.g. cart switcher in a multi-cart B2B UI) rebuilds
|
|
105
|
+
// the runner instead of carrying the previous seed forward.
|
|
106
|
+
const runner = useMemo(() => createCartRecoveryRunner({ cartClient, cookieStore, initialCartId }), [cartClient, cookieStore, initialCartId]);
|
|
52
107
|
// Subscribe to cart-expired events. The returned unsubscribe function is
|
|
53
108
|
// bound to the current runner identity — wrap calls in
|
|
54
109
|
// `useEffect(() => onExpired(...), [onExpired])` so React re-subscribes when
|
|
@@ -98,6 +153,11 @@ export function useCartManager() {
|
|
|
98
153
|
run: (cartId) => cartClient.updateNote(cartId, note),
|
|
99
154
|
recreateAndRun: recreateWithInput({ note }),
|
|
100
155
|
}), 'Failed to update note'), [runner, cartClient, wrapMutation]);
|
|
156
|
+
const updateAttributes = useCallback((attributes) => wrapMutation('updateAttributes', () => runner.execute({
|
|
157
|
+
name: 'updateAttributes',
|
|
158
|
+
run: (cartId) => cartClient.updateAttributes(cartId, attributes),
|
|
159
|
+
recreateAndRun: recreateWithInput({ attributes }),
|
|
160
|
+
}), 'Failed to update cart attributes'), [runner, cartClient, wrapMutation]);
|
|
101
161
|
// --- Bail-on-stale mutations (no recreateAndRun — fires onExpired + throws) ---
|
|
102
162
|
const updateItem = useCallback((lines) => wrapMutation('updateItem', () => runner.execute({
|
|
103
163
|
name: 'updateItem',
|
|
@@ -107,6 +167,68 @@ export function useCartManager() {
|
|
|
107
167
|
name: 'removeItem',
|
|
108
168
|
run: (cartId) => cartClient.removeItems(cartId, lineIds),
|
|
109
169
|
}), 'Failed to remove from cart'), [runner, cartClient, wrapMutation]);
|
|
170
|
+
const setBillingAddress = useCallback((address) => wrapMutation('setBillingAddress', () => runner.execute({
|
|
171
|
+
name: 'setBillingAddress',
|
|
172
|
+
run: (cartId) => cartClient.setBillingAddress({ cartId, address }),
|
|
173
|
+
}), 'Failed to set billing address'), [runner, cartClient, wrapMutation]);
|
|
174
|
+
const selectShippingMethod = useCallback((input) => wrapMutation('selectShippingMethod', () => runner.execute({
|
|
175
|
+
name: 'selectShippingMethod',
|
|
176
|
+
run: (cartId) => cartClient.selectShippingMethod({ cartId, ...input }),
|
|
177
|
+
}), 'Failed to select shipping method'), [runner, cartClient, wrapMutation]);
|
|
178
|
+
const selectPaymentMethod = useCallback((input) => wrapMutation('selectPaymentMethod', () => runner.execute({
|
|
179
|
+
name: 'selectPaymentMethod',
|
|
180
|
+
run: (cartId) => cartClient.selectPaymentMethod({ cartId, ...input }),
|
|
181
|
+
}), 'Failed to select payment method'), [runner, cartClient, wrapMutation]);
|
|
182
|
+
const clearPaymentSelection = useCallback((input) => wrapMutation('clearPaymentSelection', () => runner.execute({
|
|
183
|
+
name: 'clearPaymentSelection',
|
|
184
|
+
run: (cartId) => cartClient.clearPaymentSelection({ cartId, ...(input ?? {}) }),
|
|
185
|
+
}), 'Failed to clear payment selection'), [runner, cartClient, wrapMutation]);
|
|
186
|
+
const applyGiftCard = useCallback((input) => wrapMutation('applyGiftCard', () => runner.execute({
|
|
187
|
+
name: 'applyGiftCard',
|
|
188
|
+
run: (cartId) => cartClient.applyGiftCard({ cartId, ...input }),
|
|
189
|
+
}), 'Failed to apply gift card'), [runner, cartClient, wrapMutation]);
|
|
190
|
+
const removeGiftCard = useCallback((input) => wrapMutation('removeGiftCard', () => runner.execute({
|
|
191
|
+
name: 'removeGiftCard',
|
|
192
|
+
run: (cartId) => cartClient.removeGiftCard({ cartId, ...input }),
|
|
193
|
+
}), 'Failed to remove gift card'), [runner, cartClient, wrapMutation]);
|
|
194
|
+
const updateGiftCardRecipient = useCallback((input) => wrapMutation('updateGiftCardRecipient', () => runner.execute({
|
|
195
|
+
name: 'updateGiftCardRecipient',
|
|
196
|
+
run: (cartId) => cartClient.updateGiftCardRecipient({ cartId, ...input }),
|
|
197
|
+
}), 'Failed to update gift card recipient'), [runner, cartClient, wrapMutation]);
|
|
198
|
+
// --- Completion lifecycle ---
|
|
199
|
+
/**
|
|
200
|
+
* Bail-on-stale completion. On success the cart is CONVERTED/locked, so the
|
|
201
|
+
* hook auto-clears the cart cookie and resets status to `idle`. A follow-up
|
|
202
|
+
* `addItem` recreates a fresh cart through the standard recovery runner.
|
|
203
|
+
* `wrapMutation` is intentionally bypassed — its success path leaves status
|
|
204
|
+
* as `{ type: 'success', operation: 'complete' }`, but the cart no longer
|
|
205
|
+
* exists in the cookie state, so `idle` is the truthful representation.
|
|
206
|
+
*/
|
|
207
|
+
const complete = useCallback(async (input) => {
|
|
208
|
+
setStatus({ type: 'loading', operation: 'complete' });
|
|
209
|
+
try {
|
|
210
|
+
const result = await runner.execute({
|
|
211
|
+
name: 'complete',
|
|
212
|
+
run: (cartId) => cartClient.complete({ cartId, ...(input ?? {}) }),
|
|
213
|
+
});
|
|
214
|
+
cookieStore.clear();
|
|
215
|
+
setStatus({ type: 'idle' });
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
const error = err instanceof Error ? err : new Error('Failed to complete cart');
|
|
220
|
+
setStatus({ type: 'error', operation: 'complete', error });
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
}, [runner, cartClient, cookieStore]);
|
|
224
|
+
/**
|
|
225
|
+
* Payment session creation. NOT a cart operation (works on `orderId` from
|
|
226
|
+
* `complete().order.id`) so the recovery runner is bypassed — there is no
|
|
227
|
+
* cart to recover from. Wrapped in `wrapMutation` only for status tracking
|
|
228
|
+
* so consumers can render a single `<Spinner label={status.operation} />`
|
|
229
|
+
* across the whole checkout lifecycle.
|
|
230
|
+
*/
|
|
231
|
+
const createPayment = useCallback((input) => wrapMutation('createPayment', () => cartClient.createPayment(input), 'Failed to create payment'), [cartClient, wrapMutation]);
|
|
110
232
|
// --- Lifecycle ---
|
|
111
233
|
const clearCart = useCallback(() => {
|
|
112
234
|
cookieStore.clear();
|
|
@@ -123,8 +245,18 @@ export function useCartManager() {
|
|
|
123
245
|
setShippingAddress,
|
|
124
246
|
updateDiscountCodes,
|
|
125
247
|
updateNote,
|
|
248
|
+
updateAttributes,
|
|
126
249
|
updateItem,
|
|
127
250
|
removeItem,
|
|
251
|
+
setBillingAddress,
|
|
252
|
+
selectShippingMethod,
|
|
253
|
+
selectPaymentMethod,
|
|
254
|
+
clearPaymentSelection,
|
|
255
|
+
applyGiftCard,
|
|
256
|
+
removeGiftCard,
|
|
257
|
+
updateGiftCardRecipient,
|
|
258
|
+
complete,
|
|
259
|
+
createPayment,
|
|
128
260
|
clearCart,
|
|
129
261
|
onExpired,
|
|
130
262
|
status,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-login.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,gHAAgH;IAChH,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACjE,iDAAiD;IACjD,WAAW,EAAE,OAAO,CAAC;IACrB,0GAA0G;IAC1G,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,
|
|
1
|
+
{"version":3,"file":"use-login.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,gHAAgH;IAChH,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACjE,iDAAiD;IACjD,WAAW,EAAE,OAAO,CAAC;IACrB,0GAA0G;IAC1G,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CAgEtE"}
|
|
@@ -40,14 +40,14 @@ export function useLogin(options = {}) {
|
|
|
40
40
|
firstName: customer.firstName ?? undefined,
|
|
41
41
|
lastName: customer.lastName ?? undefined,
|
|
42
42
|
phone: customer.phone ?? undefined,
|
|
43
|
-
}, result.accessToken);
|
|
43
|
+
}, result.accessToken, result.expiresAt);
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
46
|
-
setAuth({ id: '', email }, result.accessToken);
|
|
46
|
+
setAuth({ id: '', email }, result.accessToken, result.expiresAt);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
catch {
|
|
50
|
-
setAuth({ id: '', email }, result.accessToken);
|
|
50
|
+
setAuth({ id: '', email }, result.accessToken, result.expiresAt);
|
|
51
51
|
}
|
|
52
52
|
return {
|
|
53
53
|
success: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-refresh-token.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-refresh-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AASH,MAAM,WAAW,sBAAsB;IACrC,qFAAqF;IACrF,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,yDAAyD;IACzD,YAAY,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAChD,mDAAmD;IACnD,iBAAiB,EAAE,OAAO,CAAC;IAC3B,yFAAyF;IACzF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,qBAAqB,
|
|
1
|
+
{"version":3,"file":"use-refresh-token.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-refresh-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AASH,MAAM,WAAW,sBAAsB;IACrC,qFAAqF;IACrF,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,yDAAyD;IACzD,YAAY,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAChD,mDAAmD;IACnD,iBAAiB,EAAE,OAAO,CAAC;IAC3B,yFAAyF;IACzF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,qBAAqB,CAiD3F"}
|
|
@@ -36,10 +36,12 @@ export function useRefreshToken(options = {}) {
|
|
|
36
36
|
if (options.onSetToken) {
|
|
37
37
|
await options.onSetToken(result.accessToken);
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
// Thread the new token + expiry into the store unconditionally — `setAuth`
|
|
40
|
+
// accepts a null customer, so a server-seeded session without a loaded
|
|
41
|
+
// profile (SSO callback / magic link / dev seed) still gets its refreshed
|
|
42
|
+
// `expiresAt`, keeping the proactive scheduler armed (parity with the
|
|
43
|
+
// in-provider refresh + scheduler, which also pass `getState().customer`).
|
|
44
|
+
setAuth(authStore.getState().customer, result.accessToken, result.expiresAt);
|
|
43
45
|
return {
|
|
44
46
|
success: true,
|
|
45
47
|
userErrors: [],
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSessionExpired — subscribe to the global session-expired signal.
|
|
3
|
+
*
|
|
4
|
+
* Fired when the SDK can no longer keep the customer session alive: a proactive
|
|
5
|
+
* refresh failed on tab wake (R6.3), or a reactive refresh after a 401 also
|
|
6
|
+
* failed (R2.4). Use once near the app root to react globally — show a notice
|
|
7
|
+
* and redirect to sign-in (R14.2). No-op outside `StorefrontProvider`.
|
|
8
|
+
*/
|
|
9
|
+
import type { SessionExpiredEmitter, SessionExpiredEvent } from '../../core/auth/session-events';
|
|
10
|
+
/**
|
|
11
|
+
* Context carrying the provider-scoped session-expired emitter. Created in
|
|
12
|
+
* `StorefrontProvider` via `useRef` (Inv-3 — never a module-level singleton).
|
|
13
|
+
*/
|
|
14
|
+
export declare const SessionExpiredContext: import("react").Context<SessionExpiredEmitter | null>;
|
|
15
|
+
export declare function useSessionExpired(listener: (event: SessionExpiredEvent) => void): void;
|
|
16
|
+
//# sourceMappingURL=use-session-expired.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-session-expired.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-session-expired.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAEjG;;;GAGG;AACH,eAAO,MAAM,qBAAqB,uDAAoD,CAAC;AAEvF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,GAAG,IAAI,CAUtF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSessionExpired — subscribe to the global session-expired signal.
|
|
3
|
+
*
|
|
4
|
+
* Fired when the SDK can no longer keep the customer session alive: a proactive
|
|
5
|
+
* refresh failed on tab wake (R6.3), or a reactive refresh after a 401 also
|
|
6
|
+
* failed (R2.4). Use once near the app root to react globally — show a notice
|
|
7
|
+
* and redirect to sign-in (R14.2). No-op outside `StorefrontProvider`.
|
|
8
|
+
*/
|
|
9
|
+
'use client';
|
|
10
|
+
import { createContext, useContext, useEffect, useRef } from 'react';
|
|
11
|
+
/**
|
|
12
|
+
* Context carrying the provider-scoped session-expired emitter. Created in
|
|
13
|
+
* `StorefrontProvider` via `useRef` (Inv-3 — never a module-level singleton).
|
|
14
|
+
*/
|
|
15
|
+
export const SessionExpiredContext = createContext(null);
|
|
16
|
+
export function useSessionExpired(listener) {
|
|
17
|
+
const emitter = useContext(SessionExpiredContext);
|
|
18
|
+
// Keep the latest listener without re-subscribing on every render.
|
|
19
|
+
const ref = useRef(listener);
|
|
20
|
+
ref.current = listener;
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!emitter)
|
|
23
|
+
return;
|
|
24
|
+
return emitter.subscribe((event) => ref.current(event));
|
|
25
|
+
}, [emitter]);
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSessionRefresh — proactive, browser-only session-refresh scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Renews the access token shortly *before* it expires so an active buyer is
|
|
5
|
+
* never logged out mid-session, reschedules from each new expiry, and never runs
|
|
6
|
+
* on the server. On tab wake after the token already lapsed it tries once and —
|
|
7
|
+
* if the session can no longer be recovered — emits `session-expired` and clears
|
|
8
|
+
* local auth.
|
|
9
|
+
*
|
|
10
|
+
* The refresh goes through the same-origin BFF route via
|
|
11
|
+
* `AuthClient.refreshSession()` (`POST {authBasePath}/refresh`). The route handler
|
|
12
|
+
* reads the httpOnly refresh cookie server-side, rotates it against the backend,
|
|
13
|
+
* and sets the new first-party cookies — so an ALREADY-EXPIRED access token still
|
|
14
|
+
* refreshes (the old GraphQL `customerRefreshToken` could not, as it needed a
|
|
15
|
+
* valid access token). No `setToken` round-trip and no consumer callback.
|
|
16
|
+
*
|
|
17
|
+
* The timer lives in the effect closure (provider lifecycle), never in module state.
|
|
18
|
+
*/
|
|
19
|
+
import type { SessionExpiredEmitter } from '../../core/auth/session-events';
|
|
20
|
+
export interface UseSessionRefreshOptions {
|
|
21
|
+
/** Master switch. Defaults to `true` in the browser, `false` on the server. */
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
/** Refresh this many ms before the token's `expiresAt` (default 60_000). */
|
|
24
|
+
bufferMs?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Emitter the scheduler notifies when a wake-time refresh cannot recover the
|
|
27
|
+
* session. Provided by `StorefrontProvider`; subscribe via `useSessionExpired`.
|
|
28
|
+
*/
|
|
29
|
+
sessionExpiredEmitter?: SessionExpiredEmitter;
|
|
30
|
+
}
|
|
31
|
+
export declare function useSessionRefresh(options?: UseSessionRefreshOptions): void;
|
|
32
|
+
//# sourceMappingURL=use-session-refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-session-refresh.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-session-refresh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAE5E,MAAM,WAAW,wBAAwB;IACvC,+EAA+E;IAC/E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;CAC/C;AAYD,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,GAAG,IAAI,CAkH9E"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSessionRefresh — proactive, browser-only session-refresh scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Renews the access token shortly *before* it expires so an active buyer is
|
|
5
|
+
* never logged out mid-session, reschedules from each new expiry, and never runs
|
|
6
|
+
* on the server. On tab wake after the token already lapsed it tries once and —
|
|
7
|
+
* if the session can no longer be recovered — emits `session-expired` and clears
|
|
8
|
+
* local auth.
|
|
9
|
+
*
|
|
10
|
+
* The refresh goes through the same-origin BFF route via
|
|
11
|
+
* `AuthClient.refreshSession()` (`POST {authBasePath}/refresh`). The route handler
|
|
12
|
+
* reads the httpOnly refresh cookie server-side, rotates it against the backend,
|
|
13
|
+
* and sets the new first-party cookies — so an ALREADY-EXPIRED access token still
|
|
14
|
+
* refreshes (the old GraphQL `customerRefreshToken` could not, as it needed a
|
|
15
|
+
* valid access token). No `setToken` round-trip and no consumer callback.
|
|
16
|
+
*
|
|
17
|
+
* The timer lives in the effect closure (provider lifecycle), never in module state.
|
|
18
|
+
*/
|
|
19
|
+
'use client';
|
|
20
|
+
import { useEffect, useRef } from 'react';
|
|
21
|
+
import { useAuthStoreApi } from '../stores/store-context';
|
|
22
|
+
import { useStorefrontClientContext } from '../providers/storefront-client-provider';
|
|
23
|
+
const DEFAULT_BUFFER_MS = 60_000;
|
|
24
|
+
/** Backoff before retrying a proactive refresh that failed while the token was still valid. */
|
|
25
|
+
const PROACTIVE_RETRY_MS = 15_000;
|
|
26
|
+
/**
|
|
27
|
+
* When the token is already within the buffer window but still valid, space out
|
|
28
|
+
* refreshes by this much so a token whose lifetime is <= the buffer cannot
|
|
29
|
+
* busy-loop the scheduler.
|
|
30
|
+
*/
|
|
31
|
+
const WITHIN_BUFFER_RETRY_MS = 5_000;
|
|
32
|
+
export function useSessionRefresh(options = {}) {
|
|
33
|
+
const { enabled, bufferMs = DEFAULT_BUFFER_MS, sessionExpiredEmitter } = options;
|
|
34
|
+
const isBrowser = typeof window !== 'undefined';
|
|
35
|
+
const active = (enabled ?? isBrowser) && isBrowser;
|
|
36
|
+
const { authClient } = useStorefrontClientContext();
|
|
37
|
+
const authStore = useAuthStoreApi();
|
|
38
|
+
// Moving parts in refs so the scheduling effect re-runs only on structural
|
|
39
|
+
// deps (active / bufferMs / authBasePath / store identity), not on every render.
|
|
40
|
+
const authClientRef = useRef(authClient);
|
|
41
|
+
authClientRef.current = authClient;
|
|
42
|
+
const emitterRef = useRef(sessionExpiredEmitter);
|
|
43
|
+
emitterRef.current = sessionExpiredEmitter;
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!active)
|
|
46
|
+
return;
|
|
47
|
+
let timer;
|
|
48
|
+
let disposed = false;
|
|
49
|
+
let inFlight = false;
|
|
50
|
+
// Tracks the expiry we last scheduled against, so the store subscription
|
|
51
|
+
// ignores our own writes (we reschedule explicitly after a refresh).
|
|
52
|
+
let lastExpiresAt = authStore.getState().expiresAt;
|
|
53
|
+
const clearTimer = () => {
|
|
54
|
+
if (timer !== undefined) {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
timer = undefined;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const doRefresh = async (tokenAlreadyExpired) => {
|
|
60
|
+
if (inFlight)
|
|
61
|
+
return;
|
|
62
|
+
inFlight = true;
|
|
63
|
+
try {
|
|
64
|
+
// Same-origin BFF refresh: the route reads the httpOnly refresh cookie
|
|
65
|
+
// server-side, rotates it against the backend, and sets the new
|
|
66
|
+
// first-party cookies. No `setToken` round-trip — the route owns the cookie.
|
|
67
|
+
const result = await authClientRef.current.refreshSession();
|
|
68
|
+
if (disposed)
|
|
69
|
+
return;
|
|
70
|
+
lastExpiresAt = result.expiresAt;
|
|
71
|
+
authStore.getState().setAuth(authStore.getState().customer, result.accessToken, result.expiresAt);
|
|
72
|
+
schedule();
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (disposed)
|
|
76
|
+
return;
|
|
77
|
+
if (tokenAlreadyExpired) {
|
|
78
|
+
// The access token had already lapsed and the BFF refresh could not
|
|
79
|
+
// recover the session (refresh token expired/reused/revoked). Clear
|
|
80
|
+
// local auth and notify so the storefront can prompt re-login.
|
|
81
|
+
authStore.getState().clearAuth();
|
|
82
|
+
emitterRef.current?.emit({ reason: 'wake-refresh-failed', cause: err });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Token still valid — a proactive refresh failed transiently
|
|
86
|
+
// (network / server blip). Do NOT log the buyer out; retry shortly.
|
|
87
|
+
clearTimer();
|
|
88
|
+
timer = setTimeout(schedule, PROACTIVE_RETRY_MS);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
inFlight = false;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const schedule = () => {
|
|
96
|
+
clearTimer();
|
|
97
|
+
const { expiresAt, isAuthenticated } = authStore.getState();
|
|
98
|
+
if (!expiresAt || !isAuthenticated)
|
|
99
|
+
return;
|
|
100
|
+
const expiryMs = new Date(expiresAt).getTime();
|
|
101
|
+
if (Number.isNaN(expiryMs))
|
|
102
|
+
return;
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
let fireIn;
|
|
105
|
+
if (now >= expiryMs) {
|
|
106
|
+
// Already expired (e.g. tab slept past expiry) — recover ASAP.
|
|
107
|
+
fireIn = 0;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const untilBuffer = expiryMs - bufferMs - now;
|
|
111
|
+
// Within the buffer but still valid — space retries so a token whose
|
|
112
|
+
// lifetime is <= the buffer cannot busy-loop.
|
|
113
|
+
fireIn = untilBuffer > 0 ? untilBuffer : WITHIN_BUFFER_RETRY_MS;
|
|
114
|
+
}
|
|
115
|
+
// ALWAYS arm a timer — never call doRefresh synchronously. A synchronous
|
|
116
|
+
// call can re-enter while a refresh is in flight and be dropped by the
|
|
117
|
+
// in-flight guard, leaving the scheduler permanently stalled; the timer
|
|
118
|
+
// fires after the current call stack, by which point `inFlight` has reset.
|
|
119
|
+
// `tokenAlreadyExpired` is re-derived at fire time (the clock has moved).
|
|
120
|
+
timer = setTimeout(() => void doRefresh(Date.now() >= expiryMs), fireIn);
|
|
121
|
+
};
|
|
122
|
+
// Reschedule when expiresAt changes from outside (login, manual refresh).
|
|
123
|
+
const unsubscribe = authStore.subscribe((state) => {
|
|
124
|
+
if (disposed)
|
|
125
|
+
return;
|
|
126
|
+
if (state.expiresAt !== lastExpiresAt) {
|
|
127
|
+
lastExpiresAt = state.expiresAt;
|
|
128
|
+
schedule();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// Wake-from-sleep: background tabs throttle/pause timers — on return,
|
|
132
|
+
// recompute (and refresh immediately if the token already lapsed).
|
|
133
|
+
const onVisibility = () => {
|
|
134
|
+
if (document.visibilityState === 'visible' && !disposed)
|
|
135
|
+
schedule();
|
|
136
|
+
};
|
|
137
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
138
|
+
schedule();
|
|
139
|
+
return () => {
|
|
140
|
+
disposed = true;
|
|
141
|
+
clearTimer();
|
|
142
|
+
unsubscribe();
|
|
143
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
144
|
+
};
|
|
145
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
146
|
+
}, [active, bufferMs, authStore]);
|
|
147
|
+
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export { useAuth, type UseAuthOptions, type LoginResult, type LogoutResult, type
|
|
|
19
19
|
export { useLogin, type UseLoginOptions, type UseLoginReturn } from './hooks/use-login';
|
|
20
20
|
export { useLogout, type UseLogoutOptions, type UseLogoutReturn } from './hooks/use-logout';
|
|
21
21
|
export { useRefreshToken, type UseRefreshTokenOptions, type UseRefreshTokenReturn } from './hooks/use-refresh-token';
|
|
22
|
+
export { useSessionRefresh, type UseSessionRefreshOptions } from './hooks/use-session-refresh';
|
|
23
|
+
export { useSessionExpired } from './hooks/use-session-expired';
|
|
24
|
+
export type { SessionExpiredEvent, SessionExpiredReason, SessionExpiredEmitter } from '../core/auth/session-events';
|
|
22
25
|
export { useCartManager, type CartManagerOperation, type CartManagerStatus, type UseCartManagerResult } from './hooks/use-cart-manager';
|
|
23
26
|
export { useCart, type UseCartOptions, type UseCartResult, type ServerCartOperation } from './hooks/use-cart';
|
|
24
27
|
export { useStorefrontClient } from './hooks/use-storefront-client';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
package/dist/react/index.js
CHANGED
|
@@ -21,6 +21,8 @@ export { useAuth } from './hooks/use-auth';
|
|
|
21
21
|
export { useLogin } from './hooks/use-login';
|
|
22
22
|
export { useLogout } from './hooks/use-logout';
|
|
23
23
|
export { useRefreshToken } from './hooks/use-refresh-token';
|
|
24
|
+
export { useSessionRefresh } from './hooks/use-session-refresh';
|
|
25
|
+
export { useSessionExpired } from './hooks/use-session-expired';
|
|
24
26
|
export { useCartManager } from './hooks/use-cart-manager';
|
|
25
27
|
export { useCart } from './hooks/use-cart';
|
|
26
28
|
export { useStorefrontClient } from './hooks/use-storefront-client';
|
|
@@ -11,6 +11,7 @@ import { CartClient } from '../../core/cart/cart-client';
|
|
|
11
11
|
import { AuthClient } from '../../core/auth/auth-client';
|
|
12
12
|
import { type BotProtectionTokenProvider } from '../../core/middleware/bot-protection';
|
|
13
13
|
import type { StorefrontClient, StorefrontClientConfig, Middleware } from '../../core/client/types';
|
|
14
|
+
import type { SessionExpiredEmitter } from '../../core/auth/session-events';
|
|
14
15
|
export interface StorefrontClientContextValue {
|
|
15
16
|
client: StorefrontClient;
|
|
16
17
|
cartClient: CartClient;
|
|
@@ -28,8 +29,16 @@ export interface StorefrontClientProviderProps {
|
|
|
28
29
|
botProtection?: BotProtectionTokenProvider | null;
|
|
29
30
|
/** Operations that require bot protection (from shop query) */
|
|
30
31
|
botProtectionOperations?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Auth-level session-expired emitter (from StorefrontProvider). When present,
|
|
34
|
+
* a reactive 401 on a read query triggers a single deduped refresh + replay,
|
|
35
|
+
* while a 401 on a mutation — or a refresh that also fails — fires this emitter.
|
|
36
|
+
*/
|
|
37
|
+
sessionExpiredEmitter?: SessionExpiredEmitter;
|
|
38
|
+
/** Base path of the auth route handlers used to sync the httpOnly cookie after a reactive refresh (default `/api/auth`). */
|
|
39
|
+
authBasePath?: string;
|
|
31
40
|
}
|
|
32
|
-
export declare function StorefrontClientProvider({ children, config, middleware: customMiddleware, botProtection, botProtectionOperations, }: StorefrontClientProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export declare function StorefrontClientProvider({ children, config, middleware: customMiddleware, botProtection, botProtectionOperations, sessionExpiredEmitter, authBasePath, }: StorefrontClientProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
33
42
|
/**
|
|
34
43
|
* Get StorefrontClient context value.
|
|
35
44
|
* Must be used within StorefrontClientProvider.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storefront-client-provider.d.ts","sourceRoot":"","sources":["../../../src/react/providers/storefront-client-provider.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAA6C,MAAM,OAAO,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAIzD,OAAO,EAA2B,KAAK,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAKhH,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAEpG,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,UAAU,CAAC;CACxB;AAID,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,sBAAsB,CAAC;IAC/B;;;OAGG;IACH,UAAU,CAAC,EAAE,UAAU,EAAE,CAAC;IAC1B,oEAAoE;IACpE,aAAa,CAAC,EAAE,0BAA0B,GAAG,IAAI,CAAC;IAClD,+DAA+D;IAC/D,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"storefront-client-provider.d.ts","sourceRoot":"","sources":["../../../src/react/providers/storefront-client-provider.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAA6C,MAAM,OAAO,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAIzD,OAAO,EAA2B,KAAK,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAKhH,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAEpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAE5E,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,UAAU,CAAC;CACxB;AAID,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,sBAAsB,CAAC;IAC/B;;;OAGG;IACH,UAAU,CAAC,EAAE,UAAU,EAAE,CAAC;IAC1B,oEAAoE;IACpE,aAAa,CAAC,EAAE,0BAA0B,GAAG,IAAI,CAAC;IAClD,+DAA+D;IAC/D,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;IAC9C,4HAA4H;IAC5H,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,wBAAwB,CAAC,EACvC,QAAQ,EACR,MAAM,EACN,UAAU,EAAE,gBAAqB,EACjC,aAAa,EACb,uBAAuB,EACvB,qBAAqB,EACrB,YAAY,GACb,EAAE,6BAA6B,2CAyE/B;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,4BAA4B,CAMzE"}
|
|
@@ -20,15 +20,46 @@ import { retryMiddleware } from '../../core/middleware/retry';
|
|
|
20
20
|
import { timeoutMiddleware } from '../../core/middleware/timeout';
|
|
21
21
|
import { errorMiddleware } from '../../core/middleware/errors';
|
|
22
22
|
import { useAuthStoreApi, useCurrencyStoreApi, useLanguageStoreApi } from '../stores/store-context';
|
|
23
|
+
import { sessionRetryMiddleware } from '../../core/middleware/session-retry';
|
|
23
24
|
const StorefrontClientContext = createContext(null);
|
|
24
|
-
export function StorefrontClientProvider({ children, config, middleware: customMiddleware = [], botProtection, botProtectionOperations, }) {
|
|
25
|
+
export function StorefrontClientProvider({ children, config, middleware: customMiddleware = [], botProtection, botProtectionOperations, sessionExpiredEmitter, authBasePath, }) {
|
|
25
26
|
const authStore = useAuthStoreApi();
|
|
26
27
|
const currencyStore = useCurrencyStoreApi();
|
|
27
28
|
const languageStore = useLanguageStoreApi();
|
|
28
29
|
const value = useMemo(() => {
|
|
30
|
+
// Reactive-401 renewal via the same-origin BFF route (`refreshSession`): the
|
|
31
|
+
// route reads the httpOnly refresh cookie server-side and sets the new
|
|
32
|
+
// first-party cookies, so no GraphQL refresh and no `setToken` round-trip.
|
|
33
|
+
// Forward-declared so this closure can reach the AuthClient created below.
|
|
34
|
+
// Deduped so concurrent 401s share a single renewal.
|
|
35
|
+
let authClientForRefresh;
|
|
36
|
+
let inFlightRefresh = null;
|
|
37
|
+
const refresh = () => {
|
|
38
|
+
if (inFlightRefresh)
|
|
39
|
+
return inFlightRefresh;
|
|
40
|
+
inFlightRefresh = (async () => {
|
|
41
|
+
try {
|
|
42
|
+
const result = await authClientForRefresh.refreshSession();
|
|
43
|
+
authStore.getState().setAuth(authStore.getState().customer, result.accessToken, result.expiresAt);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
authStore.getState().clearAuth();
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
inFlightRefresh = null;
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
return inFlightRefresh;
|
|
55
|
+
};
|
|
29
56
|
const client = createStorefrontClient({
|
|
30
57
|
...config,
|
|
31
58
|
middleware: [
|
|
59
|
+
// Reactive 401 — OUTERMOST so the replay re-runs auth with the fresh token.
|
|
60
|
+
...(sessionExpiredEmitter
|
|
61
|
+
? [sessionRetryMiddleware({ refresh, onSessionExpired: (e) => sessionExpiredEmitter.emit(e) })]
|
|
62
|
+
: []),
|
|
32
63
|
// Header middleware (runs first)
|
|
33
64
|
authMiddleware(() => authStore.getState().accessToken),
|
|
34
65
|
currencyMiddleware(() => currencyStore.getState().currency),
|
|
@@ -49,10 +80,14 @@ export function StorefrontClientProvider({ children, config, middleware: customM
|
|
|
49
80
|
],
|
|
50
81
|
});
|
|
51
82
|
const cartClient = new CartClient(client);
|
|
52
|
-
|
|
83
|
+
// The AuthClient's `refreshSession()` posts to the same-origin BFF route at
|
|
84
|
+
// `${authBasePath}/refresh`; pass the configured base so a non-standard mount
|
|
85
|
+
// still resolves (default `/api/auth`).
|
|
86
|
+
const authClient = new AuthClient(client, { authBasePath });
|
|
87
|
+
authClientForRefresh = authClient;
|
|
53
88
|
return { client, cartClient, authClient };
|
|
54
89
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
55
|
-
}, [config.apiUrl, config.shopSlug, authStore, currencyStore, languageStore]);
|
|
90
|
+
}, [config.apiUrl, config.shopSlug, authStore, currencyStore, languageStore, sessionExpiredEmitter, authBasePath]);
|
|
56
91
|
return (_jsx(StorefrontClientContext.Provider, { value: value, children: children }));
|
|
57
92
|
}
|
|
58
93
|
/**
|