@doswiftly/storefront-sdk 11.1.0 → 11.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +131 -0
- package/README.md +297 -2
- package/dist/core/auth/handlers.d.ts.map +1 -1
- package/dist/core/auth/handlers.js +29 -1
- package/dist/core/bot-protection/turnstile-manager.d.ts +0 -1
- package/dist/core/bot-protection/turnstile-manager.d.ts.map +1 -1
- package/dist/core/bot-protection/turnstile-manager.js +0 -1
- package/dist/core/cart/cart-recovery.d.ts +210 -0
- package/dist/core/cart/cart-recovery.d.ts.map +1 -0
- package/dist/core/cart/cart-recovery.js +271 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/react/components/AddToCartButton.d.ts +49 -0
- package/dist/react/components/AddToCartButton.d.ts.map +1 -0
- package/dist/react/components/AddToCartButton.js +47 -0
- package/dist/react/components/CartCount.d.ts +35 -0
- package/dist/react/components/CartCount.d.ts.map +1 -0
- package/dist/react/components/CartCount.js +23 -0
- package/dist/react/components/CartTotals.d.ts +54 -0
- package/dist/react/components/CartTotals.d.ts.map +1 -0
- package/dist/react/components/CartTotals.js +38 -0
- package/dist/react/components/Image.d.ts +42 -0
- package/dist/react/components/Image.d.ts.map +1 -0
- package/dist/react/components/Image.js +33 -0
- package/dist/react/components/Money.d.ts +32 -0
- package/dist/react/components/Money.d.ts.map +1 -0
- package/dist/react/components/Money.js +27 -0
- package/dist/react/components/PriceDisplay.d.ts +34 -0
- package/dist/react/components/PriceDisplay.d.ts.map +1 -0
- package/dist/react/components/PriceDisplay.js +21 -0
- package/dist/react/components/index.d.ts +15 -0
- package/dist/react/components/index.d.ts.map +1 -0
- package/dist/react/components/index.js +14 -0
- package/dist/react/cookies.d.ts +21 -0
- package/dist/react/cookies.d.ts.map +1 -1
- package/dist/react/cookies.js +29 -1
- package/dist/react/hooks/use-auth.d.ts +19 -46
- package/dist/react/hooks/use-auth.d.ts.map +1 -1
- package/dist/react/hooks/use-auth.js +24 -141
- package/dist/react/hooks/use-cart-manager.d.ts +75 -15
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +106 -194
- package/dist/react/hooks/use-login.d.ts +40 -0
- package/dist/react/hooks/use-login.d.ts.map +1 -0
- package/dist/react/hooks/use-login.js +75 -0
- package/dist/react/hooks/use-logout.d.ts +40 -0
- package/dist/react/hooks/use-logout.d.ts.map +1 -0
- package/dist/react/hooks/use-logout.js +50 -0
- package/dist/react/hooks/use-refresh-token.d.ts +40 -0
- package/dist/react/hooks/use-refresh-token.d.ts.map +1 -0
- package/dist/react/hooks/use-refresh-token.js +66 -0
- package/dist/react/index.d.ts +6 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -1
- package/dist/react/server/get-storefront-client.d.ts +15 -5
- package/dist/react/server/get-storefront-client.d.ts.map +1 -1
- package/dist/react/stores/cart.store.d.ts +57 -10
- package/dist/react/stores/cart.store.d.ts.map +1 -1
- package/dist/react/stores/cart.store.js +112 -21
- package/dist/react/stores/store-context.d.ts.map +1 -1
- package/dist/react/stores/store-context.js +0 -2
- package/package.json +11 -4
- package/dist/__tests__/unit/test-helpers.d.ts +0 -46
- package/dist/__tests__/unit/test-helpers.d.ts.map +0 -1
- package/dist/__tests__/unit/test-helpers.js +0 -72
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,136 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 11.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9ca724e: Removes the `./schema` subpath export from `@doswiftly/storefront-sdk` — it was a duplicate of what `@doswiftly/storefront-operations` already publishes.
|
|
8
|
+
|
|
9
|
+
**Why:**
|
|
10
|
+
|
|
11
|
+
`@doswiftly/storefront-operations` is a linked package — it is always installed alongside `@doswiftly/storefront-sdk`. It exposes the GraphQL SDL as a raw `schema.graphql` file via the `./schema.graphql` subpath export, which is the format every codegen tool and IDE GraphQL plugin natively consumes. Bundling the same SDL again as an ESM string literal in `@doswiftly/storefront-sdk/schema` added ~128 KB to the npm tarball without giving consumers a meaningful capability that wasn't already there.
|
|
12
|
+
|
|
13
|
+
**Migration:**
|
|
14
|
+
|
|
15
|
+
Switch from:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { schema } from "@doswiftly/storefront-sdk/schema";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
to a direct path reference in your codegen config (or `graphql-config` for your IDE):
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// codegen.ts
|
|
25
|
+
const config = {
|
|
26
|
+
schema: "node_modules/@doswiftly/storefront-operations/schema.graphql",
|
|
27
|
+
// …
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The schema content is byte-for-byte identical — your codegen output does not change.
|
|
32
|
+
|
|
33
|
+
**Internal:**
|
|
34
|
+
- Removed `scripts/build-schema-export.cjs` and the matching `pnpm build` chain step.
|
|
35
|
+
- `pnpm build` is now plain `tsc` again.
|
|
36
|
+
- Removed the `dist/schema.js` + `dist/schema.d.ts` build artifacts and the `schema-export.test.ts` unit test.
|
|
37
|
+
- `pnpm run doctor` no longer checks for the schema bundle.
|
|
38
|
+
|
|
39
|
+
`@doswiftly/storefront-operations` is bumped in lockstep (no code change — required because the packages are linked and ship together).
|
|
40
|
+
|
|
41
|
+
## 11.3.0
|
|
42
|
+
|
|
43
|
+
### Minor Changes
|
|
44
|
+
|
|
45
|
+
- 0a2dfd8: Phase 1 of the senior-level audit ships security hardening, dead-code cleanup, and a coverage gate.
|
|
46
|
+
|
|
47
|
+
**Security:**
|
|
48
|
+
- `createSetTokenHandler`, `createClearTokenHandler`, `createWhoamiHandler` now validate the `Origin` header against the `Host` header by parsed URL host (`new URL(origin).host === host`) instead of a substring `includes()` check. The previous implementation was bypassable by empty Host (`includes('')` is always true) and by Origin URLs that embedded the trusted host in their query string. Requests without both `Origin` and `Host` headers now respond with `403`.
|
|
49
|
+
|
|
50
|
+
**Removed:**
|
|
51
|
+
- `ServerClientOptions.getHeaders` (accepted by `getStorefrontClient()` but never wired through). Inject request-scoped headers via the `middleware` option — see updated JSDoc.
|
|
52
|
+
|
|
53
|
+
**Internal:**
|
|
54
|
+
- Inconsistent `setIsRenewingToken` setter in `useAuth` is renamed to `setIsRefreshingToken` to match the exposed `isRefreshingToken` flag.
|
|
55
|
+
- Removed an unused `callbackName` field in `TurnstileManager` and a pre-cookie-era `localStorage.removeItem('cart-storage')` call in `createCartStore`.
|
|
56
|
+
|
|
57
|
+
**Infrastructure:**
|
|
58
|
+
- `package.json` now declares `sideEffects: false` so consumer bundlers can tree-shake unused exports aggressively.
|
|
59
|
+
- Adds `test:coverage` script (`vitest run --coverage`) with a baseline threshold (60% statements / 50% branches / 55% functions / 60% lines). The floor will rise as later phases cover the React providers, auth/currency/language stores, and remaining hooks.
|
|
60
|
+
- Adds 102 new unit tests across `format`, `image`, `cookies`, `auth/handlers`, and additional `errorMiddleware`/`timeoutMiddleware`/`languageMiddleware` cases — including two regression tests for the origin-validation bypass vectors.
|
|
61
|
+
|
|
62
|
+
`@doswiftly/storefront-operations` is bumped in lockstep (no code change — required because the packages are linked and ship together).
|
|
63
|
+
|
|
64
|
+
- 09c3744: Phase 2 of the SDK audit ships the developer-experience features external storefront builders have been writing by hand: pre-built React components, tagged-union mutation state, a bundled GraphQL schema export, and a focused-hook auth API.
|
|
65
|
+
|
|
66
|
+
**Pre-built React components** (from `@doswiftly/storefront-sdk/react`):
|
|
67
|
+
|
|
68
|
+
Six headless, accessibility-aware components — zero styling, framework-agnostic, no `next/image` dependency so they work in any React 18/19 environment.
|
|
69
|
+
- `<Money amount currency>` — locale-formatted price from minor units (wraps `formatPrice`).
|
|
70
|
+
- `<Image data sizes priority>` — `<img>` with thumbhash blur placeholder, lazy by default, `fetchpriority` hint.
|
|
71
|
+
- `<CartCount count label hideWhenEmpty>` — aria-live cart item count.
|
|
72
|
+
- `<AddToCartButton variantId quantity onSuccess onError>` — orchestrates `useCartManager.addItem` with loading state + sr-only error alert.
|
|
73
|
+
- `<PriceDisplay price compareAtPrice currency>` — current price + optional strikethrough sale price.
|
|
74
|
+
- `<CartTotals subtotal discount shipping tax total currency labels>` — `<dl>` breakdown that elides empty rows.
|
|
75
|
+
|
|
76
|
+
**Tagged-union status in `useCartManager`**:
|
|
77
|
+
- New `status` field on the hook return — `{ type: 'idle' | 'loading' | 'error' | 'success', operation? }` — enables exhaustive switching without juggling booleans.
|
|
78
|
+
- `isLoading` and `error` remain available as derived selectors (backward-compatible).
|
|
79
|
+
- `clearCart()` resets status to `idle`.
|
|
80
|
+
|
|
81
|
+
**Bundled GraphQL schema** (`@doswiftly/storefront-sdk/schema`):
|
|
82
|
+
- New export path ships the full GraphQL SDL (123 KB) as a string constant, regenerated from `@doswiftly/storefront-operations/schema.graphql` on each build.
|
|
83
|
+
- Wire GraphQL codegen / IDE plugins without a live backend — no extra `npm install`.
|
|
84
|
+
|
|
85
|
+
**`useAuth` split into focused hooks**:
|
|
86
|
+
- `useLogin({ onSetToken })` — login flow only, `{ login, isLoggingIn, error }`.
|
|
87
|
+
- `useLogout({ onClearToken })` — logout, always clears local store even on backend failure.
|
|
88
|
+
- `useRefreshToken({ onSetToken })` — token rotation, keeps existing customer.
|
|
89
|
+
- `useAuth(options)` is preserved as a thin facade composing the three (backward-compatible, no breaking change).
|
|
90
|
+
|
|
91
|
+
Prefer the focused hooks in new code — smaller bundles, isolated state, smaller dependency arrays.
|
|
92
|
+
|
|
93
|
+
**Docs**:
|
|
94
|
+
- README adds a 7-step Next.js 16 App Router setup, BFF route handler examples, components catalog, bundled schema usage.
|
|
95
|
+
- New `docs/storefront-developer/sdk/nextjs-setup.md` and `auth.md` with end-to-end recipes.
|
|
96
|
+
|
|
97
|
+
**Tests**: 476 → 494 unit tests (+18). New files: `components.test.tsx` (37 cases), `schema-export.test.ts` (5 cases), `use-auth.test.tsx` (13 cases). Build clean, 0-deps invariant preserved.
|
|
98
|
+
|
|
99
|
+
`@doswiftly/storefront-operations` is bumped in lockstep (no code change — required because the packages are linked and ship together).
|
|
100
|
+
|
|
101
|
+
## 11.2.0
|
|
102
|
+
|
|
103
|
+
### Minor Changes
|
|
104
|
+
|
|
105
|
+
- 760f965: Added automatic stale-cart recovery to the core SDK so every consumer (React, Vue, mobile, CLI, SSR) gets the same protection against `CART_NOT_FOUND` / `ALREADY_COMPLETED` errors without re-implementing detection, cookie cleanup, and retry orchestration.
|
|
106
|
+
|
|
107
|
+
**What's new** (importable from `@doswiftly/storefront-sdk`):
|
|
108
|
+
- `isCartRecoverableError(err)` — type-safe predicate that inspects `err.userErrors[].code` (works across PL/EN/any backend locale, unlike message string matching).
|
|
109
|
+
- `CART_RECOVERABLE_ERROR_CODES` — readonly tuple of codes that warrant cart recreation; mirrored against the backend `CartErrorCode` enum by a drift test.
|
|
110
|
+
- `executeWithCartRecovery(opts)` — pure async orchestrator for any consumer.
|
|
111
|
+
- `createCartRecoveryRunner({ cartClient, cookieStore })` — DX-friendly factory that curries the dependencies, shares a Phase-0 concurrency mutex across operations, and exposes an `onExpired(listener)` subscription for global UI feedback.
|
|
112
|
+
- `CartCookieStore` — port interface (`get` / `set` / `clear`) the caller implements once per runtime. From `@doswiftly/storefront-sdk/react` we now also export `createBrowserCartCookieStore()` (SSR-safe).
|
|
113
|
+
- `CartRecoveryNotPossibleError` — thrown when an operation cannot be safely replayed (line-id / shipping-method references the dead cart). Carries `reason: 'state-dependent' | 'recreate-failed' | 'retry-also-failed'` for diagnostic UX.
|
|
114
|
+
- `recreateWithInput(input)` / `recreateWithLines(lines)` — sugar helpers for the most common atomic `cartCreate(payload)` recovery strategies.
|
|
115
|
+
|
|
116
|
+
**Per-operation taxonomy (decision is in the SDK, not in caller code):**
|
|
117
|
+
|
|
118
|
+
| Operation | Strategy |
|
|
119
|
+
| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
120
|
+
| `addItems`, `updateBuyerIdentity`, `setShippingAddress`, `updateDiscountCodes`, `updateNote` | Auto-replay via a single atomic `cartCreate(input)` — recovery is invisible to the caller |
|
|
121
|
+
| `updateItems`, `removeItems` | Bail with `CartRecoveryNotPossibleError` and emit a `cart-expired` event — replaying on a fresh empty cart would silently lose the user's intent |
|
|
122
|
+
|
|
123
|
+
**React updates:**
|
|
124
|
+
- `useCartManager` now uses the recovery runner internally. Detection is code-based (no more PL/EN regression — previous implementation matched `'cart not found'` in the error message, which never matched a Polish backend response). Recovery now covers all mutations exposed by the hook, not just `addItem`. New methods: `updateBuyerIdentity`, `setShippingAddress`. New API: `onExpired(listener)` returning an unsubscribe function.
|
|
125
|
+
- `createCartStore` now recovers `addToCart` automatically. New optional `CartActions.createCartWithLines(lines)` lets templates wire the atomic single-round-trip path; without it the store falls back to `createCart()` + `addLines()`. `updateQuantity` and `removeFromCart` bail with the `cart-expired` event when the cart is gone (previously the store kept its local id pointing at a dead cart). `CartStoreConfig.onExpired?(event)` lets the consumer subscribe.
|
|
126
|
+
|
|
127
|
+
**Migration:**
|
|
128
|
+
- No required code changes for existing React consumers — `useCartManager` and `createCartStore` keep their public surface.
|
|
129
|
+
- If you previously caught `StorefrontError` to detect stale carts via message string matching, switch to `isCartRecoverableError(err)` (code-based, locale-proof) or — preferably — subscribe to `runner.onExpired(...)` / `useCartManager().onExpired(...)` / `createCartStore({ onExpired })`.
|
|
130
|
+
- Non-React consumers should wrap their cart mutations in `createCartRecoveryRunner` and implement a `CartCookieStore` adapter for their environment (browser via `createBrowserCartCookieStore()`, server-side via `next/headers`, CLI via in-memory `Map`, mobile via AsyncStorage).
|
|
131
|
+
|
|
132
|
+
`@doswiftly/storefront-operations` is bumped in lockstep (no code change — required because the packages are linked and ship together).
|
|
133
|
+
|
|
3
134
|
## 11.1.0
|
|
4
135
|
|
|
5
136
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @doswiftly/storefront-sdk
|
|
2
2
|
|
|
3
|
-
Layered runtime SDK for DoSwiftly Commerce storefronts.
|
|
3
|
+
Layered runtime SDK for DoSwiftly Commerce storefronts. Framework-agnostic core + React adapter, **0 runtime dependencies** in core.
|
|
4
4
|
|
|
5
5
|
## Architecture
|
|
6
6
|
|
|
@@ -94,10 +94,222 @@ const { currency, setCurrency } = useCurrencyStore();
|
|
|
94
94
|
| Path | Description | Dependencies |
|
|
95
95
|
|------|-------------|-------------|
|
|
96
96
|
| `@doswiftly/storefront-sdk` | Core: transport, middleware, clients, errors, format, image types, sanitize, auth handlers, route matching | **0** |
|
|
97
|
-
| `@doswiftly/storefront-sdk/react` | Providers, hooks,
|
|
97
|
+
| `@doswiftly/storefront-sdk/react` | Providers, hooks, pre-built UI components, Zustand stores | react, zustand |
|
|
98
98
|
| `@doswiftly/storefront-sdk/react/server` | Server-side client factory | react |
|
|
99
99
|
| `@doswiftly/storefront-sdk/cache` | Cache strategy functions | **0** |
|
|
100
100
|
|
|
101
|
+
## Next.js 16 App Router — first add-to-cart in 15 minutes
|
|
102
|
+
|
|
103
|
+
End-to-end skeleton that gets you a working cart + auth flow without writing
|
|
104
|
+
the middleware pipeline by hand. Copy the files below into a fresh `app/`
|
|
105
|
+
directory and you're live.
|
|
106
|
+
|
|
107
|
+
**1. `app/layout.tsx`** — wrap the tree in `StorefrontProvider`:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { StorefrontProvider } from '@doswiftly/storefront-sdk/react';
|
|
111
|
+
import { getStorefrontClient } from '@doswiftly/storefront-sdk/react/server';
|
|
112
|
+
import { cookies } from 'next/headers';
|
|
113
|
+
import { AUTH_COOKIE_NAME } from '@doswiftly/storefront-sdk';
|
|
114
|
+
|
|
115
|
+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
116
|
+
const serverClient = getStorefrontClient({
|
|
117
|
+
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
|
|
118
|
+
shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
|
|
119
|
+
});
|
|
120
|
+
// `SHOP_BASICS_QUERY` is your own operation — generated by graphql-codegen
|
|
121
|
+
// from `@doswiftly/storefront-operations/schema.graphql`. It should request
|
|
122
|
+
// `shop.currency`, `shop.supportedCurrencies`, `shop.supportedLanguages`,
|
|
123
|
+
// `shop.botProtection` — whatever StorefrontProvider needs.
|
|
124
|
+
const { shop } = await serverClient.query(SHOP_BASICS_QUERY);
|
|
125
|
+
|
|
126
|
+
// Read the httpOnly auth cookie server-side so we can hydrate the auth store
|
|
127
|
+
// without a flash of "Sign In".
|
|
128
|
+
const cookieStore = await cookies();
|
|
129
|
+
const initialIsAuthenticated = Boolean(cookieStore.get(AUTH_COOKIE_NAME)?.value);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<html lang="pl">
|
|
133
|
+
<body>
|
|
134
|
+
<StorefrontProvider
|
|
135
|
+
config={{
|
|
136
|
+
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
|
|
137
|
+
shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
|
|
138
|
+
}}
|
|
139
|
+
shopData={shop}
|
|
140
|
+
initialIsAuthenticated={initialIsAuthenticated}
|
|
141
|
+
>
|
|
142
|
+
{children}
|
|
143
|
+
</StorefrontProvider>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**2. `app/api/auth/set-token/route.ts`** — BFF route that sets the httpOnly
|
|
151
|
+
cookie. The SDK ships factories so each file is two lines:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { createSetTokenHandler } from '@doswiftly/storefront-sdk';
|
|
155
|
+
export const POST = createSetTokenHandler();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**3. `app/api/auth/clear-token/route.ts`** — clears the cookie on logout:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { createClearTokenHandler } from '@doswiftly/storefront-sdk';
|
|
162
|
+
export const POST = createClearTokenHandler();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**4. `app/api/auth/whoami/route.ts`** — hydrates customer state from the
|
|
166
|
+
httpOnly cookie after a hard refresh (`accessToken` no longer persists in
|
|
167
|
+
localStorage — XSS hardening):
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { createWhoamiHandler } from '@doswiftly/storefront-sdk';
|
|
171
|
+
|
|
172
|
+
export const GET = createWhoamiHandler({
|
|
173
|
+
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
|
|
174
|
+
shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**5. Sign-in form** — uses the focused `useLogin` hook + wires the BFF
|
|
179
|
+
callback:
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
'use client';
|
|
183
|
+
import { useLogin } from '@doswiftly/storefront-sdk/react';
|
|
184
|
+
|
|
185
|
+
export function LoginForm() {
|
|
186
|
+
const { login, isLoggingIn, error } = useLogin({
|
|
187
|
+
onSetToken: async (token) => {
|
|
188
|
+
await fetch('/api/auth/set-token', {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: { 'Content-Type': 'application/json' },
|
|
191
|
+
body: JSON.stringify({ token }),
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<form onSubmit={async (e) => {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
const data = new FormData(e.currentTarget);
|
|
200
|
+
const result = await login(String(data.get('email')), String(data.get('password')));
|
|
201
|
+
if (result.success) location.href = '/account';
|
|
202
|
+
}}>
|
|
203
|
+
<input name="email" type="email" required />
|
|
204
|
+
<input name="password" type="password" required />
|
|
205
|
+
<button type="submit" disabled={isLoggingIn}>{isLoggingIn ? 'Signing in…' : 'Sign in'}</button>
|
|
206
|
+
{error && <p role="alert">{error}</p>}
|
|
207
|
+
</form>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**6. Product card with add-to-cart** — pre-built components:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
'use client';
|
|
216
|
+
import { Image, PriceDisplay, AddToCartButton } from '@doswiftly/storefront-sdk/react';
|
|
217
|
+
|
|
218
|
+
export function ProductCard({ product }: { product: ProductCardFields }) {
|
|
219
|
+
return (
|
|
220
|
+
<article>
|
|
221
|
+
<Image data={product.featuredImage} sizes="(max-width: 768px) 100vw, 33vw" />
|
|
222
|
+
<h2>{product.title}</h2>
|
|
223
|
+
<PriceDisplay
|
|
224
|
+
price={product.price.amount * 100}
|
|
225
|
+
compareAtPrice={product.compareAtPrice ? product.compareAtPrice.amount * 100 : undefined}
|
|
226
|
+
currency={product.price.currencyCode}
|
|
227
|
+
/>
|
|
228
|
+
<AddToCartButton variantId={product.firstVariant.id}>Add to cart</AddToCartButton>
|
|
229
|
+
</article>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
That's it. Cart recovery (stale-cart auto-replay) is automatic — wire a single
|
|
235
|
+
global `onExpired` toast subscriber once in `app/layout.tsx`:
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
'use client';
|
|
239
|
+
import { useEffect } from 'react';
|
|
240
|
+
import { useCartManager } from '@doswiftly/storefront-sdk/react';
|
|
241
|
+
import { toast } from 'sonner';
|
|
242
|
+
|
|
243
|
+
export function CartExpiredToast() {
|
|
244
|
+
const { onExpired } = useCartManager();
|
|
245
|
+
useEffect(() => onExpired((e) => {
|
|
246
|
+
toast.error(e.reason === 'state-dependent'
|
|
247
|
+
? 'Twój koszyk wygasł, dodaj produkty ponownie'
|
|
248
|
+
: 'Nie udało się odzyskać koszyka, spróbuj ponownie');
|
|
249
|
+
}), [onExpired]);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Pre-built React components
|
|
255
|
+
|
|
256
|
+
Headless, accessibility-aware, zero styling — pass `className` to integrate
|
|
257
|
+
with your CSS approach. Available from `@doswiftly/storefront-sdk/react`:
|
|
258
|
+
|
|
259
|
+
| Component | Purpose |
|
|
260
|
+
|-----------|---------|
|
|
261
|
+
| `<Money amount currency>` | Locale-formatted price string from minor units |
|
|
262
|
+
| `<Image data sizes priority>` | `<img>` with thumbhash blur placeholder + sane defaults |
|
|
263
|
+
| `<CartCount count label>` | Aria-live cart item count |
|
|
264
|
+
| `<AddToCartButton variantId quantity>` | Button wired to `useCartManager.addItem` |
|
|
265
|
+
| `<PriceDisplay price compareAtPrice currency>` | Price + optional strikethrough sale price |
|
|
266
|
+
| `<CartTotals subtotal tax shipping discount total currency>` | Cart financial breakdown `<dl>` |
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
import { Money, Image, CartCount, AddToCartButton, PriceDisplay, CartTotals } from '@doswiftly/storefront-sdk/react';
|
|
270
|
+
|
|
271
|
+
<Money amount={9990} currency="PLN" /> {/* "99,90 zł" */}
|
|
272
|
+
<PriceDisplay price={7990} compareAtPrice={9990} currency="PLN" />
|
|
273
|
+
<CartCount count={3} label="items" />
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## GraphQL schema for codegen
|
|
277
|
+
|
|
278
|
+
The GraphQL SDL ships in the linked `@doswiftly/storefront-operations` package
|
|
279
|
+
(which is installed alongside the SDK) — point your codegen / IDE plugin at it
|
|
280
|
+
directly. No live backend required:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
// codegen.ts — uses raw .graphql file from the operations package
|
|
284
|
+
const config = {
|
|
285
|
+
schema: 'node_modules/@doswiftly/storefront-operations/schema.graphql',
|
|
286
|
+
documents: ['./app/**/*.{ts,tsx,graphql}'],
|
|
287
|
+
generates: { './generated/graphql.ts': { /* … */ } },
|
|
288
|
+
};
|
|
289
|
+
export default config;
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
For VS Code GraphQL extension, point `graphql-config` at the same path.
|
|
293
|
+
|
|
294
|
+
## Auth: focused hooks vs the facade
|
|
295
|
+
|
|
296
|
+
Prefer the per-flow hooks in new code — smaller bundles, isolated state:
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
import { useLogin, useLogout, useRefreshToken } from '@doswiftly/storefront-sdk/react';
|
|
300
|
+
|
|
301
|
+
const { login, isLoggingIn } = useLogin({ onSetToken });
|
|
302
|
+
const { logout, isLoggingOut } = useLogout({ onClearToken });
|
|
303
|
+
const { refreshToken } = useRefreshToken({ onSetToken });
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The legacy `useAuth` facade still works (combines all three):
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
import { useAuth } from '@doswiftly/storefront-sdk/react';
|
|
310
|
+
const { login, logout, refreshToken, isLoading, error } = useAuth({ onSetToken, onClearToken });
|
|
311
|
+
```
|
|
312
|
+
|
|
101
313
|
## Core API
|
|
102
314
|
|
|
103
315
|
### createStorefrontClient
|
|
@@ -158,6 +370,89 @@ const existing = await cartClient.get(cartId);
|
|
|
158
370
|
|
|
159
371
|
Auto-throws `StorefrontError` with code `USER_ERROR` on validation failures.
|
|
160
372
|
|
|
373
|
+
### Cart recovery (stale carts)
|
|
374
|
+
|
|
375
|
+
Carts can become unusable between reads and writes — they expire (TTL), lock after checkout (`ALREADY_COMPLETED`), or get cleaned up by a purge worker. Write mutations then reject with `userErrors[].code = 'CART_NOT_FOUND'` even though the previous `cart(id)` query succeeded.
|
|
376
|
+
|
|
377
|
+
The recovery utilities in core make this transparent for the caller. Every operation classifies itself: replay-safe operations (`addItems`, `updateBuyerIdentity`, `setShippingAddress`, discount codes, note) auto-recover via an atomic `cartCreate(input)`; state-dependent operations (`updateItems`, `removeItems`) bail with a `CartRecoveryNotPossibleError` and emit a `cart-expired` event so your UI can surface a single toast/banner instead of every call site handling the error.
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
import {
|
|
381
|
+
CartClient,
|
|
382
|
+
createCartRecoveryRunner,
|
|
383
|
+
recreateWithInput,
|
|
384
|
+
CartRecoveryNotPossibleError,
|
|
385
|
+
type CartCookieStore,
|
|
386
|
+
} from '@doswiftly/storefront-sdk';
|
|
387
|
+
|
|
388
|
+
// Implement the cookie port for your runtime (or use createBrowserCartCookieStore from /react)
|
|
389
|
+
const cookieStore: CartCookieStore = {
|
|
390
|
+
get: () => readCartCookie(),
|
|
391
|
+
set: (cartId) => writeCartCookie(cartId),
|
|
392
|
+
clear: () => deleteCartCookie(),
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const cartClient = new CartClient(client);
|
|
396
|
+
const runner = createCartRecoveryRunner({ cartClient, cookieStore });
|
|
397
|
+
|
|
398
|
+
runner.onExpired((event) => {
|
|
399
|
+
// Fired when the runner cannot transparently recover. UI prompts user to retry.
|
|
400
|
+
console.warn(`Cart expired (${event.reason}). Reset local state.`);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Auto-replay: storefront caller never thinks about recovery
|
|
404
|
+
const { cart, warnings } = await runner.execute({
|
|
405
|
+
name: 'addItems',
|
|
406
|
+
run: (cartId) => cartClient.addItems(cartId, [{ variantId: 'v-123', quantity: 1 }]),
|
|
407
|
+
recreateAndRun: recreateWithInput({ lines: [{ variantId: 'v-123', quantity: 1 }] }),
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Bail-on-stale: operation throws CartRecoveryNotPossibleError if cart is gone
|
|
411
|
+
try {
|
|
412
|
+
await runner.execute({
|
|
413
|
+
name: 'updateItem',
|
|
414
|
+
run: (cartId) => cartClient.updateItems(cartId, [{ id: 'line-1', quantity: 2 }]),
|
|
415
|
+
// No recreateAndRun — replaying on an empty cart would silently lose the user's intent.
|
|
416
|
+
});
|
|
417
|
+
} catch (err) {
|
|
418
|
+
if (err instanceof CartRecoveryNotPossibleError) {
|
|
419
|
+
// Already handled by the onExpired listener above.
|
|
420
|
+
} else {
|
|
421
|
+
throw err;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Detection inspects `err.userErrors[].code` (`CART_NOT_FOUND` / `ALREADY_COMPLETED`) — locale-independent.
|
|
427
|
+
|
|
428
|
+
### React: `useCartManager` (DX-first hook)
|
|
429
|
+
|
|
430
|
+
`useCartManager` is the React wrapper around the recovery runner — same per-operation taxonomy, just `useState`-driven loading/error and an `onExpired` subscription helper:
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
'use client';
|
|
434
|
+
import { useEffect } from 'react';
|
|
435
|
+
import { useCartManager } from '@doswiftly/storefront-sdk/react';
|
|
436
|
+
import { toast } from 'sonner';
|
|
437
|
+
|
|
438
|
+
export function CartButton() {
|
|
439
|
+
const { addItem, updateItem, onExpired, isLoading } = useCartManager();
|
|
440
|
+
|
|
441
|
+
useEffect(
|
|
442
|
+
() => onExpired((e) => toast.error(e.reason === 'state-dependent' ? 'Twój koszyk wygasł, dodaj produkty ponownie' : 'Nie udało się odzyskać koszyka')),
|
|
443
|
+
[onExpired],
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
return (
|
|
447
|
+
<button onClick={() => addItem([{ variantId: 'v-123', quantity: 1 }])} disabled={isLoading}>
|
|
448
|
+
Add to cart
|
|
449
|
+
</button>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Methods returning `CartMutationOutcome` are: `addItem`, `updateBuyerIdentity`, `setShippingAddress`, `updateDiscountCodes`, `updateNote` (auto-replay) and `updateItem`, `removeItem` (bail + `onExpired`). Use `clearCart` to reset locally without backend call.
|
|
455
|
+
|
|
161
456
|
### AuthClient
|
|
162
457
|
|
|
163
458
|
```typescript
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../../src/core/auth/handlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,UAAU,sBAAsB;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../../src/core/auth/handlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,UAAU,sBAAsB;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAoED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,CAAC,EAAE,sBAAsB,GACjC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAgEzC;AAED,UAAU,oBAAoB;IAC5B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,oBAAyB,GACjC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA6DzC;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAkCjF"}
|
|
@@ -27,15 +27,43 @@ function serializeCookie(name, value, options) {
|
|
|
27
27
|
parts.push('HttpOnly');
|
|
28
28
|
return parts.join('; ');
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Validate the Origin header against the Host header to prevent CSRF from
|
|
32
|
+
* unrelated origins.
|
|
33
|
+
*
|
|
34
|
+
* Compares the *parsed* origin's host with the request's host header, not a
|
|
35
|
+
* substring match — `origin.includes(host)` was bypassable two ways:
|
|
36
|
+
* - `host = ""` (or missing) → `origin.includes('')` always true;
|
|
37
|
+
* - `origin = "https://attacker.example/?host=trusted"` → substring match
|
|
38
|
+
* passes for any trusted host name.
|
|
39
|
+
*
|
|
40
|
+
* Both Origin and Host MUST be present (typical browser fetch sends Origin
|
|
41
|
+
* for cross-origin and same-origin POSTs; server-to-server callers without
|
|
42
|
+
* an Origin header should hit the GraphQL API directly, not these BFF
|
|
43
|
+
* handlers).
|
|
44
|
+
*/
|
|
30
45
|
function validateOrigin(request) {
|
|
31
46
|
const origin = request.headers.get('origin');
|
|
32
47
|
const host = request.headers.get('host');
|
|
33
|
-
if (origin
|
|
48
|
+
if (!origin || !host) {
|
|
49
|
+
return new Response(JSON.stringify({ error: 'Origin and Host headers are required' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
|
|
50
|
+
}
|
|
51
|
+
let originHost;
|
|
52
|
+
try {
|
|
53
|
+
originHost = new URL(origin).host;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
34
56
|
return new Response(JSON.stringify({ error: 'Invalid origin' }), {
|
|
35
57
|
status: 403,
|
|
36
58
|
headers: { 'Content-Type': 'application/json' },
|
|
37
59
|
});
|
|
38
60
|
}
|
|
61
|
+
if (originHost !== host) {
|
|
62
|
+
return new Response(JSON.stringify({ error: 'Origin mismatch' }), {
|
|
63
|
+
status: 403,
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
39
67
|
return null;
|
|
40
68
|
}
|
|
41
69
|
/**
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { AbstractBotProtectionManager } from './abstract-manager';
|
|
8
8
|
export declare class TurnstileManager extends AbstractBotProtectionManager {
|
|
9
|
-
private callbackName;
|
|
10
9
|
protected _waitForApi(): Promise<void>;
|
|
11
10
|
protected _renderWidget(container: HTMLElement, action?: string): string;
|
|
12
11
|
protected _executeChallenge(widgetId: string, action?: string): Promise<string | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turnstile-manager.d.ts","sourceRoot":"","sources":["../../../src/core/bot-protection/turnstile-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAElE,qBAAa,gBAAiB,SAAQ,4BAA4B;IAChE,
|
|
1
|
+
{"version":3,"file":"turnstile-manager.d.ts","sourceRoot":"","sources":["../../../src/core/bot-protection/turnstile-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAElE,qBAAa,gBAAiB,SAAQ,4BAA4B;IAChE,SAAS,CAAC,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBtC,SAAS,CAAC,aAAa,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM;IAaxE,SAAS,CAAC,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAuCtF,SAAS,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;CAKjD"}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
/// <reference path="./types/turnstile.d.ts" />
|
|
8
8
|
import { AbstractBotProtectionManager } from './abstract-manager';
|
|
9
9
|
export class TurnstileManager extends AbstractBotProtectionManager {
|
|
10
|
-
callbackName = `onloadTurnstileCallback_${Math.random().toString(36).slice(2)}`;
|
|
11
10
|
_waitForApi() {
|
|
12
11
|
if (typeof window !== 'undefined' && window.turnstile) {
|
|
13
12
|
return Promise.resolve();
|