@doswiftly/storefront-sdk 4.0.0 → 4.1.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 (109) hide show
  1. package/README.md +33 -6
  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/index.d.ts +7 -0
  24. package/dist/core/index.d.ts.map +1 -1
  25. package/dist/core/index.js +11 -0
  26. package/dist/core/language/cookie-config.d.ts +14 -0
  27. package/dist/core/language/cookie-config.d.ts.map +1 -0
  28. package/dist/core/language/cookie-config.js +13 -0
  29. package/dist/core/middleware/bot-protection.d.ts +71 -0
  30. package/dist/core/middleware/bot-protection.d.ts.map +1 -0
  31. package/dist/core/middleware/bot-protection.js +63 -0
  32. package/dist/core/middleware/currency.d.ts.map +1 -1
  33. package/dist/core/middleware/currency.js +2 -1
  34. package/dist/core/middleware/language.d.ts +18 -0
  35. package/dist/core/middleware/language.d.ts.map +1 -0
  36. package/dist/core/middleware/language.js +25 -0
  37. package/dist/react/bot-protection/bot-protection-context.d.ts +12 -0
  38. package/dist/react/bot-protection/bot-protection-context.d.ts.map +1 -0
  39. package/dist/react/bot-protection/bot-protection-context.js +9 -0
  40. package/dist/react/bot-protection/bot-protection-widget.d.ts +13 -0
  41. package/dist/react/bot-protection/bot-protection-widget.d.ts.map +1 -0
  42. package/dist/react/bot-protection/bot-protection-widget.js +34 -0
  43. package/dist/react/cookies.d.ts +17 -0
  44. package/dist/react/cookies.d.ts.map +1 -1
  45. package/dist/react/cookies.js +36 -3
  46. package/dist/react/hooks/use-bot-protection.d.ts +16 -0
  47. package/dist/react/hooks/use-bot-protection.d.ts.map +1 -0
  48. package/dist/react/hooks/use-bot-protection.js +24 -0
  49. package/dist/react/index.d.ts +10 -1
  50. package/dist/react/index.d.ts.map +1 -1
  51. package/dist/react/index.js +9 -1
  52. package/dist/react/providers/language-provider.d.ts +18 -0
  53. package/dist/react/providers/language-provider.d.ts.map +1 -0
  54. package/dist/react/providers/language-provider.js +24 -0
  55. package/dist/react/providers/storefront-client-provider.d.ts +7 -2
  56. package/dist/react/providers/storefront-client-provider.d.ts.map +1 -1
  57. package/dist/react/providers/storefront-client-provider.js +14 -3
  58. package/dist/react/providers/storefront-provider.d.ts +7 -1
  59. package/dist/react/providers/storefront-provider.d.ts.map +1 -1
  60. package/dist/react/providers/storefront-provider.js +11 -4
  61. package/dist/react/stores/cart.context.d.ts +12 -0
  62. package/dist/react/stores/cart.context.d.ts.map +1 -0
  63. package/dist/react/stores/cart.context.js +3 -0
  64. package/dist/react/stores/cart.store.d.ts +71 -0
  65. package/dist/react/stores/cart.store.d.ts.map +1 -0
  66. package/dist/react/stores/cart.store.js +166 -0
  67. package/dist/react/stores/currency.store.d.ts +6 -9
  68. package/dist/react/stores/currency.store.d.ts.map +1 -1
  69. package/dist/react/stores/currency.store.js +5 -22
  70. package/dist/react/stores/language.store.d.ts +33 -0
  71. package/dist/react/stores/language.store.d.ts.map +1 -0
  72. package/dist/react/stores/language.store.js +67 -0
  73. package/dist/react/stores/store-context.d.ts +5 -0
  74. package/dist/react/stores/store-context.d.ts.map +1 -1
  75. package/dist/react/stores/store-context.js +14 -0
  76. package/dist/react/types/shop-config.d.ts +19 -0
  77. package/dist/react/types/shop-config.d.ts.map +1 -0
  78. package/dist/react/types/shop-config.js +7 -0
  79. package/package.json +1 -1
  80. package/src/__tests__/unit/bot-protection.test.ts +461 -0
  81. package/src/__tests__/unit/cart-store.test.ts +349 -0
  82. package/src/core/bot-protection/abstract-manager.ts +185 -0
  83. package/src/core/bot-protection/create-manager.ts +37 -0
  84. package/src/core/bot-protection/eucaptcha-manager.ts +88 -0
  85. package/src/core/bot-protection/fallback-manager.ts +43 -0
  86. package/src/core/bot-protection/turnstile-manager.ts +92 -0
  87. package/src/core/bot-protection/types/eucaptcha.d.ts +28 -0
  88. package/src/core/bot-protection/types/turnstile.d.ts +33 -0
  89. package/src/core/cart/cookie-config.ts +13 -0
  90. package/src/core/currency/cookie-config.ts +13 -0
  91. package/src/core/index.ts +23 -0
  92. package/src/core/language/cookie-config.ts +13 -0
  93. package/src/core/middleware/bot-protection.ts +140 -0
  94. package/src/core/middleware/currency.ts +2 -1
  95. package/src/core/middleware/language.ts +30 -0
  96. package/src/react/bot-protection/bot-protection-context.ts +17 -0
  97. package/src/react/bot-protection/bot-protection-widget.tsx +46 -0
  98. package/src/react/cookies.ts +39 -4
  99. package/src/react/hooks/use-bot-protection.ts +31 -0
  100. package/src/react/index.ts +27 -1
  101. package/src/react/providers/language-provider.tsx +34 -0
  102. package/src/react/providers/storefront-client-provider.tsx +20 -3
  103. package/src/react/providers/storefront-provider.tsx +34 -6
  104. package/src/react/stores/cart.context.ts +10 -0
  105. package/src/react/stores/cart.store.ts +254 -0
  106. package/src/react/stores/currency.store.ts +12 -32
  107. package/src/react/stores/language.store.ts +90 -0
  108. package/src/react/stores/store-context.tsx +21 -0
  109. package/src/react/types/shop-config.ts +22 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * TurnstileManager — Cloudflare Turnstile bot protection provider.
3
+ *
4
+ * Implements BotProtectionTokenProvider via AbstractBotProtectionManager.
5
+ * Invisible widget, execution-mode challenge (no user interaction).
6
+ */
7
+
8
+ /// <reference path="./types/turnstile.d.ts" />
9
+ import { AbstractBotProtectionManager } from './abstract-manager';
10
+
11
+ export class TurnstileManager extends AbstractBotProtectionManager {
12
+ private callbackName = `onloadTurnstileCallback_${Math.random().toString(36).slice(2)}`;
13
+
14
+ protected _waitForApi(): Promise<void> {
15
+ if (typeof window !== 'undefined' && window.turnstile) {
16
+ return Promise.resolve();
17
+ }
18
+
19
+ return new Promise<void>((resolve) => {
20
+ const check = setInterval(() => {
21
+ if (typeof window !== 'undefined' && window.turnstile) {
22
+ clearInterval(check);
23
+ resolve();
24
+ }
25
+ }, 50);
26
+
27
+ // Safety timeout — don't poll forever
28
+ setTimeout(() => {
29
+ clearInterval(check);
30
+ resolve(); // Resolve anyway — execute will fail gracefully
31
+ }, 15000);
32
+ });
33
+ }
34
+
35
+ protected _renderWidget(container: HTMLElement, action?: string): string {
36
+ if (!window.turnstile) {
37
+ throw new Error('Turnstile API not loaded');
38
+ }
39
+
40
+ return window.turnstile.render(container, {
41
+ sitekey: this.siteKey,
42
+ size: 'invisible',
43
+ execution: 'execute',
44
+ action,
45
+ });
46
+ }
47
+
48
+ protected _executeChallenge(widgetId: string, action?: string): Promise<string | null> {
49
+ return new Promise<string | null>((resolve) => {
50
+ if (!window.turnstile) {
51
+ resolve(null);
52
+ return;
53
+ }
54
+
55
+ // Reset to get a fresh token (tokens are single-use)
56
+ window.turnstile.reset(widgetId);
57
+
58
+ // Remove old widget and re-render with callback
59
+ window.turnstile.remove(widgetId);
60
+
61
+ if (!this.container) {
62
+ resolve(null);
63
+ return;
64
+ }
65
+
66
+ this.widgetId = window.turnstile.render(this.container, {
67
+ sitekey: this.siteKey,
68
+ size: 'invisible',
69
+ execution: 'execute',
70
+ action,
71
+ callback: (token: string) => {
72
+ resolve(token);
73
+ },
74
+ 'error-callback': () => {
75
+ resolve(null);
76
+ },
77
+ 'expired-callback': () => {
78
+ resolve(null);
79
+ },
80
+ });
81
+
82
+ // Trigger execution
83
+ window.turnstile.execute(this.widgetId, { action });
84
+ });
85
+ }
86
+
87
+ protected _destroyWidget(widgetId: string): void {
88
+ if (window.turnstile) {
89
+ window.turnstile.remove(widgetId);
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * EU CAPTCHA (Myra Security) global type declarations.
3
+ */
4
+
5
+ interface EuCaptchaRenderOptions {
6
+ sitekey: string;
7
+ callback?: (token: string) => void;
8
+ 'error-callback'?: (error: unknown) => void;
9
+ 'expired-callback'?: () => void;
10
+ size?: 'normal' | 'compact' | 'invisible';
11
+ action?: string;
12
+ }
13
+
14
+ interface EuCaptchaApi {
15
+ render(container: string | HTMLElement, options: EuCaptchaRenderOptions): string;
16
+ execute(widgetId: string): void;
17
+ reset(widgetId: string): void;
18
+ getResponse(widgetId: string): string | undefined;
19
+ }
20
+
21
+ declare global {
22
+ interface Window {
23
+ eucaptcha?: EuCaptchaApi;
24
+ onloadEuCaptchaCallback?: () => void;
25
+ }
26
+ }
27
+
28
+ export {};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Cloudflare Turnstile global type declarations.
3
+ */
4
+
5
+ interface TurnstileRenderOptions {
6
+ sitekey: string;
7
+ callback?: (token: string) => void;
8
+ 'error-callback'?: (error: unknown) => void;
9
+ 'expired-callback'?: () => void;
10
+ size?: 'normal' | 'compact' | 'invisible';
11
+ theme?: 'light' | 'dark' | 'auto';
12
+ execution?: 'render' | 'execute';
13
+ action?: string;
14
+ cData?: string;
15
+ }
16
+
17
+ interface TurnstileApi {
18
+ render(container: string | HTMLElement, options: TurnstileRenderOptions): string;
19
+ execute(widgetId: string, options?: { action?: string }): void;
20
+ reset(widgetId: string): void;
21
+ remove(widgetId: string): void;
22
+ getResponse(widgetId: string): string | undefined;
23
+ isExpired(widgetId: string): boolean;
24
+ }
25
+
26
+ declare global {
27
+ interface Window {
28
+ turnstile?: TurnstileApi;
29
+ onloadTurnstileCallback?: () => void;
30
+ }
31
+ }
32
+
33
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Cart cookie configuration — platform contract.
3
+ *
4
+ * Used by:
5
+ * - SDK cart store (client-side cookie read/write for cartId)
6
+ * - Server-side cart prefetching (SSR cart badge, middleware)
7
+ * - proxy.ts (edge cart ID detection)
8
+ *
9
+ * Single cookie for cart ID persistence. Value is a plain cart ID string
10
+ * (not JSON) — server can read it directly via cookies().get('cart-id').
11
+ */
12
+ export const CART_COOKIE_NAME = 'cart-id';
13
+ export const CART_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Currency configuration constants — platform contract.
3
+ *
4
+ * Used by:
5
+ * - SDK currency store (client-side cookie read/write)
6
+ * - SDK currency middleware (X-Preferred-Currency header)
7
+ * - Server-side currency detection (SSR helpers)
8
+ *
9
+ * Single cookie for preferred currency persistence.
10
+ */
11
+ export const CURRENCY_COOKIE_NAME = 'preferred-currency';
12
+ export const CURRENCY_COOKIE_MAX_AGE = 365 * 24 * 60 * 60; // 1 year
13
+ export const CURRENCY_HEADER_NAME = 'X-Preferred-Currency';
package/src/core/index.ts CHANGED
@@ -55,10 +55,24 @@ export type {
55
55
  // Middleware
56
56
  export { authMiddleware } from './middleware/auth';
57
57
  export { currencyMiddleware } from './middleware/currency';
58
+ export { languageMiddleware } from './middleware/language';
59
+ export {
60
+ botProtectionMiddleware,
61
+ BOT_PROTECTION_HEADER,
62
+ type BotProtectionTokenProvider,
63
+ type BotProtectionConfig,
64
+ type BotProtectionProviderConfig,
65
+ type BotProtectionMiddlewareOptions,
66
+ type FailStrategy,
67
+ } from './middleware/bot-protection';
58
68
  export { retryMiddleware, type RetryOptions } from './middleware/retry';
59
69
  export { timeoutMiddleware, type TimeoutOptions } from './middleware/timeout';
60
70
  export { errorMiddleware } from './middleware/errors';
61
71
 
72
+ // Bot protection managers (framework-agnostic, DOM APIs)
73
+ export { createBotProtectionManager } from './bot-protection/create-manager';
74
+ export { FallbackBotProtectionManager } from './bot-protection/fallback-manager';
75
+
62
76
  // Errors
63
77
  export { StorefrontError, ErrorCodes, type StorefrontErrorOptions } from './errors';
64
78
 
@@ -134,6 +148,15 @@ export {
134
148
  type AuthCookieConfig,
135
149
  } from './auth/cookie-config';
136
150
 
151
+ // Language config
152
+ export { LANGUAGE_COOKIE_NAME, LANGUAGE_COOKIE_MAX_AGE, LANGUAGE_HEADER_NAME } from './language/cookie-config';
153
+
154
+ // Currency config
155
+ export { CURRENCY_COOKIE_NAME, CURRENCY_COOKIE_MAX_AGE, CURRENCY_HEADER_NAME } from './currency/cookie-config';
156
+
157
+ // Cart cookie config
158
+ export { CART_COOKIE_NAME, CART_COOKIE_MAX_AGE } from './cart/cookie-config';
159
+
137
160
  // Auth route matching
138
161
  export { matchesRoute, type RouteProtectionConfig } from './auth/routes';
139
162
 
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Language configuration constants — platform contract.
3
+ *
4
+ * Used by:
5
+ * - SDK language store (client-side cookie read/write)
6
+ * - SDK language middleware (X-Lang header)
7
+ * - next-intl middleware `localeCookie.name` (server-side locale detection)
8
+ *
9
+ * Single cookie replaces both next-intl's NEXT_LOCALE and SDK's preferred-language.
10
+ */
11
+ export const LANGUAGE_COOKIE_NAME = 'preferred-language';
12
+ export const LANGUAGE_COOKIE_MAX_AGE = 365 * 24 * 60 * 60; // 1 year
13
+ export const LANGUAGE_HEADER_NAME = 'X-Lang';
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Bot protection middleware — vendor-agnostic token injection.
3
+ *
4
+ * Intercepts protected mutations and adds a bot verification token
5
+ * via the X-Bot-Protection-Token header. Token acquisition is delegated
6
+ * to a BotProtectionTokenProvider (EU CAPTCHA, Turnstile, etc.).
7
+ *
8
+ * Fail strategy is per-operation: 'open' = continue without token,
9
+ * 'closed' = throw if token unavailable.
10
+ */
11
+
12
+ import type { Middleware } from '../client/types';
13
+ import { StorefrontError } from '../errors';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Token provider interface (implemented by managers)
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** Vendor-agnostic token provider interface */
20
+ export interface BotProtectionTokenProvider {
21
+ /**
22
+ * Get a fresh bot protection token.
23
+ * Handles: in-flight deduplication, timeout, singleton script loading.
24
+ *
25
+ * @param options.action - Operation name (e.g. 'CustomerCreate') for action validation
26
+ * @param options.timeoutMs - Timeout in ms (default: 10000)
27
+ * @returns Token string or null if unavailable (adblock, timeout, error)
28
+ */
29
+ execute(options?: { action?: string; timeoutMs?: number }): Promise<string | null>;
30
+
31
+ /** Cleanup widget and script resources */
32
+ destroy(): void;
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Configuration types
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /** Single provider config (from shop query) */
40
+ export interface BotProtectionProviderConfig {
41
+ /** Provider identifier: 'eucaptcha' | 'turnstile' | 'recaptcha' | extensible */
42
+ provider: string;
43
+ /** Site key for the bot protection widget */
44
+ siteKey: string;
45
+ /** Script URL — configurable for region-specific / enterprise overrides */
46
+ scriptUrl: string;
47
+ }
48
+
49
+ /** Bot protection configuration from shop query */
50
+ export interface BotProtectionConfig {
51
+ /** Primary provider */
52
+ primary: BotProtectionProviderConfig;
53
+ /** Fallback provider (runtime: if primary fails -> fallback -> fail-open) */
54
+ fallback?: BotProtectionProviderConfig | null;
55
+ /** GraphQL operation names requiring verification */
56
+ protectedOperations: string[];
57
+ }
58
+
59
+ /** Fail strategy per request */
60
+ export type FailStrategy = 'open' | 'closed';
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Middleware options
64
+ // ---------------------------------------------------------------------------
65
+
66
+ export interface BotProtectionMiddlewareOptions {
67
+ /** Token provider instance (manager or fallback chain) */
68
+ tokenProvider: BotProtectionTokenProvider;
69
+ /** GraphQL operation names that require bot protection */
70
+ protectedOperations: string[];
71
+ /** Default fail strategy when token acquisition fails (default: 'open') */
72
+ defaultFailStrategy?: FailStrategy;
73
+ /** Override fail strategy per operation (e.g. CheckoutComplete = closed) */
74
+ failStrategyOverrides?: Record<string, FailStrategy>;
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Constants
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /** Header name for bot protection token */
82
+ export const BOT_PROTECTION_HEADER = 'X-Bot-Protection-Token';
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Middleware factory
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /**
89
+ * Creates bot protection middleware for the SDK pipeline.
90
+ *
91
+ * Only intercepts mutations whose operationName is in protectedOperations.
92
+ * Token is fetched fresh on every call (single-use tokens, retry-safe).
93
+ *
94
+ * Pipeline position: AFTER auth/currency, BEFORE retry/timeout/errors.
95
+ * Retry middleware re-executes the full chain, so each attempt gets a fresh token.
96
+ */
97
+ export function botProtectionMiddleware(options: BotProtectionMiddlewareOptions): Middleware {
98
+ const {
99
+ tokenProvider,
100
+ protectedOperations,
101
+ defaultFailStrategy = 'open',
102
+ failStrategyOverrides = {},
103
+ } = options;
104
+
105
+ const protectedSet = new Set(protectedOperations);
106
+
107
+ return async (request, next) => {
108
+ // Only intercept mutations
109
+ if (!request.isMutation) return next(request);
110
+
111
+ // Only intercept protected operations
112
+ const opName = request.operationName;
113
+ if (!opName || !protectedSet.has(opName)) {
114
+ return next(request);
115
+ }
116
+
117
+ // Fetch fresh token — action param enables backend action validation
118
+ const token = await tokenProvider.execute({
119
+ action: opName,
120
+ timeoutMs: 10000,
121
+ });
122
+
123
+ if (token) {
124
+ request.headers[BOT_PROTECTION_HEADER] = token;
125
+ } else {
126
+ // Fail strategy: open = continue without token, closed = throw
127
+ const strategy = failStrategyOverrides[opName] ?? defaultFailStrategy;
128
+ if (strategy === 'closed') {
129
+ throw new StorefrontError({
130
+ code: 'BOT_PROTECTION_REQUIRED',
131
+ message: 'Bot protection verification failed. Please try again.',
132
+ status: 0,
133
+ });
134
+ }
135
+ // fail-open: continue without token — backend guard may still reject
136
+ }
137
+
138
+ return next(request);
139
+ };
140
+ }
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import type { Middleware } from '../client/types';
15
+ import { CURRENCY_HEADER_NAME } from '../currency/cookie-config';
15
16
 
16
17
  export function currencyMiddleware(
17
18
  getCurrency: () => string | null | undefined,
@@ -19,7 +20,7 @@ export function currencyMiddleware(
19
20
  return (request, next) => {
20
21
  const currency = getCurrency();
21
22
  if (currency) {
22
- request.headers['X-Preferred-Currency'] = currency;
23
+ request.headers[CURRENCY_HEADER_NAME] = currency;
23
24
  }
24
25
  return next(request);
25
26
  };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Language middleware — adds X-Lang header.
3
+ *
4
+ * Language is resolved lazily so language switches are reflected immediately.
5
+ * CRITICAL: does NOT send header when language=null (store not yet initialized)
6
+ * — without this, backend returns default language, React Query caches it,
7
+ * and after setLanguage() the cache is stale.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { languageMiddleware } from '@doswiftly/storefront-sdk';
12
+ *
13
+ * client.use(languageMiddleware(() => languageStore.getState().language));
14
+ * ```
15
+ */
16
+
17
+ import type { Middleware } from '../client/types';
18
+ import { LANGUAGE_HEADER_NAME } from '../language/cookie-config';
19
+
20
+ export function languageMiddleware(
21
+ getLanguage: () => string | null | undefined,
22
+ ): Middleware {
23
+ return (request, next) => {
24
+ const language = getLanguage();
25
+ if (language) {
26
+ request.headers[LANGUAGE_HEADER_NAME] = language;
27
+ }
28
+ return next(request);
29
+ };
30
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Bot protection React context.
3
+ *
4
+ * Provides access to the BotProtectionTokenProvider manager
5
+ * for the useBotProtection() escape hatch hook.
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import { createContext } from 'react';
11
+ import type { BotProtectionTokenProvider } from '../../core/middleware/bot-protection';
12
+
13
+ export interface BotProtectionContextValue {
14
+ manager: BotProtectionTokenProvider | null;
15
+ }
16
+
17
+ export const BotProtectionContext = createContext<BotProtectionContextValue | null>(null);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * BotProtectionWidget — invisible React wrapper that mounts the bot protection manager.
3
+ *
4
+ * Renders a hidden div and mounts/unmounts the manager lifecycle.
5
+ * Triggers lazy script preload on mount.
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React, { useEffect, useRef } from 'react';
11
+ import type { BotProtectionTokenProvider } from '../../core/middleware/bot-protection';
12
+ import type { AbstractBotProtectionManager } from '../../core/bot-protection/abstract-manager';
13
+
14
+ interface BotProtectionWidgetProps {
15
+ manager: BotProtectionTokenProvider | null;
16
+ }
17
+
18
+ export function BotProtectionWidget({ manager }: BotProtectionWidgetProps) {
19
+ const containerRef = useRef<HTMLDivElement>(null);
20
+ const mountedRef = useRef(false);
21
+
22
+ useEffect(() => {
23
+ if (!manager || !containerRef.current || mountedRef.current) return;
24
+
25
+ mountedRef.current = true;
26
+
27
+ // Trigger lazy script preload + mount container
28
+ const mgr = manager as AbstractBotProtectionManager;
29
+ if (typeof mgr.loadScript === 'function') {
30
+ mgr.loadScript().catch(() => {
31
+ // Script load failure is handled gracefully in execute()
32
+ });
33
+ }
34
+ if (typeof mgr.mount === 'function') {
35
+ mgr.mount(containerRef.current);
36
+ }
37
+
38
+ return () => {
39
+ mountedRef.current = false;
40
+ };
41
+ }, [manager]);
42
+
43
+ if (!manager) return null;
44
+
45
+ return <div ref={containerRef} style={{ display: 'none' }} aria-hidden="true" />;
46
+ }
@@ -16,15 +16,20 @@ export function getCookie(name: string): string | null {
16
16
 
17
17
  /**
18
18
  * Set cookie (client-side only).
19
+ *
20
+ * `secure` defaults to auto-detect from `location.protocol` (true on HTTPS).
19
21
  */
20
22
  export function setCookie(
21
23
  name: string,
22
24
  value: string,
23
- options: { maxAge?: number; path?: string; sameSite?: string } = {},
25
+ options: { maxAge?: number; path?: string; sameSite?: string; secure?: boolean } = {},
24
26
  ): void {
25
27
  if (typeof document === 'undefined') return;
26
28
  const { maxAge = 365 * 24 * 60 * 60, path = '/', sameSite = 'lax' } = options;
27
- document.cookie = `${name}=${encodeURIComponent(value)};max-age=${maxAge};path=${path};samesite=${sameSite}`;
29
+ const secure = options.secure ??
30
+ (typeof location !== 'undefined' && location.protocol === 'https:');
31
+ const securePart = secure ? ';secure' : '';
32
+ document.cookie = `${name}=${encodeURIComponent(value)};max-age=${maxAge};path=${path};samesite=${sameSite}${securePart}`;
28
33
  }
29
34
 
30
35
  /**
@@ -35,6 +40,9 @@ export function deleteCookie(name: string, path = '/'): void {
35
40
  document.cookie = `${name}=;max-age=0;path=${path}`;
36
41
  }
37
42
 
43
+ import { CURRENCY_COOKIE_NAME } from '../core/currency/cookie-config';
44
+ import { CART_COOKIE_NAME } from '../core/cart/cookie-config';
45
+
38
46
  /**
39
47
  * Get preferred currency from cookie (async — works with Next.js cookies()).
40
48
  * Falls back to document.cookie on client.
@@ -44,11 +52,38 @@ export async function getCurrencyFromCookieAsync(): Promise<string | null> {
44
52
  try {
45
53
  const { cookies } = await import('next/headers');
46
54
  const cookieStore = await cookies();
47
- return cookieStore.get('preferred-currency')?.value ?? null;
55
+ return cookieStore.get(CURRENCY_COOKIE_NAME)?.value ?? null;
56
+ } catch {
57
+ // Not in a server context or next not available
58
+ }
59
+
60
+ // Client-side fallback
61
+ return getCookie(CURRENCY_COOKIE_NAME);
62
+ }
63
+
64
+ /**
65
+ * Get cart ID from cookie (async — works with Next.js cookies()).
66
+ * Falls back to document.cookie on client.
67
+ *
68
+ * Use in Server Components for SSR cart badge:
69
+ * ```typescript
70
+ * const cartId = await getCartIdFromCookieAsync();
71
+ * if (cartId) {
72
+ * const cart = await fetchCart(cartId);
73
+ * // Render cart badge with real totalQuantity — no skeleton needed
74
+ * }
75
+ * ```
76
+ */
77
+ export async function getCartIdFromCookieAsync(): Promise<string | null> {
78
+ // Server-side: try Next.js cookies()
79
+ try {
80
+ const { cookies } = await import('next/headers');
81
+ const cookieStore = await cookies();
82
+ return cookieStore.get(CART_COOKIE_NAME)?.value ?? null;
48
83
  } catch {
49
84
  // Not in a server context or next not available
50
85
  }
51
86
 
52
87
  // Client-side fallback
53
- return getCookie('preferred-currency');
88
+ return getCookie(CART_COOKIE_NAME);
54
89
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * useBotProtection — escape hatch hook for custom bot protection use cases.
3
+ *
4
+ * Normally the middleware handles token injection automatically.
5
+ * Use this hook only when you need manual control (e.g. custom forms).
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import { useContext, useCallback } from 'react';
11
+ import { BotProtectionContext } from '../bot-protection/bot-protection-context';
12
+
13
+ export function useBotProtection() {
14
+ const ctx = useContext(BotProtectionContext);
15
+ const manager = ctx?.manager ?? null;
16
+
17
+ const execute = useCallback(
18
+ (options?: { action?: string; timeoutMs?: number }) => {
19
+ if (!manager) return Promise.resolve(null);
20
+ return manager.execute(options);
21
+ },
22
+ [manager],
23
+ );
24
+
25
+ return {
26
+ /** Execute a bot protection challenge and get a token */
27
+ execute,
28
+ /** Whether bot protection is configured and available */
29
+ isAvailable: !!manager,
30
+ };
31
+ }
@@ -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';