@doswiftly/storefront-sdk 4.0.0 → 4.2.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 (113) hide show
  1. package/README.md +51 -9
  2. package/dist/core/bot-protection/abstract-manager.d.ts +57 -0
  3. package/dist/core/bot-protection/abstract-manager.d.ts.map +1 -0
  4. package/dist/core/bot-protection/abstract-manager.js +144 -0
  5. package/dist/core/bot-protection/create-manager.d.ts +15 -0
  6. package/dist/core/bot-protection/create-manager.d.ts.map +1 -0
  7. package/dist/core/bot-protection/create-manager.js +33 -0
  8. package/dist/core/bot-protection/eucaptcha-manager.d.ts +15 -0
  9. package/dist/core/bot-protection/eucaptcha-manager.d.ts.map +1 -0
  10. package/dist/core/bot-protection/eucaptcha-manager.js +76 -0
  11. package/dist/core/bot-protection/fallback-manager.d.ts +18 -0
  12. package/dist/core/bot-protection/fallback-manager.d.ts.map +1 -0
  13. package/dist/core/bot-protection/fallback-manager.js +42 -0
  14. package/dist/core/bot-protection/turnstile-manager.d.ts +15 -0
  15. package/dist/core/bot-protection/turnstile-manager.d.ts.map +1 -0
  16. package/dist/core/bot-protection/turnstile-manager.js +78 -0
  17. package/dist/core/cart/cookie-config.d.ts +14 -0
  18. package/dist/core/cart/cookie-config.d.ts.map +1 -0
  19. package/dist/core/cart/cookie-config.js +13 -0
  20. package/dist/core/currency/cookie-config.d.ts +14 -0
  21. package/dist/core/currency/cookie-config.d.ts.map +1 -0
  22. package/dist/core/currency/cookie-config.js +13 -0
  23. package/dist/core/image.d.ts +55 -0
  24. package/dist/core/image.d.ts.map +1 -0
  25. package/dist/core/image.js +48 -0
  26. package/dist/core/index.d.ts +8 -0
  27. package/dist/core/index.d.ts.map +1 -1
  28. package/dist/core/index.js +13 -0
  29. package/dist/core/language/cookie-config.d.ts +14 -0
  30. package/dist/core/language/cookie-config.d.ts.map +1 -0
  31. package/dist/core/language/cookie-config.js +13 -0
  32. package/dist/core/middleware/bot-protection.d.ts +71 -0
  33. package/dist/core/middleware/bot-protection.d.ts.map +1 -0
  34. package/dist/core/middleware/bot-protection.js +63 -0
  35. package/dist/core/middleware/currency.d.ts.map +1 -1
  36. package/dist/core/middleware/currency.js +2 -1
  37. package/dist/core/middleware/language.d.ts +18 -0
  38. package/dist/core/middleware/language.d.ts.map +1 -0
  39. package/dist/core/middleware/language.js +25 -0
  40. package/dist/react/bot-protection/bot-protection-context.d.ts +12 -0
  41. package/dist/react/bot-protection/bot-protection-context.d.ts.map +1 -0
  42. package/dist/react/bot-protection/bot-protection-context.js +9 -0
  43. package/dist/react/bot-protection/bot-protection-widget.d.ts +13 -0
  44. package/dist/react/bot-protection/bot-protection-widget.d.ts.map +1 -0
  45. package/dist/react/bot-protection/bot-protection-widget.js +34 -0
  46. package/dist/react/cookies.d.ts +17 -0
  47. package/dist/react/cookies.d.ts.map +1 -1
  48. package/dist/react/cookies.js +36 -3
  49. package/dist/react/hooks/use-bot-protection.d.ts +16 -0
  50. package/dist/react/hooks/use-bot-protection.d.ts.map +1 -0
  51. package/dist/react/hooks/use-bot-protection.js +24 -0
  52. package/dist/react/index.d.ts +10 -1
  53. package/dist/react/index.d.ts.map +1 -1
  54. package/dist/react/index.js +9 -1
  55. package/dist/react/providers/language-provider.d.ts +18 -0
  56. package/dist/react/providers/language-provider.d.ts.map +1 -0
  57. package/dist/react/providers/language-provider.js +24 -0
  58. package/dist/react/providers/storefront-client-provider.d.ts +7 -2
  59. package/dist/react/providers/storefront-client-provider.d.ts.map +1 -1
  60. package/dist/react/providers/storefront-client-provider.js +14 -3
  61. package/dist/react/providers/storefront-provider.d.ts +7 -1
  62. package/dist/react/providers/storefront-provider.d.ts.map +1 -1
  63. package/dist/react/providers/storefront-provider.js +11 -4
  64. package/dist/react/stores/cart.context.d.ts +12 -0
  65. package/dist/react/stores/cart.context.d.ts.map +1 -0
  66. package/dist/react/stores/cart.context.js +3 -0
  67. package/dist/react/stores/cart.store.d.ts +71 -0
  68. package/dist/react/stores/cart.store.d.ts.map +1 -0
  69. package/dist/react/stores/cart.store.js +166 -0
  70. package/dist/react/stores/currency.store.d.ts +6 -9
  71. package/dist/react/stores/currency.store.d.ts.map +1 -1
  72. package/dist/react/stores/currency.store.js +5 -22
  73. package/dist/react/stores/language.store.d.ts +33 -0
  74. package/dist/react/stores/language.store.d.ts.map +1 -0
  75. package/dist/react/stores/language.store.js +67 -0
  76. package/dist/react/stores/store-context.d.ts +5 -0
  77. package/dist/react/stores/store-context.d.ts.map +1 -1
  78. package/dist/react/stores/store-context.js +14 -0
  79. package/dist/react/types/shop-config.d.ts +19 -0
  80. package/dist/react/types/shop-config.d.ts.map +1 -0
  81. package/dist/react/types/shop-config.js +7 -0
  82. package/package.json +1 -1
  83. package/src/__tests__/unit/bot-protection.test.ts +461 -0
  84. package/src/__tests__/unit/cart-store.test.ts +349 -0
  85. package/src/core/bot-protection/abstract-manager.ts +185 -0
  86. package/src/core/bot-protection/create-manager.ts +37 -0
  87. package/src/core/bot-protection/eucaptcha-manager.ts +88 -0
  88. package/src/core/bot-protection/fallback-manager.ts +43 -0
  89. package/src/core/bot-protection/turnstile-manager.ts +92 -0
  90. package/src/core/bot-protection/types/eucaptcha.d.ts +28 -0
  91. package/src/core/bot-protection/types/turnstile.d.ts +33 -0
  92. package/src/core/cart/cookie-config.ts +13 -0
  93. package/src/core/currency/cookie-config.ts +13 -0
  94. package/src/core/image.ts +75 -0
  95. package/src/core/index.ts +30 -0
  96. package/src/core/language/cookie-config.ts +13 -0
  97. package/src/core/middleware/bot-protection.ts +140 -0
  98. package/src/core/middleware/currency.ts +2 -1
  99. package/src/core/middleware/language.ts +30 -0
  100. package/src/react/bot-protection/bot-protection-context.ts +17 -0
  101. package/src/react/bot-protection/bot-protection-widget.tsx +46 -0
  102. package/src/react/cookies.ts +39 -4
  103. package/src/react/hooks/use-bot-protection.ts +31 -0
  104. package/src/react/index.ts +27 -1
  105. package/src/react/providers/language-provider.tsx +34 -0
  106. package/src/react/providers/storefront-client-provider.tsx +20 -3
  107. package/src/react/providers/storefront-provider.tsx +34 -6
  108. package/src/react/stores/cart.context.ts +10 -0
  109. package/src/react/stores/cart.store.ts +254 -0
  110. package/src/react/stores/currency.store.ts +12 -32
  111. package/src/react/stores/language.store.ts +90 -0
  112. package/src/react/stores/store-context.tsx +21 -0
  113. package/src/react/types/shop-config.ts +22 -0
@@ -16,6 +16,7 @@
16
16
  export { StorefrontProvider, type StorefrontProviderProps } from './providers/storefront-provider';
17
17
  export { StorefrontClientProvider, type StorefrontClientProviderProps } from './providers/storefront-client-provider';
18
18
  export { CurrencyProvider, type CurrencyProviderProps } from './providers/currency-provider';
19
+ export { LanguageProvider, type LanguageProviderProps } from './providers/language-provider';
19
20
 
20
21
  // Hooks
21
22
  export { useAuth, type UseAuthOptions, type LoginResult, type LogoutResult, type TokenRenewResult } from './hooks/use-auth';
@@ -26,20 +27,45 @@ export { useCurrency } from './hooks/use-currency';
26
27
  // Store hooks (Context-based)
27
28
  export { useAuthStore, useAuthStoreApi, useAuthHydrated } from './stores/store-context';
28
29
  export { useCurrencyStore, useCurrencyStoreApi } from './stores/store-context';
30
+ export { useLanguageStore, useLanguageStoreApi } from './stores/store-context';
29
31
 
30
32
  // Store types
31
33
  export type { AuthStore, CustomerInfo } from './stores/auth.store';
32
34
  export type { CurrencyStore, ShopCurrencyData } from './stores/currency.store';
35
+ export type { LanguageStore } from './stores/language.store';
36
+ export type { ShopConfig } from './types/shop-config';
33
37
 
34
38
  // Selectors
35
39
  export { selectCurrency, selectBaseCurrency, selectSupportedCurrencies, selectIsLoaded } from './stores/currency.store';
40
+ export { selectLanguage, selectDefaultLanguage, selectSupportedLanguages, selectLanguageIsLoaded } from './stores/language.store';
36
41
 
37
42
  // Cookie utilities
38
- export { getCookie, setCookie, deleteCookie, getCurrencyFromCookieAsync } from './cookies';
43
+ export { getCookie, setCookie, deleteCookie, getCurrencyFromCookieAsync, getCartIdFromCookieAsync } from './cookies';
44
+
45
+ // Bot protection
46
+ export { useBotProtection } from './hooks/use-bot-protection';
39
47
 
40
48
  // Generic hooks
41
49
  export { useHydrated } from './hooks/use-hydrated';
42
50
  export { useDebouncedValue } from './hooks/use-debounced-value';
43
51
 
52
+ // Cart store (DI-based)
53
+ export {
54
+ createCartStore,
55
+ selectCartId,
56
+ selectIsCartOpen,
57
+ selectCartIsLoading,
58
+ } from './stores/cart.store';
59
+ export type {
60
+ CartState,
61
+ CartStoreConfig,
62
+ CartActions,
63
+ CartData,
64
+ CartMutationAction,
65
+ CartLineInput,
66
+ CartLineUpdateInput,
67
+ } from './stores/cart.store';
68
+ export { CartProvider, useCartStore, useCartStoreApi } from './stores/cart.context';
69
+
44
70
  // Store context helper
45
71
  export { createStoreContext } from './helpers/create-store-context';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * LanguageProvider — initializes language store from Shop data.
3
+ *
4
+ * Thin wrapper that calls useLanguageStore.initialize() on mount
5
+ * with the shop's language configuration from the server.
6
+ *
7
+ * NOTE: This only sets defaultLanguage and supportedLanguages.
8
+ * The active `language` is set by the template's LanguageSyncProvider
9
+ * which reads locale from the URL (via next-intl) and calls setLanguage().
10
+ */
11
+
12
+ 'use client';
13
+
14
+ import React, { useEffect } from 'react';
15
+ import { useLanguageStore } from '../stores/store-context';
16
+ import type { ShopConfig } from '../types/shop-config';
17
+
18
+ export interface LanguageProviderProps {
19
+ children: React.ReactNode;
20
+ shopData: ShopConfig;
21
+ }
22
+
23
+ export function LanguageProvider({ children, shopData }: LanguageProviderProps) {
24
+ const initialize = useLanguageStore((s) => s.initialize);
25
+ const isLoaded = useLanguageStore((s) => s.isLoaded);
26
+
27
+ useEffect(() => {
28
+ if (!isLoaded) {
29
+ initialize(shopData);
30
+ }
31
+ }, [initialize, isLoaded, shopData]);
32
+
33
+ return <>{children}</>;
34
+ }
@@ -15,10 +15,12 @@ import { CartClient } from '../../core/cart/cart-client';
15
15
  import { AuthClient } from '../../core/auth/auth-client';
16
16
  import { authMiddleware } from '../../core/middleware/auth';
17
17
  import { currencyMiddleware } from '../../core/middleware/currency';
18
+ import { languageMiddleware } from '../../core/middleware/language';
19
+ import { botProtectionMiddleware, type BotProtectionTokenProvider } from '../../core/middleware/bot-protection';
18
20
  import { retryMiddleware } from '../../core/middleware/retry';
19
21
  import { timeoutMiddleware } from '../../core/middleware/timeout';
20
22
  import { errorMiddleware } from '../../core/middleware/errors';
21
- import { useAuthStoreApi, useCurrencyStoreApi } from '../stores/store-context';
23
+ import { useAuthStoreApi, useCurrencyStoreApi, useLanguageStoreApi } from '../stores/store-context';
22
24
  import type { StorefrontClient, StorefrontClientConfig, Middleware } from '../../core/client/types';
23
25
 
24
26
  export interface StorefrontClientContextValue {
@@ -34,18 +36,25 @@ export interface StorefrontClientProviderProps {
34
36
  config: StorefrontClientConfig;
35
37
  /**
36
38
  * Additional middleware to prepend to the default pipeline.
37
- * Default pipeline: auth → currency → [custom] → retry → timeout → errors
39
+ * Default pipeline: auth → currency → bot-protection → [custom] → retry → timeout → errors
38
40
  */
39
41
  middleware?: Middleware[];
42
+ /** Bot protection token provider (created by StorefrontProvider) */
43
+ botProtection?: BotProtectionTokenProvider | null;
44
+ /** Operations that require bot protection (from shop query) */
45
+ botProtectionOperations?: string[];
40
46
  }
41
47
 
42
48
  export function StorefrontClientProvider({
43
49
  children,
44
50
  config,
45
51
  middleware: customMiddleware = [],
52
+ botProtection,
53
+ botProtectionOperations,
46
54
  }: StorefrontClientProviderProps) {
47
55
  const authStore = useAuthStoreApi();
48
56
  const currencyStore = useCurrencyStoreApi();
57
+ const languageStore = useLanguageStoreApi();
49
58
 
50
59
  const value = useMemo(() => {
51
60
  const client = createStorefrontClient({
@@ -54,6 +63,14 @@ export function StorefrontClientProvider({
54
63
  // Header middleware (runs first)
55
64
  authMiddleware(() => authStore.getState().accessToken),
56
65
  currencyMiddleware(() => currencyStore.getState().currency),
66
+ languageMiddleware(() => languageStore.getState().language),
67
+ // Bot protection (if configured)
68
+ ...(botProtection && botProtectionOperations?.length
69
+ ? [botProtectionMiddleware({
70
+ tokenProvider: botProtection,
71
+ protectedOperations: botProtectionOperations,
72
+ })]
73
+ : []),
57
74
  // Custom middleware from props
58
75
  ...customMiddleware,
59
76
  // Infrastructure middleware (runs last)
@@ -68,7 +85,7 @@ export function StorefrontClientProvider({
68
85
 
69
86
  return { client, cartClient, authClient };
70
87
  // eslint-disable-next-line react-hooks/exhaustive-deps
71
- }, [config.apiUrl, config.shopSlug, authStore, currencyStore]);
88
+ }, [config.apiUrl, config.shopSlug, authStore, currencyStore, languageStore]);
72
89
 
73
90
  return (
74
91
  <StorefrontClientContext.Provider value={value}>
@@ -29,11 +29,16 @@
29
29
  'use client';
30
30
 
31
31
  import React, { useRef } from 'react';
32
- import { AuthStoreContext, CurrencyStoreContext } from '../stores/store-context';
32
+ import { AuthStoreContext, CurrencyStoreContext, LanguageStoreContext } from '../stores/store-context';
33
33
  import { createAuthStore } from '../stores/auth.store';
34
34
  import { createCurrencyStore } from '../stores/currency.store';
35
+ import { createLanguageStore } from '../stores/language.store';
35
36
  import { StorefrontClientProvider, type StorefrontClientProviderProps } from './storefront-client-provider';
36
37
  import { CurrencyProvider, type CurrencyProviderProps } from './currency-provider';
38
+ import { LanguageProvider } from './language-provider';
39
+ import { createBotProtectionManager } from '../../core/bot-protection/create-manager';
40
+ import { BotProtectionContext } from '../bot-protection/bot-protection-context';
41
+ import { BotProtectionWidget } from '../bot-protection/bot-protection-widget';
37
42
 
38
43
  export interface StorefrontProviderProps extends StorefrontClientProviderProps {
39
44
  shopData: CurrencyProviderProps['shopData'];
@@ -45,6 +50,12 @@ export interface StorefrontProviderProps extends StorefrontClientProviderProps {
45
50
  * Read from cookies() in a Server Component (layout.tsx) and pass here.
46
51
  */
47
52
  initialIsAuthenticated?: boolean;
53
+ /**
54
+ * Server-side language hint — pass the URL locale from next-intl params.
55
+ * Eliminates flash of wrong language on first render by initializing the
56
+ * language store with the correct value from the server.
57
+ */
58
+ initialLanguage?: string;
48
59
  }
49
60
 
50
61
  export function StorefrontProvider({
@@ -53,18 +64,35 @@ export function StorefrontProvider({
53
64
  middleware,
54
65
  shopData,
55
66
  initialIsAuthenticated,
67
+ initialLanguage,
56
68
  }: StorefrontProviderProps) {
57
69
  const authStoreRef = useRef(createAuthStore(initialIsAuthenticated));
58
70
  const currencyStoreRef = useRef(createCurrencyStore());
71
+ const languageStoreRef = useRef(createLanguageStore(initialLanguage));
72
+ const botProtectionRef = useRef(
73
+ shopData.botProtection ? createBotProtectionManager(shopData.botProtection) : null,
74
+ );
59
75
 
60
76
  return (
61
77
  <AuthStoreContext.Provider value={authStoreRef.current}>
62
78
  <CurrencyStoreContext.Provider value={currencyStoreRef.current}>
63
- <StorefrontClientProvider config={config} middleware={middleware}>
64
- <CurrencyProvider shopData={shopData}>
65
- {children}
66
- </CurrencyProvider>
67
- </StorefrontClientProvider>
79
+ <LanguageStoreContext.Provider value={languageStoreRef.current}>
80
+ <StorefrontClientProvider
81
+ config={config}
82
+ middleware={middleware}
83
+ botProtection={botProtectionRef.current}
84
+ botProtectionOperations={shopData.botProtection?.protectedOperations}
85
+ >
86
+ <BotProtectionContext.Provider value={{ manager: botProtectionRef.current }}>
87
+ <CurrencyProvider shopData={shopData}>
88
+ <LanguageProvider shopData={shopData}>
89
+ <BotProtectionWidget manager={botProtectionRef.current} />
90
+ {children}
91
+ </LanguageProvider>
92
+ </CurrencyProvider>
93
+ </BotProtectionContext.Provider>
94
+ </StorefrontClientProvider>
95
+ </LanguageStoreContext.Provider>
68
96
  </CurrencyStoreContext.Provider>
69
97
  </AuthStoreContext.Provider>
70
98
  );
@@ -0,0 +1,10 @@
1
+ 'use client';
2
+
3
+ import { createStoreContext } from '../helpers/create-store-context';
4
+ import type { CartState } from './cart.store';
5
+
6
+ export const {
7
+ Provider: CartProvider,
8
+ useStore: useCartStore,
9
+ useApi: useCartStoreApi,
10
+ } = createStoreContext<CartState>('CartStore');
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Cart Store — DI-based cart state management with cookie persistence.
3
+ *
4
+ * SDK orchestrates cart lifecycle (init, mutations, error handling).
5
+ * Template provides CartActions implementation via DI (getActions getter).
6
+ * Cart ID persisted in cookie (SSR/edge visible) — follows currency store pattern.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { createCartStore, type CartActions } from '@doswiftly/storefront-sdk/react';
11
+ *
12
+ * const actions: CartActions = {
13
+ * fetchCart: async (cartId) => api.getCart(cartId),
14
+ * createCart: async () => api.createCart().then(c => c.id),
15
+ * addLines: async (cartId, lines) => api.addLines(cartId, lines),
16
+ * updateLines: async (cartId, lines) => api.updateLines(cartId, lines),
17
+ * removeLines: async (cartId, lineIds) => api.removeLines(cartId, lineIds),
18
+ * };
19
+ *
20
+ * const store = createCartStore({ getActions: () => actions });
21
+ * ```
22
+ */
23
+
24
+ import { createStore } from 'zustand/vanilla';
25
+ import type { CartLineInput, CartLineUpdateInput } from '../../core/cart/types';
26
+ import { CART_COOKIE_NAME, CART_COOKIE_MAX_AGE } from '../../core/cart/cookie-config';
27
+ import { getCookie, setCookie, deleteCookie } from '../cookies';
28
+
29
+ // Re-export input types from core for convenience
30
+ export type { CartLineInput, CartLineUpdateInput } from '../../core/cart/types';
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // DI Contract
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /** Minimal cart data returned by DI actions. */
37
+ export interface CartData {
38
+ id: string;
39
+ totalQuantity: number;
40
+ }
41
+
42
+ /** DI contract — template implements these methods. */
43
+ export interface CartActions {
44
+ /** Fetch cart by ID. Return null if cart expired/not found. */
45
+ fetchCart: (cartId: string) => Promise<CartData | null>;
46
+ /** Create new empty cart. Return new cart ID. */
47
+ createCart: () => Promise<string>;
48
+ /** Add line items. Return updated cart or throw. */
49
+ addLines: (cartId: string, lines: CartLineInput[]) => Promise<CartData>;
50
+ /** Update line quantities. Return updated cart or throw. */
51
+ updateLines: (cartId: string, lines: CartLineUpdateInput[]) => Promise<CartData>;
52
+ /** Remove line items. Return updated cart or throw. */
53
+ removeLines: (cartId: string, lineIds: string[]) => Promise<CartData>;
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Config
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /** Action names passed to mutation callbacks. */
61
+ export type CartMutationAction = 'initCart' | 'addToCart' | 'updateQuantity' | 'removeFromCart';
62
+
63
+ export interface CartStoreConfig {
64
+ /** Getter for DI actions — called on every operation (not captured). */
65
+ getActions: () => CartActions;
66
+ /** Called after successful mutation. */
67
+ onMutationSuccess?: (action: CartMutationAction, cart: CartData) => void;
68
+ /** Called on mutation error. */
69
+ onMutationError?: (action: CartMutationAction, error: unknown) => void;
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // State
74
+ // ---------------------------------------------------------------------------
75
+
76
+ export interface CartState {
77
+ // State
78
+ cartId: string | null;
79
+ isOpen: boolean;
80
+ isLoading: boolean;
81
+ error: unknown | null;
82
+
83
+ // UI Actions (pure state, no side effects)
84
+ openCart: () => void;
85
+ closeCart: () => void;
86
+ toggleCart: () => void;
87
+
88
+ // Orchestrated Actions (call DI via getActions)
89
+ initCart: () => Promise<void>;
90
+ addToCart: (lines: CartLineInput[]) => Promise<void>;
91
+ updateQuantity: (lines: CartLineUpdateInput[]) => Promise<void>;
92
+ removeFromCart: (lineIds: string[]) => Promise<void>;
93
+ clearCart: () => void;
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Selectors
98
+ // ---------------------------------------------------------------------------
99
+
100
+ export const selectCartId = (state: CartState) => state.cartId;
101
+ export const selectIsCartOpen = (state: CartState) => state.isOpen;
102
+ export const selectCartIsLoading = (state: CartState) => state.isLoading;
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Factory
106
+ // ---------------------------------------------------------------------------
107
+
108
+ export function createCartStore(config: CartStoreConfig) {
109
+ // Deduplication: shared Promise in closure (not in state)
110
+ let initPromise: Promise<void> | null = null;
111
+
112
+ // Read initial cartId from cookie (client-side only, returns null on server)
113
+ const initialCartId = getCookie(CART_COOKIE_NAME);
114
+
115
+ // Clean up old localStorage entry from pre-cookie era (v1 persist)
116
+ if (typeof localStorage !== 'undefined') {
117
+ try { localStorage.removeItem('cart-storage'); } catch { /* ignore */ }
118
+ }
119
+
120
+ async function performInit(
121
+ set: (partial: Partial<CartState> | ((state: CartState) => Partial<CartState>)) => void,
122
+ get: () => CartState,
123
+ ): Promise<void> {
124
+ const actions = config.getActions();
125
+ set({ isLoading: true, error: null });
126
+
127
+ try {
128
+ const currentCartId = get().cartId;
129
+
130
+ if (currentCartId) {
131
+ const cart = await actions.fetchCart(currentCartId);
132
+ if (cart) {
133
+ // Cart exists — keep current cartId
134
+ set({ isLoading: false });
135
+ return;
136
+ }
137
+ // Cart expired — fall through to create
138
+ }
139
+
140
+ // No cart or expired — create new
141
+ const newCartId = await actions.createCart();
142
+ set({ cartId: newCartId, isLoading: false, error: null });
143
+ } catch (error) {
144
+ set({ error, isLoading: false });
145
+ config.onMutationError?.('initCart', error);
146
+ }
147
+ }
148
+
149
+ const store = createStore<CartState>()(
150
+ (set, get) => ({
151
+ // State — initialCartId from cookie
152
+ cartId: initialCartId,
153
+ isOpen: false,
154
+ isLoading: false,
155
+ error: null,
156
+
157
+ // UI Actions
158
+ openCart: () => set({ isOpen: true }),
159
+ closeCart: () => set({ isOpen: false }),
160
+ toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
161
+
162
+ // Orchestrated: initCart (deduplicated)
163
+ initCart: async () => {
164
+ if (initPromise) return initPromise;
165
+ initPromise = performInit(set, get).finally(() => {
166
+ initPromise = null;
167
+ });
168
+ return initPromise;
169
+ },
170
+
171
+ // Orchestrated: addToCart (auto-init if no cartId)
172
+ addToCart: async (lines: CartLineInput[]) => {
173
+ const actions = config.getActions();
174
+ set({ isLoading: true, error: null });
175
+
176
+ try {
177
+ let cartId = get().cartId;
178
+ if (!cartId) {
179
+ await get().initCart();
180
+ cartId = get().cartId;
181
+ }
182
+ if (!cartId) {
183
+ throw new Error('Failed to initialize cart');
184
+ }
185
+
186
+ const cart = await actions.addLines(cartId, lines);
187
+ set({ isLoading: false, error: null });
188
+ config.onMutationSuccess?.('addToCart', cart);
189
+ } catch (error) {
190
+ set({ error, isLoading: false });
191
+ config.onMutationError?.('addToCart', error);
192
+ }
193
+ },
194
+
195
+ // Orchestrated: updateQuantity (error if no cartId)
196
+ updateQuantity: async (lines: CartLineUpdateInput[]) => {
197
+ const actions = config.getActions();
198
+ const cartId = get().cartId;
199
+
200
+ if (!cartId) {
201
+ const err = new Error('Cannot update quantity: no cart exists');
202
+ set({ error: err });
203
+ config.onMutationError?.('updateQuantity', err);
204
+ return;
205
+ }
206
+
207
+ set({ isLoading: true, error: null });
208
+ try {
209
+ const cart = await actions.updateLines(cartId, lines);
210
+ set({ isLoading: false, error: null });
211
+ config.onMutationSuccess?.('updateQuantity', cart);
212
+ } catch (error) {
213
+ set({ error, isLoading: false });
214
+ config.onMutationError?.('updateQuantity', error);
215
+ }
216
+ },
217
+
218
+ // Orchestrated: removeFromCart (silent return if no cartId)
219
+ removeFromCart: async (lineIds: string[]) => {
220
+ const cartId = get().cartId;
221
+ if (!cartId) return;
222
+
223
+ const actions = config.getActions();
224
+ set({ isLoading: true, error: null });
225
+ try {
226
+ const cart = await actions.removeLines(cartId, lineIds);
227
+ set({ isLoading: false, error: null });
228
+ config.onMutationSuccess?.('removeFromCart', cart);
229
+ } catch (error) {
230
+ set({ error, isLoading: false });
231
+ config.onMutationError?.('removeFromCart', error);
232
+ }
233
+ },
234
+
235
+ // clearCart — reset all state
236
+ clearCart: () => {
237
+ set({ cartId: null, isOpen: false, isLoading: false, error: null });
238
+ },
239
+ }),
240
+ );
241
+
242
+ // Sync cartId changes to cookie (like currency store pattern)
243
+ store.subscribe((state, prevState) => {
244
+ if (state.cartId !== prevState.cartId) {
245
+ if (state.cartId) {
246
+ setCookie(CART_COOKIE_NAME, state.cartId, { maxAge: CART_COOKIE_MAX_AGE });
247
+ } else {
248
+ deleteCookie(CART_COOKIE_NAME);
249
+ }
250
+ }
251
+ });
252
+
253
+ return store;
254
+ }
@@ -7,15 +7,14 @@
7
7
  */
8
8
 
9
9
  import { createStore } from 'zustand/vanilla';
10
+ import type { ShopConfig } from '../types/shop-config';
11
+ import { CURRENCY_COOKIE_NAME, CURRENCY_COOKIE_MAX_AGE } from '../../core/currency/cookie-config';
12
+ import { getCookie, setCookie } from '../cookies';
10
13
 
11
- export interface ShopCurrencyData {
12
- currencyCode: string;
13
- supportedCurrencies: string[];
14
- localeToCurrencyMap?: Array<{
15
- locale: string;
16
- currency: string;
17
- }>;
18
- }
14
+ /**
15
+ * @deprecated Use ShopConfig instead. Kept for backward compatibility of type exports.
16
+ */
17
+ export type ShopCurrencyData = ShopConfig;
19
18
 
20
19
  export interface CurrencyStore {
21
20
  // State
@@ -25,29 +24,10 @@ export interface CurrencyStore {
25
24
  isLoaded: boolean;
26
25
 
27
26
  // Actions
28
- initialize: (shopData: ShopCurrencyData) => void;
27
+ initialize: (shopData: ShopConfig) => void;
29
28
  setCurrency: (currency: string) => void;
30
29
  }
31
30
 
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
31
  export const createCurrencyStore = () =>
52
32
  createStore<CurrencyStore>()((set, get) => ({
53
33
  baseCurrency: null,
@@ -55,9 +35,9 @@ export const createCurrencyStore = () =>
55
35
  currency: null,
56
36
  isLoaded: false,
57
37
 
58
- initialize: (shopData: ShopCurrencyData) => {
38
+ initialize: (shopData: ShopConfig) => {
59
39
  // 1. Try saved currency from cookie (SSR-safe)
60
- const saved = getCurrencyFromCookie();
40
+ const saved = getCookie(CURRENCY_COOKIE_NAME);
61
41
 
62
42
  // 2. Try to detect from browser locale
63
43
  let detected: string | undefined;
@@ -86,7 +66,7 @@ export const createCurrencyStore = () =>
86
66
 
87
67
  // Ensure cookie is set if we determined a currency
88
68
  if (finalCurrency && !saved) {
89
- setCurrencyCookie(finalCurrency);
69
+ setCookie(CURRENCY_COOKIE_NAME, finalCurrency, { maxAge: CURRENCY_COOKIE_MAX_AGE });
90
70
  }
91
71
  },
92
72
 
@@ -99,7 +79,7 @@ export const createCurrencyStore = () =>
99
79
  }
100
80
 
101
81
  set({ currency });
102
- setCurrencyCookie(currency);
82
+ setCookie(CURRENCY_COOKIE_NAME, currency, { maxAge: CURRENCY_COOKIE_MAX_AGE });
103
83
  },
104
84
  }));
105
85
 
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Language store factory — tracks active language for SDK middleware.
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
+ * KEY PRINCIPLE: URL is the single source of truth for language.
9
+ * - next-intl middleware uses `preferred-language` cookie (custom name via localeCookie.name)
10
+ * - This store tracks the active language for SDK X-Lang header
11
+ * - setLanguage() persists to `preferred-language` cookie
12
+ * - initialize() sets defaultLanguage and supportedLanguages from Shop query
13
+ * - setLanguage() is called by LanguageSyncProvider when URL locale changes
14
+ *
15
+ * Single `preferred-language` cookie serves both:
16
+ * - next-intl middleware (locale detection/routing, via localeCookie.name override)
17
+ * - SDK language store (X-Lang header, framework-agnostic)
18
+ */
19
+
20
+ import { createStore } from 'zustand/vanilla';
21
+ import type { ShopConfig } from '../types/shop-config';
22
+ import { LANGUAGE_COOKIE_NAME, LANGUAGE_COOKIE_MAX_AGE } from '../../core/language/cookie-config';
23
+ import { getCookie, setCookie } from '../cookies';
24
+
25
+ export interface LanguageStore {
26
+ // State
27
+ defaultLanguage: string | null;
28
+ supportedLanguages: string[];
29
+ language: string | null;
30
+ isLoaded: boolean;
31
+
32
+ // Actions
33
+ initialize: (shopData: ShopConfig) => void;
34
+ setLanguage: (language: string) => void;
35
+ }
36
+
37
+ export const createLanguageStore = (initialLanguage?: string | null) =>
38
+ createStore<LanguageStore>()((set, get) => ({
39
+ defaultLanguage: null,
40
+ supportedLanguages: [],
41
+ language: initialLanguage ?? null,
42
+ isLoaded: false,
43
+
44
+ initialize: (shopData: ShopConfig) => {
45
+ const supported = shopData.supportedLanguages ?? [];
46
+ const current = get().language;
47
+ const saved = getCookie(LANGUAGE_COOKIE_NAME);
48
+
49
+ // Priority: current (from initialLanguage/URL) > cookie (validated) > default
50
+ const finalLanguage =
51
+ current && (supported.length === 0 || supported.includes(current))
52
+ ? current
53
+ : saved && (supported.length === 0 || supported.includes(saved))
54
+ ? saved
55
+ : shopData.defaultLanguage ?? null;
56
+
57
+ set({
58
+ defaultLanguage: shopData.defaultLanguage ?? null,
59
+ supportedLanguages: supported,
60
+ language: finalLanguage,
61
+ isLoaded: true,
62
+ });
63
+
64
+ // Persist cookie if determined from fallback (not already saved)
65
+ if (finalLanguage && finalLanguage !== saved) {
66
+ setCookie(LANGUAGE_COOKIE_NAME, finalLanguage, { maxAge: LANGUAGE_COOKIE_MAX_AGE });
67
+ }
68
+ },
69
+
70
+ setLanguage: (language: string) => {
71
+ const { supportedLanguages } = get();
72
+ // Validate vs supported (bypass if empty — LanguageSyncProvider may call before initialize)
73
+ if (supportedLanguages.length > 0 && !supportedLanguages.includes(language)) {
74
+ console.warn(`[LanguageStore] Language ${language} not supported`);
75
+ return;
76
+ }
77
+ if (get().language === language) return;
78
+ setCookie(LANGUAGE_COOKIE_NAME, language, { maxAge: LANGUAGE_COOKIE_MAX_AGE });
79
+ set({ language });
80
+ },
81
+ }));
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Selectors (for use with useLanguageStore(selector))
85
+ // ---------------------------------------------------------------------------
86
+
87
+ export const selectLanguage = (state: LanguageStore) => state.language;
88
+ export const selectDefaultLanguage = (state: LanguageStore) => state.defaultLanguage;
89
+ export const selectSupportedLanguages = (state: LanguageStore) => state.supportedLanguages;
90
+ export const selectLanguageIsLoaded = (state: LanguageStore) => state.isLoaded;