@doswiftly/storefront-sdk 4.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.
Files changed (206) hide show
  1. package/README.md +430 -0
  2. package/dist/__tests__/unit/test-helpers.d.ts +46 -0
  3. package/dist/__tests__/unit/test-helpers.d.ts.map +1 -0
  4. package/dist/__tests__/unit/test-helpers.js +72 -0
  5. package/dist/core/auth/auth-client.d.ts +46 -0
  6. package/dist/core/auth/auth-client.d.ts.map +1 -0
  7. package/dist/core/auth/auth-client.js +82 -0
  8. package/dist/core/auth/cookie-config.d.ts +18 -0
  9. package/dist/core/auth/cookie-config.d.ts.map +1 -0
  10. package/dist/core/auth/cookie-config.js +18 -0
  11. package/dist/core/auth/handlers.d.ts +32 -0
  12. package/dist/core/auth/handlers.d.ts.map +1 -0
  13. package/dist/core/auth/handlers.js +127 -0
  14. package/dist/core/auth/routes.d.ts +21 -0
  15. package/dist/core/auth/routes.d.ts.map +1 -0
  16. package/dist/core/auth/routes.js +14 -0
  17. package/dist/core/auth/token-client.d.ts +26 -0
  18. package/dist/core/auth/token-client.d.ts.map +1 -0
  19. package/dist/core/auth/token-client.js +42 -0
  20. package/dist/core/auth/types.d.ts +53 -0
  21. package/dist/core/auth/types.d.ts.map +1 -0
  22. package/dist/core/auth/types.js +4 -0
  23. package/dist/core/cache.d.ts +54 -0
  24. package/dist/core/cache.d.ts.map +1 -0
  25. package/dist/core/cache.js +82 -0
  26. package/dist/core/cart/cart-client.d.ts +57 -0
  27. package/dist/core/cart/cart-client.d.ts.map +1 -0
  28. package/dist/core/cart/cart-client.js +89 -0
  29. package/dist/core/cart/types.d.ts +110 -0
  30. package/dist/core/cart/types.d.ts.map +1 -0
  31. package/dist/core/cart/types.js +6 -0
  32. package/dist/core/client/compose.d.ts +9 -0
  33. package/dist/core/client/compose.d.ts.map +1 -0
  34. package/dist/core/client/compose.js +9 -0
  35. package/dist/core/client/create-client.d.ts +15 -0
  36. package/dist/core/client/create-client.d.ts.map +1 -0
  37. package/dist/core/client/create-client.js +85 -0
  38. package/dist/core/client/dedupe.d.ts +7 -0
  39. package/dist/core/client/dedupe.d.ts.map +1 -0
  40. package/dist/core/client/dedupe.js +16 -0
  41. package/dist/core/client/execute.d.ts +20 -0
  42. package/dist/core/client/execute.d.ts.map +1 -0
  43. package/dist/core/client/execute.js +48 -0
  44. package/dist/core/client/hash.d.ts +7 -0
  45. package/dist/core/client/hash.d.ts.map +1 -0
  46. package/dist/core/client/hash.js +21 -0
  47. package/dist/core/client/operation-name.d.ts +7 -0
  48. package/dist/core/client/operation-name.d.ts.map +1 -0
  49. package/dist/core/client/operation-name.js +10 -0
  50. package/dist/core/client/types.d.ts +126 -0
  51. package/dist/core/client/types.d.ts.map +1 -0
  52. package/dist/core/client/types.js +26 -0
  53. package/dist/core/errors.d.ts +43 -0
  54. package/dist/core/errors.d.ts.map +1 -0
  55. package/dist/core/errors.js +43 -0
  56. package/dist/core/format.d.ts +92 -0
  57. package/dist/core/format.d.ts.map +1 -0
  58. package/dist/core/format.js +216 -0
  59. package/dist/core/helpers/assert-no-user-errors.d.ts +10 -0
  60. package/dist/core/helpers/assert-no-user-errors.d.ts.map +1 -0
  61. package/dist/core/helpers/assert-no-user-errors.js +16 -0
  62. package/dist/core/helpers/normalize-connection.d.ts +36 -0
  63. package/dist/core/helpers/normalize-connection.d.ts.map +1 -0
  64. package/dist/core/helpers/normalize-connection.js +21 -0
  65. package/dist/core/helpers/sanitize-html.d.ts +8 -0
  66. package/dist/core/helpers/sanitize-html.d.ts.map +1 -0
  67. package/dist/core/helpers/sanitize-html.js +35 -0
  68. package/dist/core/index.d.ts +59 -0
  69. package/dist/core/index.d.ts.map +1 -0
  70. package/dist/core/index.js +68 -0
  71. package/dist/core/middleware/auth.d.ts +16 -0
  72. package/dist/core/middleware/auth.d.ts.map +1 -0
  73. package/dist/core/middleware/auth.js +22 -0
  74. package/dist/core/middleware/currency.d.ts +15 -0
  75. package/dist/core/middleware/currency.d.ts.map +1 -0
  76. package/dist/core/middleware/currency.js +21 -0
  77. package/dist/core/middleware/errors.d.ts +24 -0
  78. package/dist/core/middleware/errors.d.ts.map +1 -0
  79. package/dist/core/middleware/errors.js +77 -0
  80. package/dist/core/middleware/retry.d.ts +22 -0
  81. package/dist/core/middleware/retry.d.ts.map +1 -0
  82. package/dist/core/middleware/retry.js +58 -0
  83. package/dist/core/middleware/timeout.d.ts +19 -0
  84. package/dist/core/middleware/timeout.d.ts.map +1 -0
  85. package/dist/core/middleware/timeout.js +51 -0
  86. package/dist/core/operations/auth.d.ts +11 -0
  87. package/dist/core/operations/auth.d.ts.map +1 -0
  88. package/dist/core/operations/auth.js +112 -0
  89. package/dist/core/operations/cart.d.ts +15 -0
  90. package/dist/core/operations/cart.d.ts.map +1 -0
  91. package/dist/core/operations/cart.js +169 -0
  92. package/dist/index.d.ts +24 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +24 -0
  95. package/dist/react/cookies.d.ts +28 -0
  96. package/dist/react/cookies.d.ts.map +1 -0
  97. package/dist/react/cookies.js +49 -0
  98. package/dist/react/helpers/create-store-context.d.ts +37 -0
  99. package/dist/react/helpers/create-store-context.d.ts.map +1 -0
  100. package/dist/react/helpers/create-store-context.js +47 -0
  101. package/dist/react/hooks/use-auth.d.ts +65 -0
  102. package/dist/react/hooks/use-auth.d.ts.map +1 -0
  103. package/dist/react/hooks/use-auth.js +168 -0
  104. package/dist/react/hooks/use-cart-manager.d.ts +30 -0
  105. package/dist/react/hooks/use-cart-manager.d.ts.map +1 -0
  106. package/dist/react/hooks/use-cart-manager.js +223 -0
  107. package/dist/react/hooks/use-currency.d.ts +11 -0
  108. package/dist/react/hooks/use-currency.d.ts.map +1 -0
  109. package/dist/react/hooks/use-currency.js +19 -0
  110. package/dist/react/hooks/use-debounced-value.d.ts +15 -0
  111. package/dist/react/hooks/use-debounced-value.d.ts.map +1 -0
  112. package/dist/react/hooks/use-debounced-value.js +25 -0
  113. package/dist/react/hooks/use-hydrated.d.ts +9 -0
  114. package/dist/react/hooks/use-hydrated.d.ts.map +1 -0
  115. package/dist/react/hooks/use-hydrated.js +14 -0
  116. package/dist/react/hooks/use-storefront-client.d.ts +6 -0
  117. package/dist/react/hooks/use-storefront-client.d.ts.map +1 -0
  118. package/dist/react/hooks/use-storefront-client.js +8 -0
  119. package/dist/react/index.d.ts +30 -0
  120. package/dist/react/index.d.ts.map +1 -0
  121. package/dist/react/index.js +34 -0
  122. package/dist/react/providers/currency-provider.d.ts +14 -0
  123. package/dist/react/providers/currency-provider.d.ts.map +1 -0
  124. package/dist/react/providers/currency-provider.js +20 -0
  125. package/dist/react/providers/storefront-client-provider.d.ts +33 -0
  126. package/dist/react/providers/storefront-client-provider.d.ts.map +1 -0
  127. package/dist/react/providers/storefront-client-provider.js +57 -0
  128. package/dist/react/providers/storefront-provider.d.ts +42 -0
  129. package/dist/react/providers/storefront-provider.d.ts.map +1 -0
  130. package/dist/react/providers/storefront-provider.js +40 -0
  131. package/dist/react/server/get-storefront-client.d.ts +42 -0
  132. package/dist/react/server/get-storefront-client.d.ts.map +1 -0
  133. package/dist/react/server/get-storefront-client.js +44 -0
  134. package/dist/react/server/index.d.ts +2 -0
  135. package/dist/react/server/index.d.ts.map +1 -0
  136. package/dist/react/server/index.js +1 -0
  137. package/dist/react/stores/auth.store.d.ts +48 -0
  138. package/dist/react/stores/auth.store.d.ts.map +1 -0
  139. package/dist/react/stores/auth.store.js +67 -0
  140. package/dist/react/stores/currency.store.d.ts +29 -0
  141. package/dist/react/stores/currency.store.d.ts.map +1 -0
  142. package/dist/react/stores/currency.store.js +76 -0
  143. package/dist/react/stores/index.d.ts +8 -0
  144. package/dist/react/stores/index.d.ts.map +1 -0
  145. package/dist/react/stores/index.js +10 -0
  146. package/dist/react/stores/store-context.d.ts +27 -0
  147. package/dist/react/stores/store-context.d.ts.map +1 -0
  148. package/dist/react/stores/store-context.js +62 -0
  149. package/package.json +71 -0
  150. package/src/__tests__/contract/storefront-api.contract.test.ts +450 -0
  151. package/src/__tests__/unit/auth-client.test.ts +210 -0
  152. package/src/__tests__/unit/cart-client.test.ts +233 -0
  153. package/src/__tests__/unit/create-client.test.ts +356 -0
  154. package/src/__tests__/unit/helpers.test.ts +377 -0
  155. package/src/__tests__/unit/middleware.test.ts +374 -0
  156. package/src/__tests__/unit/test-helpers.ts +103 -0
  157. package/src/core/auth/auth-client.ts +123 -0
  158. package/src/core/auth/cookie-config.ts +23 -0
  159. package/src/core/auth/handlers.ts +168 -0
  160. package/src/core/auth/routes.ts +26 -0
  161. package/src/core/auth/token-client.ts +51 -0
  162. package/src/core/auth/types.ts +54 -0
  163. package/src/core/cache.ts +102 -0
  164. package/src/core/cart/cart-client.ts +150 -0
  165. package/src/core/cart/types.ts +104 -0
  166. package/src/core/client/compose.ts +15 -0
  167. package/src/core/client/create-client.ts +129 -0
  168. package/src/core/client/dedupe.ts +19 -0
  169. package/src/core/client/execute.ts +70 -0
  170. package/src/core/client/hash.ts +21 -0
  171. package/src/core/client/operation-name.ts +12 -0
  172. package/src/core/client/types.ts +171 -0
  173. package/src/core/errors.ts +67 -0
  174. package/src/core/format.ts +254 -0
  175. package/src/core/helpers/assert-no-user-errors.ts +21 -0
  176. package/src/core/helpers/normalize-connection.ts +48 -0
  177. package/src/core/helpers/sanitize-html.ts +42 -0
  178. package/src/core/index.ts +148 -0
  179. package/src/core/middleware/auth.ts +27 -0
  180. package/src/core/middleware/currency.ts +26 -0
  181. package/src/core/middleware/errors.ts +86 -0
  182. package/src/core/middleware/retry.ts +75 -0
  183. package/src/core/middleware/timeout.ts +61 -0
  184. package/src/core/operations/auth.ts +123 -0
  185. package/src/core/operations/cart.ts +185 -0
  186. package/src/index.ts +25 -0
  187. package/src/react/cookies.ts +54 -0
  188. package/src/react/helpers/create-store-context.ts +56 -0
  189. package/src/react/hooks/use-auth.ts +218 -0
  190. package/src/react/hooks/use-cart-manager.ts +236 -0
  191. package/src/react/hooks/use-currency.ts +23 -0
  192. package/src/react/hooks/use-debounced-value.ts +30 -0
  193. package/src/react/hooks/use-hydrated.ts +20 -0
  194. package/src/react/hooks/use-storefront-client.ts +12 -0
  195. package/src/react/index.ts +45 -0
  196. package/src/react/providers/currency-provider.tsx +30 -0
  197. package/src/react/providers/storefront-client-provider.tsx +90 -0
  198. package/src/react/providers/storefront-provider.tsx +71 -0
  199. package/src/react/server/get-storefront-client.ts +60 -0
  200. package/src/react/server/index.ts +1 -0
  201. package/src/react/stores/auth.store.ts +112 -0
  202. package/src/react/stores/currency.store.ts +113 -0
  203. package/src/react/stores/index.ts +17 -0
  204. package/src/react/stores/store-context.tsx +82 -0
  205. package/tsconfig.json +20 -0
  206. package/vitest.config.ts +14 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * StorefrontClientProvider — React context for SDK clients.
3
+ *
4
+ * Creates StorefrontClient + CartClient + AuthClient and provides them
5
+ * to the component tree via React context.
6
+ *
7
+ * Registers default middleware pipeline: auth → currency → retry → timeout → errors.
8
+ */
9
+
10
+ 'use client';
11
+
12
+ import React, { createContext, useContext, useMemo } from 'react';
13
+ import { createStorefrontClient } from '../../core/client/create-client';
14
+ import { CartClient } from '../../core/cart/cart-client';
15
+ import { AuthClient } from '../../core/auth/auth-client';
16
+ import { authMiddleware } from '../../core/middleware/auth';
17
+ import { currencyMiddleware } from '../../core/middleware/currency';
18
+ import { retryMiddleware } from '../../core/middleware/retry';
19
+ import { timeoutMiddleware } from '../../core/middleware/timeout';
20
+ import { errorMiddleware } from '../../core/middleware/errors';
21
+ import { useAuthStoreApi, useCurrencyStoreApi } from '../stores/store-context';
22
+ import type { StorefrontClient, StorefrontClientConfig, Middleware } from '../../core/client/types';
23
+
24
+ export interface StorefrontClientContextValue {
25
+ client: StorefrontClient;
26
+ cartClient: CartClient;
27
+ authClient: AuthClient;
28
+ }
29
+
30
+ const StorefrontClientContext = createContext<StorefrontClientContextValue | null>(null);
31
+
32
+ export interface StorefrontClientProviderProps {
33
+ children: React.ReactNode;
34
+ config: StorefrontClientConfig;
35
+ /**
36
+ * Additional middleware to prepend to the default pipeline.
37
+ * Default pipeline: auth → currency → [custom] → retry → timeout → errors
38
+ */
39
+ middleware?: Middleware[];
40
+ }
41
+
42
+ export function StorefrontClientProvider({
43
+ children,
44
+ config,
45
+ middleware: customMiddleware = [],
46
+ }: StorefrontClientProviderProps) {
47
+ const authStore = useAuthStoreApi();
48
+ const currencyStore = useCurrencyStoreApi();
49
+
50
+ const value = useMemo(() => {
51
+ const client = createStorefrontClient({
52
+ ...config,
53
+ middleware: [
54
+ // Header middleware (runs first)
55
+ authMiddleware(() => authStore.getState().accessToken),
56
+ currencyMiddleware(() => currencyStore.getState().currency),
57
+ // Custom middleware from props
58
+ ...customMiddleware,
59
+ // Infrastructure middleware (runs last)
60
+ retryMiddleware({ maxRetries: 2 }),
61
+ timeoutMiddleware({ timeout: 5000 }),
62
+ errorMiddleware(),
63
+ ],
64
+ });
65
+
66
+ const cartClient = new CartClient(client);
67
+ const authClient = new AuthClient(client);
68
+
69
+ return { client, cartClient, authClient };
70
+ // eslint-disable-next-line react-hooks/exhaustive-deps
71
+ }, [config.apiUrl, config.shopSlug, authStore, currencyStore]);
72
+
73
+ return (
74
+ <StorefrontClientContext.Provider value={value}>
75
+ {children}
76
+ </StorefrontClientContext.Provider>
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Get StorefrontClient context value.
82
+ * Must be used within StorefrontClientProvider.
83
+ */
84
+ export function useStorefrontClientContext(): StorefrontClientContextValue {
85
+ const ctx = useContext(StorefrontClientContext);
86
+ if (!ctx) {
87
+ throw new Error('useStorefrontClientContext must be used within StorefrontClientProvider');
88
+ }
89
+ return ctx;
90
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * StorefrontProvider — convenience composition of all SDK providers.
3
+ *
4
+ * Creates store instances via useRef (stable across re-renders) and provides
5
+ * them through React Context. Store Contexts wrap StorefrontClientProvider
6
+ * so that middleware can access store state via useAuthStoreApi/useCurrencyStoreApi.
7
+ *
8
+ * Composition order: Store Contexts → StorefrontClientProvider → CurrencyProvider
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * // app/layout.tsx
13
+ * import { StorefrontProvider } from '@doswiftly/storefront-sdk/react';
14
+ *
15
+ * export default async function RootLayout({ children }) {
16
+ * const shopData = await fetchShop();
17
+ * return (
18
+ * <StorefrontProvider
19
+ * config={{ apiUrl: '...', shopSlug: '...' }}
20
+ * shopData={shopData}
21
+ * >
22
+ * {children}
23
+ * </StorefrontProvider>
24
+ * );
25
+ * }
26
+ * ```
27
+ */
28
+
29
+ 'use client';
30
+
31
+ import React, { useRef } from 'react';
32
+ import { AuthStoreContext, CurrencyStoreContext } from '../stores/store-context';
33
+ import { createAuthStore } from '../stores/auth.store';
34
+ import { createCurrencyStore } from '../stores/currency.store';
35
+ import { StorefrontClientProvider, type StorefrontClientProviderProps } from './storefront-client-provider';
36
+ import { CurrencyProvider, type CurrencyProviderProps } from './currency-provider';
37
+
38
+ export interface StorefrontProviderProps extends StorefrontClientProviderProps {
39
+ shopData: CurrencyProviderProps['shopData'];
40
+ /**
41
+ * Server-side auth hint — set to `true` when httpOnly auth cookie exists.
42
+ * This allows the auth store to start with `isAuthenticated: true` on first render,
43
+ * eliminating the flash of "Sign In" while Zustand persist rehydrates from localStorage.
44
+ *
45
+ * Read from cookies() in a Server Component (layout.tsx) and pass here.
46
+ */
47
+ initialIsAuthenticated?: boolean;
48
+ }
49
+
50
+ export function StorefrontProvider({
51
+ children,
52
+ config,
53
+ middleware,
54
+ shopData,
55
+ initialIsAuthenticated,
56
+ }: StorefrontProviderProps) {
57
+ const authStoreRef = useRef(createAuthStore(initialIsAuthenticated));
58
+ const currencyStoreRef = useRef(createCurrencyStore());
59
+
60
+ return (
61
+ <AuthStoreContext.Provider value={authStoreRef.current}>
62
+ <CurrencyStoreContext.Provider value={currencyStoreRef.current}>
63
+ <StorefrontClientProvider config={config} middleware={middleware}>
64
+ <CurrencyProvider shopData={shopData}>
65
+ {children}
66
+ </CurrencyProvider>
67
+ </StorefrontClientProvider>
68
+ </CurrencyStoreContext.Provider>
69
+ </AuthStoreContext.Provider>
70
+ );
71
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Server-side StorefrontClient factory — for Server Components, API routes, etc.
3
+ *
4
+ * Creates a StorefrontClient with default middleware pipeline,
5
+ * without requiring React context (no providers needed server-side).
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // lib/graphql/server.ts
10
+ * import { getStorefrontClient } from '@doswiftly/storefront-sdk/react/server';
11
+ *
12
+ * const client = getStorefrontClient({
13
+ * apiUrl: process.env.NEXT_PUBLIC_API_URL!,
14
+ * shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
15
+ * });
16
+ *
17
+ * export const fetchProduct = cache(async (handle: string) => {
18
+ * return client.query(ProductDocument, { handle });
19
+ * });
20
+ * ```
21
+ */
22
+
23
+ import { createStorefrontClient } from '../../core/client/create-client';
24
+ import { retryMiddleware } from '../../core/middleware/retry';
25
+ import { timeoutMiddleware } from '../../core/middleware/timeout';
26
+ import { errorMiddleware } from '../../core/middleware/errors';
27
+ import type { StorefrontClient, StorefrontClientConfig, Middleware } from '../../core/client/types';
28
+
29
+ export interface ServerClientOptions extends Omit<StorefrontClientConfig, 'middleware'> {
30
+ /**
31
+ * Function that returns request-scoped headers (e.g. currency from cookie).
32
+ * Called once per client creation.
33
+ */
34
+ getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
35
+ /**
36
+ * Additional middleware (prepended to default pipeline).
37
+ */
38
+ middleware?: Middleware[];
39
+ }
40
+
41
+ /**
42
+ * Create a StorefrontClient for server-side use.
43
+ *
44
+ * Includes default middleware: retry → timeout → errors.
45
+ * Does NOT include auth/currency middleware (server has no Zustand stores).
46
+ * Pass headers via config.defaultHeaders or getHeaders option.
47
+ */
48
+ export function getStorefrontClient(options: ServerClientOptions): StorefrontClient {
49
+ const { middleware: customMiddleware = [], ...config } = options;
50
+
51
+ return createStorefrontClient({
52
+ ...config,
53
+ middleware: [
54
+ ...customMiddleware,
55
+ retryMiddleware({ maxRetries: 2 }),
56
+ timeoutMiddleware({ timeout: 10000 }), // Server-side: 10s timeout
57
+ errorMiddleware(),
58
+ ],
59
+ });
60
+ }
@@ -0,0 +1 @@
1
+ export { getStorefrontClient, type ServerClientOptions } from './get-storefront-client';
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Auth store factory — localStorage persistence via Zustand persist.
3
+ *
4
+ * Creates a vanilla Zustand store (not a React hook singleton).
5
+ * Instance is created in StorefrontProvider via useRef and provided
6
+ * through React Context — eliminates module duplication in Turbopack.
7
+ *
8
+ * httpOnly cookie is managed separately (for SSR/middleware).
9
+ */
10
+
11
+ import { createStore } from 'zustand/vanilla';
12
+ import { persist } from 'zustand/middleware';
13
+
14
+ export interface CustomerInfo {
15
+ id: string;
16
+ email: string;
17
+ firstName?: string;
18
+ lastName?: string;
19
+ phone?: string;
20
+ }
21
+
22
+ export interface AuthStore {
23
+ // State
24
+ customer: CustomerInfo | null;
25
+ accessToken: string | null;
26
+ isAuthenticated: boolean;
27
+ isLoading: boolean;
28
+
29
+ // Actions
30
+ setAuth: (customer: CustomerInfo, accessToken: string) => void;
31
+ clearAuth: () => void;
32
+ updateCustomer: (updates: Partial<CustomerInfo>) => void;
33
+ setLoading: (isLoading: boolean) => void;
34
+ }
35
+
36
+ export const createAuthStore = (initialIsAuthenticated = false) =>
37
+ createStore<AuthStore>()(
38
+ persist(
39
+ (set) => ({
40
+ customer: null,
41
+ accessToken: null,
42
+ isAuthenticated: initialIsAuthenticated,
43
+ isLoading: false,
44
+
45
+ setAuth: (customer, accessToken) =>
46
+ set({
47
+ customer,
48
+ accessToken,
49
+ isAuthenticated: true,
50
+ }),
51
+
52
+ clearAuth: () =>
53
+ set({
54
+ customer: null,
55
+ accessToken: null,
56
+ isAuthenticated: false,
57
+ }),
58
+
59
+ updateCustomer: (updates) =>
60
+ set((state) => ({
61
+ customer: state.customer
62
+ ? { ...state.customer, ...updates }
63
+ : null,
64
+ })),
65
+
66
+ setLoading: (isLoading) => set({ isLoading }),
67
+ }),
68
+ {
69
+ name: 'auth-storage',
70
+ version: 2, // Invalidate stale data from old module-level store era
71
+ partialize: (state) => ({
72
+ customer: state.customer,
73
+ accessToken: state.accessToken,
74
+ isAuthenticated: state.isAuthenticated,
75
+ }),
76
+ /**
77
+ * Custom merge — server cookie check (initialIsAuthenticated) is authoritative
78
+ * for isAuthenticated. localStorage provides supplementary data (customer, token).
79
+ *
80
+ * This prevents stale localStorage from overwriting the server-provided auth state.
81
+ */
82
+ merge: (persistedState, currentState) => {
83
+ const persisted = persistedState as Partial<AuthStore> | undefined;
84
+ if (!persisted) return currentState;
85
+
86
+ return {
87
+ ...currentState,
88
+ customer: persisted.customer ?? currentState.customer,
89
+ accessToken: persisted.accessToken ?? currentState.accessToken,
90
+ // Server cookie is the authority — never let stale localStorage override it
91
+ isAuthenticated: currentState.isAuthenticated,
92
+ } as AuthStore;
93
+ },
94
+ migrate: (persistedState, version) => {
95
+ if (version < 2) {
96
+ // Data from old module-level store may be unreliable (Turbopack duplication)
97
+ // Clear it — user will re-authenticate via cookie or login
98
+ return { customer: null, accessToken: null, isAuthenticated: false } as {
99
+ customer: CustomerInfo | null;
100
+ accessToken: string | null;
101
+ isAuthenticated: boolean;
102
+ };
103
+ }
104
+ return persistedState as {
105
+ customer: CustomerInfo | null;
106
+ accessToken: string | null;
107
+ isAuthenticated: boolean;
108
+ };
109
+ },
110
+ },
111
+ ),
112
+ );
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Currency store factory — cookie persistence (SSR-safe).
3
+ *
4
+ * Creates a vanilla Zustand store (not a React hook singleton).
5
+ * Instance is created in StorefrontProvider via useRef and provided
6
+ * through React Context — eliminates module duplication in Turbopack.
7
+ */
8
+
9
+ import { createStore } from 'zustand/vanilla';
10
+
11
+ export interface ShopCurrencyData {
12
+ currencyCode: string;
13
+ supportedCurrencies: string[];
14
+ localeToCurrencyMap?: Array<{
15
+ locale: string;
16
+ currency: string;
17
+ }>;
18
+ }
19
+
20
+ export interface CurrencyStore {
21
+ // State
22
+ baseCurrency: string | null;
23
+ supportedCurrencies: string[];
24
+ currency: string | null;
25
+ isLoaded: boolean;
26
+
27
+ // Actions
28
+ initialize: (shopData: ShopCurrencyData) => void;
29
+ setCurrency: (currency: string) => void;
30
+ }
31
+
32
+ /**
33
+ * Get currency from cookie (client-side).
34
+ */
35
+ function getCurrencyFromCookie(): string | null {
36
+ if (typeof document === 'undefined') return null;
37
+ const match = document.cookie.match(/(?:^|;\s*)preferred-currency=([^;]*)/);
38
+ return match ? decodeURIComponent(match[1]) : null;
39
+ }
40
+
41
+ /**
42
+ * Set currency in cookie (client-side).
43
+ */
44
+ function setCurrencyCookie(currency: string): void {
45
+ if (typeof document === 'undefined') return;
46
+ const maxAge = 365 * 24 * 60 * 60; // 1 year
47
+ const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? ';secure' : '';
48
+ document.cookie = `preferred-currency=${encodeURIComponent(currency)};max-age=${maxAge};path=/;samesite=lax${secure}`;
49
+ }
50
+
51
+ export const createCurrencyStore = () =>
52
+ createStore<CurrencyStore>()((set, get) => ({
53
+ baseCurrency: null,
54
+ supportedCurrencies: [],
55
+ currency: null,
56
+ isLoaded: false,
57
+
58
+ initialize: (shopData: ShopCurrencyData) => {
59
+ // 1. Try saved currency from cookie (SSR-safe)
60
+ const saved = getCurrencyFromCookie();
61
+
62
+ // 2. Try to detect from browser locale
63
+ let detected: string | undefined;
64
+ if (shopData.localeToCurrencyMap && typeof navigator !== 'undefined') {
65
+ const browserLocale = navigator.language;
66
+ const match = shopData.localeToCurrencyMap.find(
67
+ (m) => m.locale === browserLocale,
68
+ );
69
+ detected = match?.currency;
70
+ }
71
+
72
+ // 3. Determine final currency (priority: saved > detected > base)
73
+ const finalCurrency =
74
+ saved && shopData.supportedCurrencies.includes(saved)
75
+ ? saved
76
+ : detected && shopData.supportedCurrencies.includes(detected)
77
+ ? detected
78
+ : shopData.currencyCode;
79
+
80
+ set({
81
+ baseCurrency: shopData.currencyCode,
82
+ supportedCurrencies: shopData.supportedCurrencies,
83
+ currency: finalCurrency,
84
+ isLoaded: true,
85
+ });
86
+
87
+ // Ensure cookie is set if we determined a currency
88
+ if (finalCurrency && !saved) {
89
+ setCurrencyCookie(finalCurrency);
90
+ }
91
+ },
92
+
93
+ setCurrency: (currency: string) => {
94
+ const { supportedCurrencies } = get();
95
+
96
+ if (!supportedCurrencies.includes(currency)) {
97
+ console.warn(`[CurrencyStore] Currency ${currency} not supported`);
98
+ return;
99
+ }
100
+
101
+ set({ currency });
102
+ setCurrencyCookie(currency);
103
+ },
104
+ }));
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Selectors (for use with useCurrencyStore(selector))
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export const selectCurrency = (state: CurrencyStore) => state.currency;
111
+ export const selectBaseCurrency = (state: CurrencyStore) => state.baseCurrency;
112
+ export const selectSupportedCurrencies = (state: CurrencyStore) => state.supportedCurrencies;
113
+ export const selectIsLoaded = (state: CurrencyStore) => state.isLoaded;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Zustand stores — factory functions + context hooks.
3
+ */
4
+
5
+ // Factory functions
6
+ export { createAuthStore, type AuthStore, type CustomerInfo } from './auth.store';
7
+ export { createCurrencyStore, type CurrencyStore, type ShopCurrencyData } from './currency.store';
8
+
9
+ // Selectors
10
+ export { selectCurrency, selectBaseCurrency, selectSupportedCurrencies, selectIsLoaded } from './currency.store';
11
+
12
+ // Context hooks
13
+ export {
14
+ useAuthStore, useAuthStoreApi, useAuthHydrated,
15
+ useCurrencyStore, useCurrencyStoreApi,
16
+ AuthStoreContext, CurrencyStoreContext,
17
+ } from './store-context';
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Store Context — React Context + hooks for Zustand vanilla stores.
3
+ *
4
+ * This is the SINGLE source for useAuthStore and useCurrencyStore hooks.
5
+ * Stores are created in StorefrontProvider (useRef) and provided via Context.
6
+ * This eliminates module-level singletons and Turbopack duplication bugs.
7
+ *
8
+ * useAuthHydrated() replaces the old isHydrated store field — tracks
9
+ * Zustand persist rehydration via React lifecycle (not module-level code).
10
+ */
11
+
12
+ 'use client';
13
+
14
+ import { createContext, useContext, useState, useEffect } from 'react';
15
+ import { useStore, type StoreApi } from 'zustand';
16
+ import type { AuthStore } from './auth.store';
17
+ import type { CurrencyStore } from './currency.store';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Contexts
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export const AuthStoreContext = createContext<StoreApi<AuthStore> | null>(null);
24
+ export const CurrencyStoreContext = createContext<StoreApi<CurrencyStore> | null>(null);
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Auth hooks
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export function useAuthStore(): AuthStore;
31
+ export function useAuthStore<T>(selector: (s: AuthStore) => T): T;
32
+ export function useAuthStore<T>(selector?: (s: AuthStore) => T) {
33
+ const store = useContext(AuthStoreContext);
34
+ if (!store) throw new Error('useAuthStore must be used within StorefrontProvider');
35
+ // eslint-disable-next-line react-hooks/rules-of-hooks
36
+ return selector ? useStore(store, selector) : useStore(store);
37
+ }
38
+
39
+ export function useAuthStoreApi(): StoreApi<AuthStore> {
40
+ const store = useContext(AuthStoreContext);
41
+ if (!store) throw new Error('useAuthStoreApi must be used within StorefrontProvider');
42
+ return store;
43
+ }
44
+
45
+ /**
46
+ * Tracks persist hydration state via React lifecycle (not module-level).
47
+ * Returns `true` once Zustand persist has finished rehydrating from localStorage.
48
+ */
49
+ export function useAuthHydrated(): boolean {
50
+ const store = useAuthStoreApi();
51
+ const [hydrated, setHydrated] = useState(false);
52
+
53
+ useEffect(() => {
54
+ const persistApi = (store as any).persist;
55
+ // Subscribe to future hydration completions
56
+ const unsubFinish = persistApi.onFinishHydration(() => setHydrated(true));
57
+ // Check if hydration already completed before effect ran
58
+ if (persistApi.hasHydrated()) setHydrated(true);
59
+ return unsubFinish;
60
+ }, [store]);
61
+
62
+ return hydrated;
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Currency hooks
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export function useCurrencyStore(): CurrencyStore;
70
+ export function useCurrencyStore<T>(selector: (s: CurrencyStore) => T): T;
71
+ export function useCurrencyStore<T>(selector?: (s: CurrencyStore) => T) {
72
+ const store = useContext(CurrencyStoreContext);
73
+ if (!store) throw new Error('useCurrencyStore must be used within StorefrontProvider');
74
+ // eslint-disable-next-line react-hooks/rules-of-hooks
75
+ return selector ? useStore(store, selector) : useStore(store);
76
+ }
77
+
78
+ export function useCurrencyStoreApi(): StoreApi<CurrencyStore> {
79
+ const store = useContext(CurrencyStoreContext);
80
+ if (!store) throw new Error('useCurrencyStoreApi must be used within StorefrontProvider');
81
+ return store;
82
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022", "DOM"],
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "moduleResolution": "bundler",
15
+ "resolveJsonModule": true,
16
+ "jsx": "react-jsx"
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
20
+ }
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ // Suppress CJS deprecation warning
5
+ define: {
6
+ "import.meta.vitest": "undefined",
7
+ },
8
+ test: {
9
+ environment: "node",
10
+ globals: true, // Enable global test functions like describe, it, expect
11
+ // Use pool threads for better compatibility
12
+ pool: "threads",
13
+ },
14
+ });