@doswiftly/storefront-sdk 17.0.0 → 18.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +976 -0
- package/README.md +47 -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 +133 -13
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +220 -16
- 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 +5 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/providers/cart-manager-provider.d.ts +50 -0
- package/dist/react/providers/cart-manager-provider.d.ts.map +1 -0
- package/dist/react/providers/cart-manager-provider.js +59 -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,79 @@
|
|
|
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
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
89
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
40
90
|
import { useStorefrontClientContext } from '../providers/storefront-client-provider';
|
|
41
|
-
import { createCartRecoveryRunner, recreateWithInput, } from '../../core/cart/cart-recovery';
|
|
91
|
+
import { createCartRecoveryRunner, recreateWithInput, CartRecoveryNotPossibleError, CartSessionRequiredError, } from '../../core/cart/cart-recovery';
|
|
42
92
|
import { createBrowserCartCookieStore } from '../cookies';
|
|
43
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Invoke an optional lifecycle callback without letting a thrown callback
|
|
95
|
+
* reject the cart mutation it wraps. A misbehaving UI side-effect (toast,
|
|
96
|
+
* navigation) must never corrupt cart state or surface as a failed mutation —
|
|
97
|
+
* but the throw is logged so the consumer bug stays debuggable.
|
|
98
|
+
*/
|
|
99
|
+
function safeInvoke(run) {
|
|
100
|
+
try {
|
|
101
|
+
run();
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
console.warn('[storefront-sdk] cart manager lifecycle callback threw', err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Cart expiry (`CartRecoveryNotPossibleError`) and session loss
|
|
109
|
+
* (`CartSessionRequiredError`) are surfaced through their own dedicated
|
|
110
|
+
* channels (`onExpired` / the session-expired event) and carry SDK-internal
|
|
111
|
+
* English messages. They are NOT routed to `onMutationError`, whose error is
|
|
112
|
+
* meant to be surfaceable to the buyer.
|
|
113
|
+
*/
|
|
114
|
+
function isDedicatedChannelError(err) {
|
|
115
|
+
return err instanceof CartRecoveryNotPossibleError || err instanceof CartSessionRequiredError;
|
|
116
|
+
}
|
|
117
|
+
export function useCartManager(options) {
|
|
44
118
|
const { cartClient } = useStorefrontClientContext();
|
|
45
119
|
const [status, setStatus] = useState({ type: 'idle' });
|
|
46
120
|
// Cookie store is stateless — keep one instance per hook mount.
|
|
47
121
|
const cookieStore = useMemo(() => createBrowserCartCookieStore(), []);
|
|
122
|
+
// Latest-callback ref: the mutation wrapper reads `lifecycleRef.current` so it
|
|
123
|
+
// always fires the current callbacks while keeping its own deps array empty
|
|
124
|
+
// (a stable `wrapMutation` avoids rebuilding every mutation on each render).
|
|
125
|
+
const lifecycleRef = useRef({});
|
|
126
|
+
lifecycleRef.current = {
|
|
127
|
+
onMutationStart: options?.onMutationStart,
|
|
128
|
+
onMutationSuccess: options?.onMutationSuccess,
|
|
129
|
+
onMutationError: options?.onMutationError,
|
|
130
|
+
};
|
|
131
|
+
// Normalize seed so the runner identity is stable across `undefined` /
|
|
132
|
+
// `null` props and only flips when the storefront actually swaps the seed.
|
|
133
|
+
const initialCartId = options?.initialCartId ?? null;
|
|
48
134
|
// Recovery runner is bound to cartClient identity (changes only on provider
|
|
49
135
|
// re-mount). Sharing one runner per hook gives concurrent operations a single
|
|
50
|
-
// recovery coordinator (Phase 0 mutex).
|
|
51
|
-
|
|
136
|
+
// recovery coordinator (Phase 0 mutex). `initialCartId` is part of the
|
|
137
|
+
// identity so a switch (e.g. cart switcher in a multi-cart B2B UI) rebuilds
|
|
138
|
+
// the runner instead of carrying the previous seed forward.
|
|
139
|
+
const runner = useMemo(() => createCartRecoveryRunner({ cartClient, cookieStore, initialCartId }), [cartClient, cookieStore, initialCartId]);
|
|
52
140
|
// Subscribe to cart-expired events. The returned unsubscribe function is
|
|
53
141
|
// bound to the current runner identity — wrap calls in
|
|
54
142
|
// `useEffect(() => onExpired(...), [onExpired])` so React re-subscribes when
|
|
@@ -58,14 +146,19 @@ export function useCartManager() {
|
|
|
58
146
|
// recovery semantics to the runner (which fires onExpired separately).
|
|
59
147
|
const wrapMutation = useCallback(async (operation, run, failureFallbackMessage) => {
|
|
60
148
|
setStatus({ type: 'loading', operation });
|
|
149
|
+
safeInvoke(() => lifecycleRef.current.onMutationStart?.(operation));
|
|
61
150
|
try {
|
|
62
151
|
const result = await run();
|
|
63
152
|
setStatus({ type: 'success', operation });
|
|
153
|
+
safeInvoke(() => lifecycleRef.current.onMutationSuccess?.(operation));
|
|
64
154
|
return result;
|
|
65
155
|
}
|
|
66
156
|
catch (err) {
|
|
67
157
|
const error = err instanceof Error ? err : new Error(failureFallbackMessage);
|
|
68
158
|
setStatus({ type: 'error', operation, error });
|
|
159
|
+
if (!isDedicatedChannelError(error)) {
|
|
160
|
+
safeInvoke(() => lifecycleRef.current.onMutationError?.(operation, error));
|
|
161
|
+
}
|
|
69
162
|
throw err;
|
|
70
163
|
}
|
|
71
164
|
}, []);
|
|
@@ -98,6 +191,11 @@ export function useCartManager() {
|
|
|
98
191
|
run: (cartId) => cartClient.updateNote(cartId, note),
|
|
99
192
|
recreateAndRun: recreateWithInput({ note }),
|
|
100
193
|
}), 'Failed to update note'), [runner, cartClient, wrapMutation]);
|
|
194
|
+
const updateAttributes = useCallback((attributes) => wrapMutation('updateAttributes', () => runner.execute({
|
|
195
|
+
name: 'updateAttributes',
|
|
196
|
+
run: (cartId) => cartClient.updateAttributes(cartId, attributes),
|
|
197
|
+
recreateAndRun: recreateWithInput({ attributes }),
|
|
198
|
+
}), 'Failed to update cart attributes'), [runner, cartClient, wrapMutation]);
|
|
101
199
|
// --- Bail-on-stale mutations (no recreateAndRun — fires onExpired + throws) ---
|
|
102
200
|
const updateItem = useCallback((lines) => wrapMutation('updateItem', () => runner.execute({
|
|
103
201
|
name: 'updateItem',
|
|
@@ -107,6 +205,73 @@ export function useCartManager() {
|
|
|
107
205
|
name: 'removeItem',
|
|
108
206
|
run: (cartId) => cartClient.removeItems(cartId, lineIds),
|
|
109
207
|
}), 'Failed to remove from cart'), [runner, cartClient, wrapMutation]);
|
|
208
|
+
const setBillingAddress = useCallback((address) => wrapMutation('setBillingAddress', () => runner.execute({
|
|
209
|
+
name: 'setBillingAddress',
|
|
210
|
+
run: (cartId) => cartClient.setBillingAddress({ cartId, address }),
|
|
211
|
+
}), 'Failed to set billing address'), [runner, cartClient, wrapMutation]);
|
|
212
|
+
const selectShippingMethod = useCallback((input) => wrapMutation('selectShippingMethod', () => runner.execute({
|
|
213
|
+
name: 'selectShippingMethod',
|
|
214
|
+
run: (cartId) => cartClient.selectShippingMethod({ cartId, ...input }),
|
|
215
|
+
}), 'Failed to select shipping method'), [runner, cartClient, wrapMutation]);
|
|
216
|
+
const selectPaymentMethod = useCallback((input) => wrapMutation('selectPaymentMethod', () => runner.execute({
|
|
217
|
+
name: 'selectPaymentMethod',
|
|
218
|
+
run: (cartId) => cartClient.selectPaymentMethod({ cartId, ...input }),
|
|
219
|
+
}), 'Failed to select payment method'), [runner, cartClient, wrapMutation]);
|
|
220
|
+
const clearPaymentSelection = useCallback((input) => wrapMutation('clearPaymentSelection', () => runner.execute({
|
|
221
|
+
name: 'clearPaymentSelection',
|
|
222
|
+
run: (cartId) => cartClient.clearPaymentSelection({ cartId, ...(input ?? {}) }),
|
|
223
|
+
}), 'Failed to clear payment selection'), [runner, cartClient, wrapMutation]);
|
|
224
|
+
const applyGiftCard = useCallback((input) => wrapMutation('applyGiftCard', () => runner.execute({
|
|
225
|
+
name: 'applyGiftCard',
|
|
226
|
+
run: (cartId) => cartClient.applyGiftCard({ cartId, ...input }),
|
|
227
|
+
}), 'Failed to apply gift card'), [runner, cartClient, wrapMutation]);
|
|
228
|
+
const removeGiftCard = useCallback((input) => wrapMutation('removeGiftCard', () => runner.execute({
|
|
229
|
+
name: 'removeGiftCard',
|
|
230
|
+
run: (cartId) => cartClient.removeGiftCard({ cartId, ...input }),
|
|
231
|
+
}), 'Failed to remove gift card'), [runner, cartClient, wrapMutation]);
|
|
232
|
+
const updateGiftCardRecipient = useCallback((input) => wrapMutation('updateGiftCardRecipient', () => runner.execute({
|
|
233
|
+
name: 'updateGiftCardRecipient',
|
|
234
|
+
run: (cartId) => cartClient.updateGiftCardRecipient({ cartId, ...input }),
|
|
235
|
+
}), 'Failed to update gift card recipient'), [runner, cartClient, wrapMutation]);
|
|
236
|
+
// --- Completion lifecycle ---
|
|
237
|
+
/**
|
|
238
|
+
* Bail-on-stale completion. On success the cart is CONVERTED/locked, so the
|
|
239
|
+
* hook auto-clears the cart cookie and resets status to `idle`. A follow-up
|
|
240
|
+
* `addItem` recreates a fresh cart through the standard recovery runner.
|
|
241
|
+
* `wrapMutation` is intentionally bypassed — its success path leaves status
|
|
242
|
+
* as `{ type: 'success', operation: 'complete' }`, but the cart no longer
|
|
243
|
+
* exists in the cookie state, so `idle` is the truthful representation.
|
|
244
|
+
*/
|
|
245
|
+
const complete = useCallback(async (input) => {
|
|
246
|
+
setStatus({ type: 'loading', operation: 'complete' });
|
|
247
|
+
safeInvoke(() => lifecycleRef.current.onMutationStart?.('complete'));
|
|
248
|
+
try {
|
|
249
|
+
const result = await runner.execute({
|
|
250
|
+
name: 'complete',
|
|
251
|
+
run: (cartId) => cartClient.complete({ cartId, ...(input ?? {}) }),
|
|
252
|
+
});
|
|
253
|
+
cookieStore.clear();
|
|
254
|
+
setStatus({ type: 'idle' });
|
|
255
|
+
safeInvoke(() => lifecycleRef.current.onMutationSuccess?.('complete'));
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
const error = err instanceof Error ? err : new Error('Failed to complete cart');
|
|
260
|
+
setStatus({ type: 'error', operation: 'complete', error });
|
|
261
|
+
if (!isDedicatedChannelError(error)) {
|
|
262
|
+
safeInvoke(() => lifecycleRef.current.onMutationError?.('complete', error));
|
|
263
|
+
}
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
}, [runner, cartClient, cookieStore]);
|
|
267
|
+
/**
|
|
268
|
+
* Payment session creation. NOT a cart operation (works on `orderId` from
|
|
269
|
+
* `complete().order.id`) so the recovery runner is bypassed — there is no
|
|
270
|
+
* cart to recover from. Wrapped in `wrapMutation` only for status tracking
|
|
271
|
+
* so consumers can render a single `<Spinner label={status.operation} />`
|
|
272
|
+
* across the whole checkout lifecycle.
|
|
273
|
+
*/
|
|
274
|
+
const createPayment = useCallback((input) => wrapMutation('createPayment', () => cartClient.createPayment(input), 'Failed to create payment'), [cartClient, wrapMutation]);
|
|
110
275
|
// --- Lifecycle ---
|
|
111
276
|
const clearCart = useCallback(() => {
|
|
112
277
|
cookieStore.clear();
|
|
@@ -115,7 +280,11 @@ export function useCartManager() {
|
|
|
115
280
|
// Backward-compatible derived selectors over the tagged-union status.
|
|
116
281
|
const isLoading = status.type === 'loading';
|
|
117
282
|
const error = status.type === 'error' ? status.error.message : null;
|
|
118
|
-
|
|
283
|
+
// Memoize the aggregate so the object identity changes only when `status`
|
|
284
|
+
// flips (the sole reactive field) — every method is already useCallback-stable.
|
|
285
|
+
// Keeps `<CartManagerProvider>`'s Context value referentially stable across
|
|
286
|
+
// unrelated re-renders, matching the sibling StorefrontClientProvider.
|
|
287
|
+
return useMemo(() => ({
|
|
119
288
|
getCart,
|
|
120
289
|
getCartId,
|
|
121
290
|
addItem,
|
|
@@ -123,12 +292,47 @@ export function useCartManager() {
|
|
|
123
292
|
setShippingAddress,
|
|
124
293
|
updateDiscountCodes,
|
|
125
294
|
updateNote,
|
|
295
|
+
updateAttributes,
|
|
126
296
|
updateItem,
|
|
127
297
|
removeItem,
|
|
298
|
+
setBillingAddress,
|
|
299
|
+
selectShippingMethod,
|
|
300
|
+
selectPaymentMethod,
|
|
301
|
+
clearPaymentSelection,
|
|
302
|
+
applyGiftCard,
|
|
303
|
+
removeGiftCard,
|
|
304
|
+
updateGiftCardRecipient,
|
|
305
|
+
complete,
|
|
306
|
+
createPayment,
|
|
128
307
|
clearCart,
|
|
129
308
|
onExpired,
|
|
130
309
|
status,
|
|
131
310
|
isLoading,
|
|
132
311
|
error,
|
|
133
|
-
}
|
|
312
|
+
}), [
|
|
313
|
+
getCart,
|
|
314
|
+
getCartId,
|
|
315
|
+
addItem,
|
|
316
|
+
updateBuyerIdentity,
|
|
317
|
+
setShippingAddress,
|
|
318
|
+
updateDiscountCodes,
|
|
319
|
+
updateNote,
|
|
320
|
+
updateAttributes,
|
|
321
|
+
updateItem,
|
|
322
|
+
removeItem,
|
|
323
|
+
setBillingAddress,
|
|
324
|
+
selectShippingMethod,
|
|
325
|
+
selectPaymentMethod,
|
|
326
|
+
clearPaymentSelection,
|
|
327
|
+
applyGiftCard,
|
|
328
|
+
removeGiftCard,
|
|
329
|
+
updateGiftCardRecipient,
|
|
330
|
+
complete,
|
|
331
|
+
createPayment,
|
|
332
|
+
clearCart,
|
|
333
|
+
onExpired,
|
|
334
|
+
status,
|
|
335
|
+
isLoading,
|
|
336
|
+
error,
|
|
337
|
+
]);
|
|
134
338
|
}
|
|
@@ -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
|
@@ -15,11 +15,15 @@ export { StorefrontProvider, type StorefrontProviderProps } from './providers/st
|
|
|
15
15
|
export { StorefrontClientProvider, type StorefrontClientProviderProps } from './providers/storefront-client-provider';
|
|
16
16
|
export { CurrencyProvider, type CurrencyProviderProps } from './providers/currency-provider';
|
|
17
17
|
export { LanguageProvider, type LanguageProviderProps } from './providers/language-provider';
|
|
18
|
+
export { CartManagerProvider, useCartManagerContext, type CartManagerProviderProps } from './providers/cart-manager-provider';
|
|
18
19
|
export { useAuth, type UseAuthOptions, type LoginResult, type LogoutResult, type TokenRefreshResult } from './hooks/use-auth';
|
|
19
20
|
export { useLogin, type UseLoginOptions, type UseLoginReturn } from './hooks/use-login';
|
|
20
21
|
export { useLogout, type UseLogoutOptions, type UseLogoutReturn } from './hooks/use-logout';
|
|
21
22
|
export { useRefreshToken, type UseRefreshTokenOptions, type UseRefreshTokenReturn } from './hooks/use-refresh-token';
|
|
22
|
-
export {
|
|
23
|
+
export { useSessionRefresh, type UseSessionRefreshOptions } from './hooks/use-session-refresh';
|
|
24
|
+
export { useSessionExpired } from './hooks/use-session-expired';
|
|
25
|
+
export type { SessionExpiredEvent, SessionExpiredReason, SessionExpiredEmitter } from '../core/auth/session-events';
|
|
26
|
+
export { useCartManager, type CartManagerOperation, type CartManagerStatus, type UseCartManagerResult, type UseCartManagerOptions, type CartManagerLifecycleCallbacks } from './hooks/use-cart-manager';
|
|
23
27
|
export { useCart, type UseCartOptions, type UseCartResult, type ServerCartOperation } from './hooks/use-cart';
|
|
24
28
|
export { useStorefrontClient } from './hooks/use-storefront-client';
|
|
25
29
|
export { useCurrency } from './hooks/use-currency';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AACnG,OAAO,EAAE,wBAAwB,EAAE,KAAK,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AACtH,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|