@doswiftly/storefront-sdk 18.0.0 → 19.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 +122 -0
- package/README.md +31 -0
- package/dist/react/cookies.d.ts +5 -22
- package/dist/react/cookies.d.ts.map +1 -1
- package/dist/react/cookies.js +5 -47
- package/dist/react/hooks/use-cart-manager.d.ts +30 -1
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +76 -4
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +4 -2
- 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/server/cookie-readers.d.ts +29 -0
- package/dist/react/server/cookie-readers.d.ts.map +1 -0
- package/dist/react/server/cookie-readers.js +52 -0
- package/dist/react/server/index.d.ts +1 -0
- package/dist/react/server/index.d.ts.map +1 -1
- package/dist/react/server/index.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,127 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 19.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 7ee241f: Move the server-side cookie readers to the server entry and rename them for intent.
|
|
8
|
+
|
|
9
|
+
The two async cookie readers now live in `@doswiftly/storefront-sdk/react/server` (next to
|
|
10
|
+
`getInitialAuth`) and are renamed to describe what they read instead of how:
|
|
11
|
+
|
|
12
|
+
| Removed (from `@doswiftly/storefront-sdk/react`) | New (in `@doswiftly/storefront-sdk/react/server`) |
|
|
13
|
+
| ------------------------------------------------ | ------------------------------------------------- |
|
|
14
|
+
| `getCurrencyFromCookieAsync()` | `readCurrencyCookie()` |
|
|
15
|
+
| `getCartIdFromCookieAsync()` | `readCartIdCookie()` |
|
|
16
|
+
|
|
17
|
+
Both keep the same return type, `Promise<string | null>`, and read the same cookies
|
|
18
|
+
(`preferred-currency`, `cart-id`). They are still server-first with a `document.cookie` fallback,
|
|
19
|
+
so they remain safe to call from either side — these cookies are readable (not httpOnly).
|
|
20
|
+
|
|
21
|
+
`getCookie`, `setCookie`, `deleteCookie`, and `createBrowserCartCookieStore` are unchanged and
|
|
22
|
+
still exported from `@doswiftly/storefront-sdk/react`.
|
|
23
|
+
|
|
24
|
+
**Why**: the readers depend on `next/headers`, a server-only API, but were exported from the
|
|
25
|
+
client React entry — so the entry's own description ("client-side; for server use `next/headers`")
|
|
26
|
+
contradicted two of its functions. Moving them next to `getInitialAuth` keeps `next/headers` out of
|
|
27
|
+
the client entry and gives both cold-start cookie reads (auth, and now cart-id / currency) one home.
|
|
28
|
+
|
|
29
|
+
**Migration** — update the import path and the names:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// Before — client React entry
|
|
33
|
+
import {
|
|
34
|
+
getCurrencyFromCookieAsync,
|
|
35
|
+
getCartIdFromCookieAsync,
|
|
36
|
+
} from "@doswiftly/storefront-sdk/react";
|
|
37
|
+
const currency = await getCurrencyFromCookieAsync();
|
|
38
|
+
const cartId = await getCartIdFromCookieAsync();
|
|
39
|
+
|
|
40
|
+
// After — server entry (use in a Server Component)
|
|
41
|
+
import {
|
|
42
|
+
readCurrencyCookie,
|
|
43
|
+
readCartIdCookie,
|
|
44
|
+
} from "@doswiftly/storefront-sdk/react/server";
|
|
45
|
+
const currency = await readCurrencyCookie();
|
|
46
|
+
const cartId = await readCartIdCookie();
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Reading the cart-id server-side is the supported way to resolve a returning buyer's cart on the
|
|
50
|
+
first render — including when a separate checkout deployment shares the storefront domain:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// app/checkout/page.tsx (Server Component)
|
|
54
|
+
import { readCartIdCookie } from "@doswiftly/storefront-sdk/react/server";
|
|
55
|
+
|
|
56
|
+
const cartId = await readCartIdCookie();
|
|
57
|
+
const cart = cartId ? await fetchCart(cartId) : null;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Migration checklist**:
|
|
61
|
+
- [ ] Replace `getCurrencyFromCookieAsync` / `getCartIdFromCookieAsync` imports from
|
|
62
|
+
`@doswiftly/storefront-sdk/react` with `readCurrencyCookie` / `readCartIdCookie` from
|
|
63
|
+
`@doswiftly/storefront-sdk/react/server`.
|
|
64
|
+
- [ ] For purely client-side reads, use `getCookie('preferred-currency')` /
|
|
65
|
+
`getCookie('cart-id')` (or `createBrowserCartCookieStore()` for the cart id) from
|
|
66
|
+
`@doswiftly/storefront-sdk/react` instead.
|
|
67
|
+
|
|
68
|
+
## 18.1.0
|
|
69
|
+
|
|
70
|
+
### Minor Changes
|
|
71
|
+
|
|
72
|
+
- ff03fd3: Add `<CartManagerProvider>` + `useCartManagerContext()` so a single cart manager can be shared across a checkout tree, plus optional lifecycle callbacks on `useCartManager` and the provider.
|
|
73
|
+
|
|
74
|
+
**Why** — `useCartManager` keeps per-mount state (loading status, recovery coordinator, cart-expired listeners). Calling it in several components creates independent managers, so a checkout that wants one loading indicator, one recovery queue, and one cart-expired subscription had to hand-roll a React Context wrapper. This ships that wrapper as a first-class, opt-in provider, matching the provider-first pattern already used by `<StorefrontProvider>`, `<CurrencyProvider>`, and `<CartProvider>`.
|
|
75
|
+
|
|
76
|
+
**Additive (backward-compatible)**:
|
|
77
|
+
1. `<CartManagerProvider>` creates one `useCartManager` instance and exposes it through Context; `useCartManagerContext()` reads it (throws when used outside the provider). The provider must be rendered inside `<StorefrontProvider>`.
|
|
78
|
+
2. New optional lifecycle callbacks, accepted both by `useCartManager(options)` and as `<CartManagerProvider>` props:
|
|
79
|
+
- `onMutationStart(operation)` — fired when a mutation starts.
|
|
80
|
+
- `onMutationSuccess(operation)` — fired after it resolves.
|
|
81
|
+
- `onMutationError(operation, error)` — fired with a buyer-surfaceable error (its message comes from the backend). Cart expiry and session loss are delivered through the dedicated `onExpired` / session-expired channels and do NOT trigger `onMutationError`, so `toast.error(error.message)` is safe to wire.
|
|
82
|
+
|
|
83
|
+
A transient missing-cart error that the manager transparently recovers from resolves as success. Callbacks are invoked defensively — a throwing callback never rejects the underlying mutation (it is logged via `console.warn`).
|
|
84
|
+
|
|
85
|
+
3. New exported types: `CartManagerProviderProps`, `CartManagerLifecycleCallbacks`, `UseCartManagerOptions`.
|
|
86
|
+
|
|
87
|
+
**Usage example**:
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
"use client";
|
|
91
|
+
import { useRouter } from "next/navigation";
|
|
92
|
+
import { toast } from "sonner";
|
|
93
|
+
import {
|
|
94
|
+
CartManagerProvider,
|
|
95
|
+
useCartManagerContext,
|
|
96
|
+
} from "@doswiftly/storefront-sdk/react";
|
|
97
|
+
|
|
98
|
+
function Checkout({ initialCartId }: { initialCartId: string | null }) {
|
|
99
|
+
const router = useRouter();
|
|
100
|
+
return (
|
|
101
|
+
<CartManagerProvider
|
|
102
|
+
initialCartId={initialCartId}
|
|
103
|
+
onMutationSuccess={() => router.refresh()}
|
|
104
|
+
onMutationError={(operation, error) => toast.error(error.message)}
|
|
105
|
+
>
|
|
106
|
+
<CheckoutForm />
|
|
107
|
+
</CartManagerProvider>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function CheckoutForm() {
|
|
112
|
+
// one shared manager for the whole form
|
|
113
|
+
const { addItem, complete, status } = useCartManagerContext();
|
|
114
|
+
// ...
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Migration checklist** — none required; everything is opt-in.
|
|
119
|
+
- [ ] Optional: replace a hand-rolled `useCartManager` Context wrapper with `<CartManagerProvider>` + `useCartManagerContext()`.
|
|
120
|
+
- [ ] Optional: move per-call toast / refresh side-effects into `onMutationSuccess` / `onMutationError` on the provider.
|
|
121
|
+
- [ ] Keep calling `useCartManager()` directly where you want independent managers (e.g. multi-cart B2B, an admin "view this cart" panel).
|
|
122
|
+
|
|
123
|
+
**Standards reference** — provider-first sharing of a stateful instance with listeners and async initialisation is the established React-ecosystem pattern (`QueryClientProvider`, Stripe `<Elements>`), and mirrors the provider-first cart APIs of leading commerce SDKs.
|
|
124
|
+
|
|
3
125
|
## 18.0.0
|
|
4
126
|
|
|
5
127
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -493,6 +493,37 @@ export function CartButton() {
|
|
|
493
493
|
|
|
494
494
|
Methods returning `CartMutationOutcome` are: `addItem`, `updateBuyerIdentity`, `setShippingAddress`, `updateDiscountCodes`, `updateNote` (auto-replay) and `updateItem`, `removeItem` (bail + `onExpired`). Use `clearCart` to reset locally without backend call.
|
|
495
495
|
|
|
496
|
+
### React: `<CartManagerProvider>` (shared instance)
|
|
497
|
+
|
|
498
|
+
`useCartManager` keeps per-mount state, so calling it in several components creates independent managers. To share one source of truth across a checkout — one loading state, one recovery queue, one `cart-expired` subscription — wrap the subtree in `<CartManagerProvider>` (inside `<StorefrontProvider>`) and read it with `useCartManagerContext()`. The provider also takes lifecycle callbacks so cross-cutting side-effects live in one place:
|
|
499
|
+
|
|
500
|
+
```tsx
|
|
501
|
+
'use client';
|
|
502
|
+
import { useRouter } from 'next/navigation';
|
|
503
|
+
import { toast } from 'sonner';
|
|
504
|
+
import { CartManagerProvider, useCartManagerContext } from '@doswiftly/storefront-sdk/react';
|
|
505
|
+
|
|
506
|
+
function Checkout({ initialCartId }: { initialCartId: string | null }) {
|
|
507
|
+
const router = useRouter();
|
|
508
|
+
return (
|
|
509
|
+
<CartManagerProvider
|
|
510
|
+
initialCartId={initialCartId}
|
|
511
|
+
onMutationSuccess={() => router.refresh()}
|
|
512
|
+
onMutationError={(operation, error) => toast.error(error.message)}
|
|
513
|
+
>
|
|
514
|
+
<CheckoutForm />
|
|
515
|
+
</CartManagerProvider>
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function CheckoutForm() {
|
|
520
|
+
const { addItem, complete, status } = useCartManagerContext();
|
|
521
|
+
// ...
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
For deliberately independent managers (multi-cart B2B, an admin "view this cart" panel) call `useCartManager()` directly instead.
|
|
526
|
+
|
|
496
527
|
### AuthClient
|
|
497
528
|
|
|
498
529
|
```typescript
|
package/dist/react/cookies.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Client-side cookie utilities for SDK consumers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Synchronous read/write over `document.cookie`, SSR-safe via a `typeof document`
|
|
5
|
+
* guard (return null / no-op when there is no document). To read these cookies in
|
|
6
|
+
* a Next.js Server Component use the `@doswiftly/storefront-sdk/react/server`
|
|
7
|
+
* entry (`readCartIdCookie`, `readCurrencyCookie`).
|
|
6
8
|
*/
|
|
7
9
|
/**
|
|
8
10
|
* Get cookie value by name (client-side only).
|
|
@@ -44,23 +46,4 @@ import type { CartCookieStore } from '../core/cart/cart-recovery';
|
|
|
44
46
|
* ```
|
|
45
47
|
*/
|
|
46
48
|
export declare function createBrowserCartCookieStore(): CartCookieStore;
|
|
47
|
-
/**
|
|
48
|
-
* Get preferred currency from cookie (async — works with Next.js cookies()).
|
|
49
|
-
* Falls back to document.cookie on client.
|
|
50
|
-
*/
|
|
51
|
-
export declare function getCurrencyFromCookieAsync(): Promise<string | null>;
|
|
52
|
-
/**
|
|
53
|
-
* Get cart ID from cookie (async — works with Next.js cookies()).
|
|
54
|
-
* Falls back to document.cookie on client.
|
|
55
|
-
*
|
|
56
|
-
* Use in Server Components for SSR cart badge:
|
|
57
|
-
* ```typescript
|
|
58
|
-
* const cartId = await getCartIdFromCookieAsync();
|
|
59
|
-
* if (cartId) {
|
|
60
|
-
* const cart = await fetchCart(cartId);
|
|
61
|
-
* // Render cart badge with real totalQuantity — no skeleton needed
|
|
62
|
-
* }
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
export declare function getCartIdFromCookieAsync(): Promise<string | null>;
|
|
66
49
|
//# sourceMappingURL=cookies.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/react/cookies.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/react/cookies.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIrD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACpF,IAAI,CAON;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAM,GAAG,IAAI,CAG3D;AAGD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,4BAA4B,IAAI,eAAe,CAQ9D"}
|
package/dist/react/cookies.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Client-side cookie utilities for SDK consumers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Synchronous read/write over `document.cookie`, SSR-safe via a `typeof document`
|
|
5
|
+
* guard (return null / no-op when there is no document). To read these cookies in
|
|
6
|
+
* a Next.js Server Component use the `@doswiftly/storefront-sdk/react/server`
|
|
7
|
+
* entry (`readCartIdCookie`, `readCurrencyCookie`).
|
|
6
8
|
*/
|
|
7
9
|
/**
|
|
8
10
|
* Get cookie value by name (client-side only).
|
|
@@ -35,7 +37,6 @@ export function deleteCookie(name, path = '/') {
|
|
|
35
37
|
return;
|
|
36
38
|
document.cookie = `${name}=;max-age=0;path=${path}`;
|
|
37
39
|
}
|
|
38
|
-
import { CURRENCY_COOKIE_NAME } from '../core/currency/cookie-config';
|
|
39
40
|
import { CART_COOKIE_NAME, CART_COOKIE_MAX_AGE } from '../core/cart/cookie-config';
|
|
40
41
|
/**
|
|
41
42
|
* Build a browser-side `CartCookieStore` backed by `document.cookie`.
|
|
@@ -65,46 +66,3 @@ export function createBrowserCartCookieStore() {
|
|
|
65
66
|
clear: () => deleteCookie(CART_COOKIE_NAME),
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Get preferred currency from cookie (async — works with Next.js cookies()).
|
|
70
|
-
* Falls back to document.cookie on client.
|
|
71
|
-
*/
|
|
72
|
-
export async function getCurrencyFromCookieAsync() {
|
|
73
|
-
// Server-side: try Next.js cookies()
|
|
74
|
-
try {
|
|
75
|
-
const { cookies } = await import('next/headers');
|
|
76
|
-
const cookieStore = await cookies();
|
|
77
|
-
return cookieStore.get(CURRENCY_COOKIE_NAME)?.value ?? null;
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
// Not in a server context or next not available
|
|
81
|
-
}
|
|
82
|
-
// Client-side fallback
|
|
83
|
-
return getCookie(CURRENCY_COOKIE_NAME);
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Get cart ID from cookie (async — works with Next.js cookies()).
|
|
87
|
-
* Falls back to document.cookie on client.
|
|
88
|
-
*
|
|
89
|
-
* Use in Server Components for SSR cart badge:
|
|
90
|
-
* ```typescript
|
|
91
|
-
* const cartId = await getCartIdFromCookieAsync();
|
|
92
|
-
* if (cartId) {
|
|
93
|
-
* const cart = await fetchCart(cartId);
|
|
94
|
-
* // Render cart badge with real totalQuantity — no skeleton needed
|
|
95
|
-
* }
|
|
96
|
-
* ```
|
|
97
|
-
*/
|
|
98
|
-
export async function getCartIdFromCookieAsync() {
|
|
99
|
-
// Server-side: try Next.js cookies()
|
|
100
|
-
try {
|
|
101
|
-
const { cookies } = await import('next/headers');
|
|
102
|
-
const cookieStore = await cookies();
|
|
103
|
-
return cookieStore.get(CART_COOKIE_NAME)?.value ?? null;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
// Not in a server context or next not available
|
|
107
|
-
}
|
|
108
|
-
// Client-side fallback
|
|
109
|
-
return getCookie(CART_COOKIE_NAME);
|
|
110
|
-
}
|
|
@@ -119,11 +119,40 @@ export type CartManagerStatus = {
|
|
|
119
119
|
type: 'success';
|
|
120
120
|
operation: CartManagerOperation;
|
|
121
121
|
};
|
|
122
|
+
/**
|
|
123
|
+
* Optional lifecycle callbacks fired around every cart and checkout operation
|
|
124
|
+
* the hook drives. Centralise cross-cutting side-effects — a global loading
|
|
125
|
+
* indicator, a toast on failure, a router refresh on success — in one place
|
|
126
|
+
* instead of wrapping each call site.
|
|
127
|
+
*
|
|
128
|
+
* `onMutationError` fires only for failures you can surface to the buyer: its
|
|
129
|
+
* `error` carries a backend-translated message. Cart expiry and session loss
|
|
130
|
+
* are delivered through their own dedicated channels (`onExpired` and the
|
|
131
|
+
* session-expired event) — they carry SDK-internal messages and do NOT trigger
|
|
132
|
+
* `onMutationError`. A transient missing-cart error that the manager recovers
|
|
133
|
+
* from (by recreating the cart) resolves as success, so it never reaches the
|
|
134
|
+
* error callback either.
|
|
135
|
+
*
|
|
136
|
+
* Callbacks are invoked defensively: a throwing callback never rejects the
|
|
137
|
+
* underlying mutation.
|
|
138
|
+
*/
|
|
139
|
+
export interface CartManagerLifecycleCallbacks {
|
|
140
|
+
/** Fired when an operation starts, before the request is sent. */
|
|
141
|
+
onMutationStart?: (operation: CartManagerOperation) => void;
|
|
142
|
+
/** Fired after an operation resolves successfully. */
|
|
143
|
+
onMutationSuccess?: (operation: CartManagerOperation) => void;
|
|
144
|
+
/**
|
|
145
|
+
* Fired when an operation fails with a buyer-surfaceable error. Cart expiry
|
|
146
|
+
* and session loss are routed to `onExpired` / the session-expired event
|
|
147
|
+
* instead, not here.
|
|
148
|
+
*/
|
|
149
|
+
onMutationError?: (operation: CartManagerOperation, error: Error) => void;
|
|
150
|
+
}
|
|
122
151
|
/**
|
|
123
152
|
* Optional configuration for `useCartManager`. All fields additive — calling
|
|
124
153
|
* the hook with no arguments preserves the original cookie-driven behaviour.
|
|
125
154
|
*/
|
|
126
|
-
export interface UseCartManagerOptions {
|
|
155
|
+
export interface UseCartManagerOptions extends CartManagerLifecycleCallbacks {
|
|
127
156
|
/**
|
|
128
157
|
* Server-known cart-id seed used when the `cart-id` cookie is empty on
|
|
129
158
|
* mount. Cookie wins when present; the seed only fills the gap on the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-cart-manager.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-cart-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AAMH,OAAO,KAAK,EACV,IAAI,EACJ,aAAa,EACb,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,6BAA6B,EAC7B,4BAA4B,EAC5B,8BAA8B,EAC9B,sBAAsB,EACtB,uBAAuB,EACvB,gCAAgC,EAChC,kBAAkB,EAClB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAC5F,OAAO,
|
|
1
|
+
{"version":3,"file":"use-cart-manager.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-cart-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -86,15 +86,48 @@
|
|
|
86
86
|
* ```
|
|
87
87
|
*/
|
|
88
88
|
'use client';
|
|
89
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
89
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
90
90
|
import { useStorefrontClientContext } from '../providers/storefront-client-provider';
|
|
91
|
-
import { createCartRecoveryRunner, recreateWithInput, } from '../../core/cart/cart-recovery';
|
|
91
|
+
import { createCartRecoveryRunner, recreateWithInput, CartRecoveryNotPossibleError, CartSessionRequiredError, } from '../../core/cart/cart-recovery';
|
|
92
92
|
import { createBrowserCartCookieStore } from '../cookies';
|
|
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
|
+
}
|
|
93
117
|
export function useCartManager(options) {
|
|
94
118
|
const { cartClient } = useStorefrontClientContext();
|
|
95
119
|
const [status, setStatus] = useState({ type: 'idle' });
|
|
96
120
|
// Cookie store is stateless — keep one instance per hook mount.
|
|
97
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
|
+
};
|
|
98
131
|
// Normalize seed so the runner identity is stable across `undefined` /
|
|
99
132
|
// `null` props and only flips when the storefront actually swaps the seed.
|
|
100
133
|
const initialCartId = options?.initialCartId ?? null;
|
|
@@ -113,14 +146,19 @@ export function useCartManager(options) {
|
|
|
113
146
|
// recovery semantics to the runner (which fires onExpired separately).
|
|
114
147
|
const wrapMutation = useCallback(async (operation, run, failureFallbackMessage) => {
|
|
115
148
|
setStatus({ type: 'loading', operation });
|
|
149
|
+
safeInvoke(() => lifecycleRef.current.onMutationStart?.(operation));
|
|
116
150
|
try {
|
|
117
151
|
const result = await run();
|
|
118
152
|
setStatus({ type: 'success', operation });
|
|
153
|
+
safeInvoke(() => lifecycleRef.current.onMutationSuccess?.(operation));
|
|
119
154
|
return result;
|
|
120
155
|
}
|
|
121
156
|
catch (err) {
|
|
122
157
|
const error = err instanceof Error ? err : new Error(failureFallbackMessage);
|
|
123
158
|
setStatus({ type: 'error', operation, error });
|
|
159
|
+
if (!isDedicatedChannelError(error)) {
|
|
160
|
+
safeInvoke(() => lifecycleRef.current.onMutationError?.(operation, error));
|
|
161
|
+
}
|
|
124
162
|
throw err;
|
|
125
163
|
}
|
|
126
164
|
}, []);
|
|
@@ -206,6 +244,7 @@ export function useCartManager(options) {
|
|
|
206
244
|
*/
|
|
207
245
|
const complete = useCallback(async (input) => {
|
|
208
246
|
setStatus({ type: 'loading', operation: 'complete' });
|
|
247
|
+
safeInvoke(() => lifecycleRef.current.onMutationStart?.('complete'));
|
|
209
248
|
try {
|
|
210
249
|
const result = await runner.execute({
|
|
211
250
|
name: 'complete',
|
|
@@ -213,11 +252,15 @@ export function useCartManager(options) {
|
|
|
213
252
|
});
|
|
214
253
|
cookieStore.clear();
|
|
215
254
|
setStatus({ type: 'idle' });
|
|
255
|
+
safeInvoke(() => lifecycleRef.current.onMutationSuccess?.('complete'));
|
|
216
256
|
return result;
|
|
217
257
|
}
|
|
218
258
|
catch (err) {
|
|
219
259
|
const error = err instanceof Error ? err : new Error('Failed to complete cart');
|
|
220
260
|
setStatus({ type: 'error', operation: 'complete', error });
|
|
261
|
+
if (!isDedicatedChannelError(error)) {
|
|
262
|
+
safeInvoke(() => lifecycleRef.current.onMutationError?.('complete', error));
|
|
263
|
+
}
|
|
221
264
|
throw err;
|
|
222
265
|
}
|
|
223
266
|
}, [runner, cartClient, cookieStore]);
|
|
@@ -237,7 +280,11 @@ export function useCartManager(options) {
|
|
|
237
280
|
// Backward-compatible derived selectors over the tagged-union status.
|
|
238
281
|
const isLoading = status.type === 'loading';
|
|
239
282
|
const error = status.type === 'error' ? status.error.message : null;
|
|
240
|
-
|
|
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(() => ({
|
|
241
288
|
getCart,
|
|
242
289
|
getCartId,
|
|
243
290
|
addItem,
|
|
@@ -262,5 +309,30 @@ export function useCartManager(options) {
|
|
|
262
309
|
status,
|
|
263
310
|
isLoading,
|
|
264
311
|
error,
|
|
265
|
-
}
|
|
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
|
+
]);
|
|
266
338
|
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ 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';
|
|
@@ -22,7 +23,7 @@ export { useRefreshToken, type UseRefreshTokenOptions, type UseRefreshTokenRetur
|
|
|
22
23
|
export { useSessionRefresh, type UseSessionRefreshOptions } from './hooks/use-session-refresh';
|
|
23
24
|
export { useSessionExpired } from './hooks/use-session-expired';
|
|
24
25
|
export type { SessionExpiredEvent, SessionExpiredReason, SessionExpiredEmitter } from '../core/auth/session-events';
|
|
25
|
-
export { useCartManager, type CartManagerOperation, type CartManagerStatus, type UseCartManagerResult } from './hooks/use-cart-manager';
|
|
26
|
+
export { useCartManager, type CartManagerOperation, type CartManagerStatus, type UseCartManagerResult, type UseCartManagerOptions, type CartManagerLifecycleCallbacks } from './hooks/use-cart-manager';
|
|
26
27
|
export { useCart, type UseCartOptions, type UseCartResult, type ServerCartOperation } from './hooks/use-cart';
|
|
27
28
|
export { useStorefrontClient } from './hooks/use-storefront-client';
|
|
28
29
|
export { useCurrency } from './hooks/use-currency';
|
|
@@ -35,7 +36,7 @@ export type { LanguageStore } from './stores/language.store';
|
|
|
35
36
|
export type { ShopConfig } from './types/shop-config';
|
|
36
37
|
export { selectCurrency, selectBaseCurrency, selectSupportedCurrencies, selectIsLoaded } from './stores/currency.store';
|
|
37
38
|
export { selectLanguage, selectDefaultLanguage, selectSupportedLanguages, selectLanguageIsLoaded } from './stores/language.store';
|
|
38
|
-
export { getCookie, setCookie, deleteCookie,
|
|
39
|
+
export { getCookie, setCookie, deleteCookie, createBrowserCartCookieStore, } from './cookies';
|
|
39
40
|
export { useBotProtection } from './hooks/use-bot-protection';
|
|
40
41
|
export { useHydrated } from './hooks/use-hydrated';
|
|
41
42
|
export { useDebouncedValue } from './hooks/use-debounced-value';
|
|
@@ -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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"}
|
package/dist/react/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export { StorefrontProvider } from './providers/storefront-provider';
|
|
|
16
16
|
export { StorefrontClientProvider } from './providers/storefront-client-provider';
|
|
17
17
|
export { CurrencyProvider } from './providers/currency-provider';
|
|
18
18
|
export { LanguageProvider } from './providers/language-provider';
|
|
19
|
+
export { CartManagerProvider, useCartManagerContext } from './providers/cart-manager-provider';
|
|
19
20
|
// Hooks
|
|
20
21
|
export { useAuth } from './hooks/use-auth';
|
|
21
22
|
export { useLogin } from './hooks/use-login';
|
|
@@ -34,8 +35,9 @@ export { useLanguageStore, useLanguageStoreApi } from './stores/store-context';
|
|
|
34
35
|
// Selectors
|
|
35
36
|
export { selectCurrency, selectBaseCurrency, selectSupportedCurrencies, selectIsLoaded } from './stores/currency.store';
|
|
36
37
|
export { selectLanguage, selectDefaultLanguage, selectSupportedLanguages, selectLanguageIsLoaded } from './stores/language.store';
|
|
37
|
-
// Cookie utilities
|
|
38
|
-
|
|
38
|
+
// Cookie utilities (client-side). Server-side readers (readCartIdCookie,
|
|
39
|
+
// readCurrencyCookie) live in `@doswiftly/storefront-sdk/react/server`.
|
|
40
|
+
export { getCookie, setCookie, deleteCookie, createBrowserCartCookieStore, } from './cookies';
|
|
39
41
|
// Bot protection
|
|
40
42
|
export { useBotProtection } from './hooks/use-bot-protection';
|
|
41
43
|
// Generic hooks
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CartManagerProvider — shares a single `useCartManager` instance across the tree.
|
|
3
|
+
*
|
|
4
|
+
* `useCartManager` owns per-mount state (status, recovery coordinator, cart-expired
|
|
5
|
+
* listeners), so calling it in several components creates independent managers
|
|
6
|
+
* with separate loading state and separate recovery queues. Wrap the checkout
|
|
7
|
+
* subtree in `<CartManagerProvider>` and read the shared instance with
|
|
8
|
+
* `useCartManagerContext()` to get one source of truth: one loading indicator,
|
|
9
|
+
* one recovery coordinator, one cart-expired subscription.
|
|
10
|
+
*
|
|
11
|
+
* Must be rendered inside `<StorefrontProvider>` — it builds on the storefront
|
|
12
|
+
* client from context. For deliberately independent managers (e.g. a multi-cart
|
|
13
|
+
* admin view) call `useCartManager()` directly instead of using this provider.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* // app/checkout/page.tsx — Server Component resolves the cart-id
|
|
18
|
+
* <CartManagerProvider
|
|
19
|
+
* initialCartId={cartId}
|
|
20
|
+
* onMutationError={(operation, error) => toast.error(error.message)}
|
|
21
|
+
* onMutationSuccess={() => router.refresh()}
|
|
22
|
+
* >
|
|
23
|
+
* <CheckoutForm />
|
|
24
|
+
* </CartManagerProvider>
|
|
25
|
+
*
|
|
26
|
+
* // CheckoutForm.tsx
|
|
27
|
+
* const { addItem, complete, status } = useCartManagerContext();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
import { type ReactNode } from 'react';
|
|
31
|
+
import { type CartManagerLifecycleCallbacks, type UseCartManagerResult } from '../hooks/use-cart-manager';
|
|
32
|
+
export interface CartManagerProviderProps extends CartManagerLifecycleCallbacks {
|
|
33
|
+
/**
|
|
34
|
+
* Server-known cart-id seed forwarded to `useCartManager`. Used only when the
|
|
35
|
+
* `cart-id` cookie is empty on mount — the cookie always wins when present.
|
|
36
|
+
*/
|
|
37
|
+
initialCartId?: string | null;
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates one `useCartManager` instance and exposes it to descendants via
|
|
42
|
+
* Context. Lifecycle callbacks are forwarded to the underlying hook.
|
|
43
|
+
*/
|
|
44
|
+
export declare function CartManagerProvider({ initialCartId, onMutationStart, onMutationSuccess, onMutationError, children, }: CartManagerProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
45
|
+
/**
|
|
46
|
+
* Read the shared cart manager provided by the nearest `<CartManagerProvider>`.
|
|
47
|
+
* Throws when called outside a provider.
|
|
48
|
+
*/
|
|
49
|
+
export declare function useCartManagerContext(): UseCartManagerResult;
|
|
50
|
+
//# sourceMappingURL=cart-manager-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-manager-provider.d.ts","sourceRoot":"","sources":["../../../src/react/providers/cart-manager-provider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,EAEL,KAAK,6BAA6B,EAClC,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAKnC,MAAM,WAAW,wBAAyB,SAAQ,6BAA6B;IAC7E;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,QAAQ,GACT,EAAE,wBAAwB,2CAS1B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,oBAAoB,CAM5D"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CartManagerProvider — shares a single `useCartManager` instance across the tree.
|
|
3
|
+
*
|
|
4
|
+
* `useCartManager` owns per-mount state (status, recovery coordinator, cart-expired
|
|
5
|
+
* listeners), so calling it in several components creates independent managers
|
|
6
|
+
* with separate loading state and separate recovery queues. Wrap the checkout
|
|
7
|
+
* subtree in `<CartManagerProvider>` and read the shared instance with
|
|
8
|
+
* `useCartManagerContext()` to get one source of truth: one loading indicator,
|
|
9
|
+
* one recovery coordinator, one cart-expired subscription.
|
|
10
|
+
*
|
|
11
|
+
* Must be rendered inside `<StorefrontProvider>` — it builds on the storefront
|
|
12
|
+
* client from context. For deliberately independent managers (e.g. a multi-cart
|
|
13
|
+
* admin view) call `useCartManager()` directly instead of using this provider.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* // app/checkout/page.tsx — Server Component resolves the cart-id
|
|
18
|
+
* <CartManagerProvider
|
|
19
|
+
* initialCartId={cartId}
|
|
20
|
+
* onMutationError={(operation, error) => toast.error(error.message)}
|
|
21
|
+
* onMutationSuccess={() => router.refresh()}
|
|
22
|
+
* >
|
|
23
|
+
* <CheckoutForm />
|
|
24
|
+
* </CartManagerProvider>
|
|
25
|
+
*
|
|
26
|
+
* // CheckoutForm.tsx
|
|
27
|
+
* const { addItem, complete, status } = useCartManagerContext();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
'use client';
|
|
31
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
32
|
+
import { createContext, useContext } from 'react';
|
|
33
|
+
import { useCartManager, } from '../hooks/use-cart-manager';
|
|
34
|
+
const CartManagerContext = createContext(null);
|
|
35
|
+
CartManagerContext.displayName = 'CartManagerContext';
|
|
36
|
+
/**
|
|
37
|
+
* Creates one `useCartManager` instance and exposes it to descendants via
|
|
38
|
+
* Context. Lifecycle callbacks are forwarded to the underlying hook.
|
|
39
|
+
*/
|
|
40
|
+
export function CartManagerProvider({ initialCartId, onMutationStart, onMutationSuccess, onMutationError, children, }) {
|
|
41
|
+
const manager = useCartManager({
|
|
42
|
+
initialCartId,
|
|
43
|
+
onMutationStart,
|
|
44
|
+
onMutationSuccess,
|
|
45
|
+
onMutationError,
|
|
46
|
+
});
|
|
47
|
+
return _jsx(CartManagerContext.Provider, { value: manager, children: children });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read the shared cart manager provided by the nearest `<CartManagerProvider>`.
|
|
51
|
+
* Throws when called outside a provider.
|
|
52
|
+
*/
|
|
53
|
+
export function useCartManagerContext() {
|
|
54
|
+
const ctx = useContext(CartManagerContext);
|
|
55
|
+
if (!ctx) {
|
|
56
|
+
throw new Error('useCartManagerContext must be used within CartManagerProvider');
|
|
57
|
+
}
|
|
58
|
+
return ctx;
|
|
59
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-first readers for the storefront's readable first-party cookies.
|
|
3
|
+
*
|
|
4
|
+
* `readCartIdCookie` / `readCurrencyCookie` return the `cart-id` /
|
|
5
|
+
* `preferred-currency` cookie value. They run server-first: in a Next.js Server
|
|
6
|
+
* Component they read the request cookies via `next/headers`; when that is
|
|
7
|
+
* unavailable (client bundle, build time, non-Next runtime) they fall back to
|
|
8
|
+
* `document.cookie`. This isomorphic behaviour is deliberate and safe here
|
|
9
|
+
* because both cookies are readable (NOT httpOnly) — unlike the auth token,
|
|
10
|
+
* which `getInitialAuth` reads server-only precisely because it is httpOnly.
|
|
11
|
+
*
|
|
12
|
+
* They live under `@doswiftly/storefront-sdk/react/server` so the `next/headers`
|
|
13
|
+
* dependency stays out of the client entry; the dynamic import keeps importing
|
|
14
|
+
* this module safe in runtimes without `next/headers`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* // app/checkout/page.tsx (Server Component)
|
|
19
|
+
* import { readCartIdCookie } from '@doswiftly/storefront-sdk/react/server';
|
|
20
|
+
*
|
|
21
|
+
* const cartId = await readCartIdCookie();
|
|
22
|
+
* const cart = cartId ? await fetchCart(cartId) : null;
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
/** Read the first-party `cart-id` cookie (server-first, client fallback). */
|
|
26
|
+
export declare function readCartIdCookie(): Promise<string | null>;
|
|
27
|
+
/** Read the first-party `preferred-currency` cookie (server-first, client fallback). */
|
|
28
|
+
export declare function readCurrencyCookie(): Promise<string | null>;
|
|
29
|
+
//# sourceMappingURL=cookie-readers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie-readers.d.ts","sourceRoot":"","sources":["../../../src/react/server/cookie-readers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAuBH,6EAA6E;AAC7E,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE/D;AAED,wFAAwF;AACxF,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEjE"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-first readers for the storefront's readable first-party cookies.
|
|
3
|
+
*
|
|
4
|
+
* `readCartIdCookie` / `readCurrencyCookie` return the `cart-id` /
|
|
5
|
+
* `preferred-currency` cookie value. They run server-first: in a Next.js Server
|
|
6
|
+
* Component they read the request cookies via `next/headers`; when that is
|
|
7
|
+
* unavailable (client bundle, build time, non-Next runtime) they fall back to
|
|
8
|
+
* `document.cookie`. This isomorphic behaviour is deliberate and safe here
|
|
9
|
+
* because both cookies are readable (NOT httpOnly) — unlike the auth token,
|
|
10
|
+
* which `getInitialAuth` reads server-only precisely because it is httpOnly.
|
|
11
|
+
*
|
|
12
|
+
* They live under `@doswiftly/storefront-sdk/react/server` so the `next/headers`
|
|
13
|
+
* dependency stays out of the client entry; the dynamic import keeps importing
|
|
14
|
+
* this module safe in runtimes without `next/headers`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* // app/checkout/page.tsx (Server Component)
|
|
19
|
+
* import { readCartIdCookie } from '@doswiftly/storefront-sdk/react/server';
|
|
20
|
+
*
|
|
21
|
+
* const cartId = await readCartIdCookie();
|
|
22
|
+
* const cart = cartId ? await fetchCart(cartId) : null;
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { CART_COOKIE_NAME } from '../../core/cart/cookie-config';
|
|
26
|
+
import { CURRENCY_COOKIE_NAME } from '../../core/currency/cookie-config';
|
|
27
|
+
import { getCookie } from '../cookies';
|
|
28
|
+
/**
|
|
29
|
+
* Read a cookie server-first: the request cookies via `next/headers` in a Server
|
|
30
|
+
* Component, falling back to `document.cookie` on the client / outside a request
|
|
31
|
+
* scope. Returns null when the cookie is absent on both sides.
|
|
32
|
+
*/
|
|
33
|
+
async function readCookieIsomorphic(name) {
|
|
34
|
+
try {
|
|
35
|
+
const { cookies } = await import('next/headers');
|
|
36
|
+
const store = await cookies();
|
|
37
|
+
return store.get(name)?.value ?? null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// No Next.js server request scope (client bundle, build time, non-Next
|
|
41
|
+
// runtime) — fall back to the client reader (null on the server).
|
|
42
|
+
return getCookie(name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Read the first-party `cart-id` cookie (server-first, client fallback). */
|
|
46
|
+
export async function readCartIdCookie() {
|
|
47
|
+
return readCookieIsomorphic(CART_COOKIE_NAME);
|
|
48
|
+
}
|
|
49
|
+
/** Read the first-party `preferred-currency` cookie (server-first, client fallback). */
|
|
50
|
+
export async function readCurrencyCookie() {
|
|
51
|
+
return readCookieIsomorphic(CURRENCY_COOKIE_NAME);
|
|
52
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { getStorefrontClient, type ServerClientOptions } from './get-storefront-client';
|
|
2
2
|
export { createStorefrontAuthRoute, type StorefrontAuthRouteOptions, type StorefrontAuthRouteHandlers, } from './create-storefront-auth-route';
|
|
3
3
|
export { getInitialAuth, type InitialAuth } from './get-initial-auth';
|
|
4
|
+
export { readCartIdCookie, readCurrencyCookie } from './cookie-readers';
|
|
4
5
|
export { trustedForwardedHostValidator, originAllowlistValidator, type OriginValidator, type OriginValidatorContext, } from '../../core/auth/handlers';
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAGxF,OAAO,EACL,yBAAyB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,2BAA2B,GACjC,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtE,OAAO,EACL,6BAA6B,EAC7B,wBAAwB,EACxB,KAAK,eAAe,EACpB,KAAK,sBAAsB,GAC5B,MAAM,0BAA0B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAGxF,OAAO,EACL,yBAAyB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,2BAA2B,GACjC,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAGxE,OAAO,EACL,6BAA6B,EAC7B,wBAAwB,EACxB,KAAK,eAAe,EACpB,KAAK,sBAAsB,GAC5B,MAAM,0BAA0B,CAAC"}
|
|
@@ -3,5 +3,7 @@ export { getStorefrontClient } from './get-storefront-client';
|
|
|
3
3
|
export { createStorefrontAuthRoute, } from './create-storefront-auth-route';
|
|
4
4
|
// Server-only cold-start auth seed from the first-party cookies.
|
|
5
5
|
export { getInitialAuth } from './get-initial-auth';
|
|
6
|
+
// Server-first readers for the readable first-party cookies (cart-id, currency).
|
|
7
|
+
export { readCartIdCookie, readCurrencyCookie } from './cookie-readers';
|
|
6
8
|
// Origin validators — wire CSRF defense-in-depth on the route handlers from a single import.
|
|
7
9
|
export { trustedForwardedHostValidator, originAllowlistValidator, } from '../../core/auth/handlers';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doswiftly/storefront-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "19.0.0",
|
|
4
4
|
"description": "Storefront runtime SDK for DoSwiftly Commerce — layered transport, middleware pipeline, React providers, Zustand stores, cache strategies. 0 runtime dependencies in core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|