@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,218 @@
1
+ /**
2
+ * useAuth — wraps AuthClient + updates auth store.
3
+ *
4
+ * Centralizes login/logout/renew with dual-layer persistence:
5
+ * - httpOnly cookie (for SSR/middleware) — via onSetToken/onClearToken callbacks
6
+ * - Zustand store (for client-side state)
7
+ *
8
+ * Does NOT use React Query — plain async + store updates.
9
+ * Template can wrap in useMutation() if React Query features are needed.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const { login, logout, isLoading } = useAuth();
14
+ *
15
+ * const result = await login('user@example.com', 'password');
16
+ * if (result.success) router.push('/account');
17
+ * ```
18
+ */
19
+
20
+ 'use client';
21
+
22
+ import { useState, useCallback } from 'react';
23
+ import { useStorefrontClientContext } from '../providers/storefront-client-provider';
24
+ import { useAuthStore, useAuthStoreApi } from '../stores/store-context';
25
+ import { StorefrontError } from '../../core/errors';
26
+
27
+ export interface UseAuthOptions {
28
+ /**
29
+ * Called after successful login/renewToken with the new access token.
30
+ * Use to set httpOnly cookie via API route.
31
+ */
32
+ onSetToken?: (token: string) => Promise<void>;
33
+ /**
34
+ * Called after logout to clear httpOnly cookie.
35
+ */
36
+ onClearToken?: () => Promise<void>;
37
+ }
38
+
39
+ export interface LoginResult {
40
+ success: boolean;
41
+ userErrors: Array<{ message: string; field?: string[] }>;
42
+ accessToken?: string;
43
+ expiresAt?: string;
44
+ }
45
+
46
+ export interface LogoutResult {
47
+ success: boolean;
48
+ userErrors: Array<{ message: string; field?: string[] }>;
49
+ }
50
+
51
+ export interface TokenRenewResult {
52
+ success: boolean;
53
+ userErrors: Array<{ message: string; field?: string[] }>;
54
+ accessToken?: string;
55
+ expiresAt?: string;
56
+ }
57
+
58
+ export function useAuth(options: UseAuthOptions = {}) {
59
+ const { authClient } = useStorefrontClientContext();
60
+ const { setAuth, clearAuth } = useAuthStore();
61
+ const authStore = useAuthStoreApi();
62
+
63
+ const [isLoggingIn, setIsLoggingIn] = useState(false);
64
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
65
+ const [isRenewingToken, setIsRenewingToken] = useState(false);
66
+ const [error, setError] = useState<string | null>(null);
67
+
68
+ const login = useCallback(async (email: string, password: string): Promise<LoginResult> => {
69
+ setError(null);
70
+ setIsLoggingIn(true);
71
+
72
+ try {
73
+ const result = await authClient.login(email, password);
74
+
75
+ // Set httpOnly cookie
76
+ if (options.onSetToken) {
77
+ await options.onSetToken(result.accessToken);
78
+ }
79
+
80
+ // Fetch customer data and set store
81
+ try {
82
+ const customer = await authClient.getCustomer(result.accessToken);
83
+ if (customer) {
84
+ setAuth(
85
+ {
86
+ id: customer.id,
87
+ email: customer.email,
88
+ firstName: customer.firstName ?? undefined,
89
+ lastName: customer.lastName ?? undefined,
90
+ phone: customer.phone ?? undefined,
91
+ },
92
+ result.accessToken,
93
+ );
94
+ } else {
95
+ setAuth({ id: '', email }, result.accessToken);
96
+ }
97
+ } catch {
98
+ // Customer fetch failed — store minimal data
99
+ setAuth({ id: '', email }, result.accessToken);
100
+ }
101
+
102
+ return {
103
+ success: true,
104
+ userErrors: [],
105
+ accessToken: result.accessToken,
106
+ expiresAt: result.expiresAt,
107
+ };
108
+ } catch (err) {
109
+ if (err instanceof StorefrontError && err.hasUserErrors) {
110
+ return {
111
+ success: false,
112
+ userErrors: err.userErrors.map((e) => ({
113
+ message: e.message,
114
+ field: e.field,
115
+ })),
116
+ };
117
+ }
118
+
119
+ const message = err instanceof Error ? err.message : 'Login failed';
120
+ setError(message);
121
+ return { success: false, userErrors: [{ message }] };
122
+ } finally {
123
+ setIsLoggingIn(false);
124
+ }
125
+ }, [authClient, setAuth, options]);
126
+
127
+ const logout = useCallback(async (): Promise<LogoutResult> => {
128
+ setError(null);
129
+ setIsLoggingOut(true);
130
+
131
+ try {
132
+ const token = authStore.getState().accessToken;
133
+ if (token) {
134
+ await authClient.logout(token);
135
+ }
136
+
137
+ // Clear httpOnly cookie
138
+ if (options.onClearToken) {
139
+ await options.onClearToken();
140
+ }
141
+
142
+ clearAuth();
143
+
144
+ return { success: true, userErrors: [] };
145
+ } catch (err) {
146
+ // Even on error, clear local state
147
+ clearAuth();
148
+ const message = err instanceof Error ? err.message : 'Logout failed';
149
+ setError(message);
150
+ return { success: false, userErrors: [{ message }] };
151
+ } finally {
152
+ setIsLoggingOut(false);
153
+ }
154
+ }, [authClient, clearAuth, options, authStore]);
155
+
156
+ const renewToken = useCallback(async (): Promise<TokenRenewResult> => {
157
+ setError(null);
158
+ setIsRenewingToken(true);
159
+
160
+ try {
161
+ const token = authStore.getState().accessToken;
162
+ if (!token) {
163
+ return {
164
+ success: false,
165
+ userErrors: [{ message: 'No token to renew' }],
166
+ };
167
+ }
168
+
169
+ const result = await authClient.renewToken(token);
170
+
171
+ // Update httpOnly cookie
172
+ if (options.onSetToken) {
173
+ await options.onSetToken(result.accessToken);
174
+ }
175
+
176
+ // Update store (keep customer data, update token)
177
+ const currentCustomer = authStore.getState().customer;
178
+ if (currentCustomer) {
179
+ setAuth(currentCustomer, result.accessToken);
180
+ }
181
+
182
+ return {
183
+ success: true,
184
+ userErrors: [],
185
+ accessToken: result.accessToken,
186
+ expiresAt: result.expiresAt,
187
+ };
188
+ } catch (err) {
189
+ if (err instanceof StorefrontError && err.hasUserErrors) {
190
+ return {
191
+ success: false,
192
+ userErrors: err.userErrors.map((e) => ({
193
+ message: e.message,
194
+ field: e.field,
195
+ })),
196
+ };
197
+ }
198
+
199
+ const message = err instanceof Error ? err.message : 'Token renewal failed';
200
+ setError(message);
201
+ return { success: false, userErrors: [{ message }] };
202
+ } finally {
203
+ setIsRenewingToken(false);
204
+ }
205
+ }, [authClient, setAuth, options, authStore]);
206
+
207
+ return {
208
+ login,
209
+ logout,
210
+ renewToken,
211
+
212
+ isLoggingIn,
213
+ isLoggingOut,
214
+ isRenewingToken,
215
+ isLoading: isLoggingIn || isLoggingOut || isRenewingToken,
216
+ error,
217
+ };
218
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * useCartManager — wraps CartClient with cart ID persistence + loading states.
3
+ *
4
+ * Auto-creates cart on first add. CartId persisted in cookie (SSR/edge visible).
5
+ * Does NOT use React Query — plain async + useState.
6
+ * Template wraps in useMutation() if React Query features are needed.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * const { addItem, updateQuantity, removeItem, isLoading } = useCartManager();
11
+ *
12
+ * await addItem([{ merchandiseId: 'variant-123', quantity: 1 }]);
13
+ * ```
14
+ */
15
+
16
+ 'use client';
17
+
18
+ import { useState, useCallback } from 'react';
19
+ import { useStorefrontClientContext } from '../providers/storefront-client-provider';
20
+ import type { Cart, CartLineInput, CartLineUpdateInput, CartCreateInput, CartBuyerIdentityInput } from '../../core/cart/types';
21
+ import { StorefrontError } from '../../core/errors';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Cart ID persistence (cookie — SSR/edge visible)
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function getCartIdFromCookie(): string | null {
28
+ if (typeof document === 'undefined') return null;
29
+ const match = document.cookie.match(/(?:^|;\s*)cart-id=([^;]*)/);
30
+ return match ? decodeURIComponent(match[1]) : null;
31
+ }
32
+
33
+ function setCartIdCookie(cartId: string): void {
34
+ if (typeof document === 'undefined') return;
35
+ const maxAge = 30 * 24 * 60 * 60; // 30 days
36
+ document.cookie = `cart-id=${encodeURIComponent(cartId)};max-age=${maxAge};path=/;samesite=lax`;
37
+ }
38
+
39
+ function clearCartIdCookie(): void {
40
+ if (typeof document === 'undefined') return;
41
+ document.cookie = 'cart-id=;max-age=0;path=/';
42
+ }
43
+
44
+ export function useCartManager() {
45
+ const { cartClient } = useStorefrontClientContext();
46
+ const [isLoading, setIsLoading] = useState(false);
47
+ const [error, setError] = useState<string | null>(null);
48
+
49
+ /**
50
+ * Get existing cart ID from cookie or create a new cart.
51
+ */
52
+ const getOrCreateCartId = useCallback(async (forceNew = false): Promise<string> => {
53
+ if (!forceNew) {
54
+ const existing = getCartIdFromCookie();
55
+ if (existing) return existing;
56
+ }
57
+
58
+ const cart = await cartClient.create();
59
+ setCartIdCookie(cart.id);
60
+ return cart.id;
61
+ }, [cartClient]);
62
+
63
+ /**
64
+ * Check if error indicates cart not found (expired).
65
+ */
66
+ const isCartExpired = (err: unknown): boolean => {
67
+ if (err instanceof StorefrontError) {
68
+ return err.message.toLowerCase().includes('cart not found') ||
69
+ err.message.toLowerCase().includes('cart does not exist');
70
+ }
71
+ return false;
72
+ };
73
+
74
+ /**
75
+ * Fetch current cart. Returns null if no cart exists.
76
+ */
77
+ const getCart = useCallback(async (): Promise<Cart | null> => {
78
+ const cartId = getCartIdFromCookie();
79
+ if (!cartId) return null;
80
+
81
+ try {
82
+ return await cartClient.get(cartId);
83
+ } catch (err) {
84
+ if (isCartExpired(err)) {
85
+ clearCartIdCookie();
86
+ return null;
87
+ }
88
+ throw err;
89
+ }
90
+ }, [cartClient]);
91
+
92
+ /**
93
+ * Add items to cart. Creates cart if needed.
94
+ * On expired cart, clears cookie, creates new cart, retries once.
95
+ */
96
+ const addItem = useCallback(async (
97
+ lines: CartLineInput[],
98
+ options?: { forceNewCart?: boolean },
99
+ ): Promise<Cart> => {
100
+ setError(null);
101
+ setIsLoading(true);
102
+
103
+ try {
104
+ const cartId = await getOrCreateCartId(options?.forceNewCart);
105
+ const cart = await cartClient.addItems(cartId, lines);
106
+ return cart;
107
+ } catch (err) {
108
+ if (isCartExpired(err) && !options?.forceNewCart) {
109
+ clearCartIdCookie();
110
+ return addItem(lines, { forceNewCart: true });
111
+ }
112
+
113
+ const message = err instanceof Error ? err.message : 'Failed to add to cart';
114
+ setError(message);
115
+ throw err;
116
+ } finally {
117
+ setIsLoading(false);
118
+ }
119
+ }, [cartClient, getOrCreateCartId]);
120
+
121
+ /**
122
+ * Update line items (quantity, attributes).
123
+ */
124
+ const updateItem = useCallback(async (
125
+ lines: CartLineUpdateInput[],
126
+ ): Promise<Cart> => {
127
+ setError(null);
128
+ setIsLoading(true);
129
+
130
+ try {
131
+ const cartId = getCartIdFromCookie();
132
+ if (!cartId) throw new Error('No cart found');
133
+ return await cartClient.updateItems(cartId, lines);
134
+ } catch (err) {
135
+ if (isCartExpired(err)) {
136
+ clearCartIdCookie();
137
+ }
138
+ const message = err instanceof Error ? err.message : 'Failed to update cart';
139
+ setError(message);
140
+ throw err;
141
+ } finally {
142
+ setIsLoading(false);
143
+ }
144
+ }, [cartClient]);
145
+
146
+ /**
147
+ * Remove items by line IDs.
148
+ */
149
+ const removeItem = useCallback(async (lineIds: string[]): Promise<Cart> => {
150
+ setError(null);
151
+ setIsLoading(true);
152
+
153
+ try {
154
+ const cartId = getCartIdFromCookie();
155
+ if (!cartId) throw new Error('No cart found');
156
+ return await cartClient.removeItems(cartId, lineIds);
157
+ } catch (err) {
158
+ if (isCartExpired(err)) {
159
+ clearCartIdCookie();
160
+ }
161
+ const message = err instanceof Error ? err.message : 'Failed to remove from cart';
162
+ setError(message);
163
+ throw err;
164
+ } finally {
165
+ setIsLoading(false);
166
+ }
167
+ }, [cartClient]);
168
+
169
+ /**
170
+ * Update discount codes (replaces all existing).
171
+ */
172
+ const updateDiscountCodes = useCallback(async (codes: string[]): Promise<Cart> => {
173
+ setError(null);
174
+ setIsLoading(true);
175
+
176
+ try {
177
+ const cartId = getCartIdFromCookie();
178
+ if (!cartId) throw new Error('No cart found');
179
+ return await cartClient.updateDiscountCodes(cartId, codes);
180
+ } catch (err) {
181
+ const message = err instanceof Error ? err.message : 'Failed to update discount codes';
182
+ setError(message);
183
+ throw err;
184
+ } finally {
185
+ setIsLoading(false);
186
+ }
187
+ }, [cartClient]);
188
+
189
+ /**
190
+ * Update cart note.
191
+ */
192
+ const updateNote = useCallback(async (note: string): Promise<Cart> => {
193
+ setError(null);
194
+ setIsLoading(true);
195
+
196
+ try {
197
+ const cartId = getCartIdFromCookie();
198
+ if (!cartId) throw new Error('No cart found');
199
+ return await cartClient.updateNote(cartId, note);
200
+ } catch (err) {
201
+ const message = err instanceof Error ? err.message : 'Failed to update note';
202
+ setError(message);
203
+ throw err;
204
+ } finally {
205
+ setIsLoading(false);
206
+ }
207
+ }, [cartClient]);
208
+
209
+ /**
210
+ * Clear cart — removes cookie.
211
+ */
212
+ const clearCart = useCallback(() => {
213
+ clearCartIdCookie();
214
+ }, []);
215
+
216
+ /**
217
+ * Get current cart ID from cookie (if exists).
218
+ */
219
+ const getCartId = useCallback((): string | null => {
220
+ return getCartIdFromCookie();
221
+ }, []);
222
+
223
+ return {
224
+ getCart,
225
+ addItem,
226
+ updateItem,
227
+ removeItem,
228
+ updateDiscountCodes,
229
+ updateNote,
230
+ clearCart,
231
+ getCartId,
232
+
233
+ isLoading,
234
+ error,
235
+ };
236
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * useCurrency — convenience hook for currency state.
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import { useCurrencyStore } from '../stores/store-context';
8
+
9
+ export function useCurrency() {
10
+ const currency = useCurrencyStore((s) => s.currency);
11
+ const baseCurrency = useCurrencyStore((s) => s.baseCurrency);
12
+ const supportedCurrencies = useCurrencyStore((s) => s.supportedCurrencies);
13
+ const setCurrency = useCurrencyStore((s) => s.setCurrency);
14
+ const isLoaded = useCurrencyStore((s) => s.isLoaded);
15
+
16
+ return {
17
+ currency,
18
+ baseCurrency,
19
+ supportedCurrencies,
20
+ setCurrency,
21
+ isLoaded,
22
+ };
23
+ }
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+
5
+ /**
6
+ * Debounce a value.
7
+ *
8
+ * @param value - Value to debounce
9
+ * @param delay - Delay in ms (default: 300)
10
+ * @returns Debounced value
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * const [query, setQuery] = useState("");
15
+ * const debouncedQuery = useDebouncedValue(query, 300);
16
+ * ```
17
+ */
18
+ export function useDebouncedValue<T>(value: T, delay = 300): T {
19
+ const [debouncedValue, setDebouncedValue] = useState(value);
20
+
21
+ useEffect(() => {
22
+ const timer = setTimeout(() => {
23
+ setDebouncedValue(value);
24
+ }, delay);
25
+
26
+ return () => clearTimeout(timer);
27
+ }, [value, delay]);
28
+
29
+ return debouncedValue;
30
+ }
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+
3
+ import { useSyncExternalStore } from 'react';
4
+
5
+ const emptySubscribe = () => () => {};
6
+
7
+ /**
8
+ * Returns false during SSR and first client render,
9
+ * then true after hydration completes.
10
+ *
11
+ * Use this to guard any content that depends on browser-only state
12
+ * (localStorage, cookies, window) to prevent hydration mismatches.
13
+ */
14
+ export function useHydrated() {
15
+ return useSyncExternalStore(
16
+ emptySubscribe,
17
+ () => true, // Client: true
18
+ () => false, // Server: false
19
+ );
20
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * useStorefrontClient — access StorefrontClient from context.
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import { useStorefrontClientContext } from '../providers/storefront-client-provider';
8
+ import type { StorefrontClient } from '../../core/client/types';
9
+
10
+ export function useStorefrontClient(): StorefrontClient {
11
+ return useStorefrontClientContext().client;
12
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @doswiftly/storefront-sdk/react — React adapter
3
+ *
4
+ * Provides React providers, Zustand stores, and hooks
5
+ * that wrap the framework-agnostic core.
6
+ *
7
+ * Peer dependencies: react ^18 || ^19, zustand ^5
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { StorefrontProvider, useAuth, useCartManager, useCurrency } from '@doswiftly/storefront-sdk/react';
12
+ * ```
13
+ */
14
+
15
+ // Providers
16
+ export { StorefrontProvider, type StorefrontProviderProps } from './providers/storefront-provider';
17
+ export { StorefrontClientProvider, type StorefrontClientProviderProps } from './providers/storefront-client-provider';
18
+ export { CurrencyProvider, type CurrencyProviderProps } from './providers/currency-provider';
19
+
20
+ // Hooks
21
+ export { useAuth, type UseAuthOptions, type LoginResult, type LogoutResult, type TokenRenewResult } from './hooks/use-auth';
22
+ export { useCartManager } from './hooks/use-cart-manager';
23
+ export { useStorefrontClient } from './hooks/use-storefront-client';
24
+ export { useCurrency } from './hooks/use-currency';
25
+
26
+ // Store hooks (Context-based)
27
+ export { useAuthStore, useAuthStoreApi, useAuthHydrated } from './stores/store-context';
28
+ export { useCurrencyStore, useCurrencyStoreApi } from './stores/store-context';
29
+
30
+ // Store types
31
+ export type { AuthStore, CustomerInfo } from './stores/auth.store';
32
+ export type { CurrencyStore, ShopCurrencyData } from './stores/currency.store';
33
+
34
+ // Selectors
35
+ export { selectCurrency, selectBaseCurrency, selectSupportedCurrencies, selectIsLoaded } from './stores/currency.store';
36
+
37
+ // Cookie utilities
38
+ export { getCookie, setCookie, deleteCookie, getCurrencyFromCookieAsync } from './cookies';
39
+
40
+ // Generic hooks
41
+ export { useHydrated } from './hooks/use-hydrated';
42
+ export { useDebouncedValue } from './hooks/use-debounced-value';
43
+
44
+ // Store context helper
45
+ export { createStoreContext } from './helpers/create-store-context';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * CurrencyProvider — initializes currency store from Shop data.
3
+ *
4
+ * Thin wrapper that calls useCurrencyStore.initialize() on mount
5
+ * with the shop's currency configuration from the server.
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React, { useEffect } from 'react';
11
+ import { useCurrencyStore } from '../stores/store-context';
12
+ import type { ShopCurrencyData } from '../stores/currency.store';
13
+
14
+ export interface CurrencyProviderProps {
15
+ children: React.ReactNode;
16
+ shopData: ShopCurrencyData;
17
+ }
18
+
19
+ export function CurrencyProvider({ children, shopData }: CurrencyProviderProps) {
20
+ const initialize = useCurrencyStore((s) => s.initialize);
21
+ const isLoaded = useCurrencyStore((s) => s.isLoaded);
22
+
23
+ useEffect(() => {
24
+ if (!isLoaded) {
25
+ initialize(shopData);
26
+ }
27
+ }, [initialize, isLoaded, shopData]);
28
+
29
+ return <>{children}</>;
30
+ }