create-brainerce-store 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/index.js +1 -1
  2. package/messages/en.json +9 -1
  3. package/messages/he.json +9 -1
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/src/app/account/page.tsx +8 -4
  6. package/templates/nextjs/base/src/app/auth/callback/page.tsx +90 -90
  7. package/templates/nextjs/base/src/app/cart/page.tsx +110 -110
  8. package/templates/nextjs/base/src/app/checkout/page.tsx +614 -614
  9. package/templates/nextjs/base/src/app/login/page.tsx +58 -58
  10. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +193 -193
  11. package/templates/nextjs/base/src/app/page.tsx +98 -98
  12. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +435 -435
  13. package/templates/nextjs/base/src/app/products/page.tsx +246 -246
  14. package/templates/nextjs/base/src/app/register/page.tsx +68 -68
  15. package/templates/nextjs/base/src/app/verify-email/page.tsx +293 -293
  16. package/templates/nextjs/base/src/components/account/order-history.tsx +198 -198
  17. package/templates/nextjs/base/src/components/account/profile-section.tsx +189 -40
  18. package/templates/nextjs/base/src/components/auth/login-form.tsx +94 -94
  19. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
  20. package/templates/nextjs/base/src/components/auth/register-form.tsx +184 -184
  21. package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
  22. package/templates/nextjs/base/src/components/cart/cart-summary.tsx +70 -70
  23. package/templates/nextjs/base/src/components/cart/coupon-input.tsx +134 -134
  24. package/templates/nextjs/base/src/components/cart/reservation-countdown.tsx +103 -103
  25. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +305 -305
  26. package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +64 -64
  27. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +350 -344
  28. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +199 -199
  29. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +110 -110
  30. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +65 -65
  31. package/templates/nextjs/base/src/components/layout/footer.tsx +38 -38
  32. package/templates/nextjs/base/src/components/layout/header.tsx +332 -332
  33. package/templates/nextjs/base/src/components/products/product-card.tsx +96 -96
  34. package/templates/nextjs/base/src/components/products/product-grid.tsx +35 -35
  35. package/templates/nextjs/base/src/components/shared/loading-spinner.tsx +32 -32
  36. package/templates/nextjs/base/src/lib/translations.ts +11 -11
  37. package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +5 -1
@@ -1,344 +1,350 @@
1
- 'use client';
2
-
3
- import { useEffect, useState, useRef, useCallback } from 'react';
4
- import type { PaymentIntent } from 'brainerce';
5
- import { getClient } from '@/lib/brainerce';
6
- import { useTranslations } from '@/lib/translations';
7
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
8
- import { cn } from '@/lib/utils';
9
-
10
- declare global {
11
- interface Window {
12
- growPayment?: {
13
- init: (config: {
14
- environment: string;
15
- version: number;
16
- events: {
17
- onSuccess?: (response: unknown) => void;
18
- onFailure?: (response: unknown) => void;
19
- onError?: (response: unknown) => void;
20
- onTimeout?: (response: unknown) => void;
21
- onWalletChange?: (state: string) => void;
22
- onPaymentStart?: (response: unknown) => void;
23
- onPaymentCancel?: (response: unknown) => void;
24
- };
25
- }) => void;
26
- renderPaymentOptions: (authCode: string) => void;
27
- };
28
- }
29
- }
30
-
31
- const GROW_SDK_URL = 'https://cdn.meshulam.co.il/sdk/gs.min.js';
32
- const APPLE_PAY_SDK_URL = 'https://meshulam.co.il/_media/js/apple_pay_sdk/sdk.min.js';
33
-
34
- interface PaymentStepProps {
35
- checkoutId: string;
36
- className?: string;
37
- }
38
-
39
- /**
40
- * Load a script tag if not already present in the DOM.
41
- * Resolves when the script loads (or immediately if already present).
42
- */
43
- function loadScript(src: string, optional = false): Promise<void> {
44
- return new Promise((resolve) => {
45
- if (document.querySelector(`script[src="${src}"]`)) {
46
- resolve();
47
- return;
48
- }
49
- const script = document.createElement('script');
50
- script.src = src;
51
- script.async = true;
52
- script.onload = () => resolve();
53
- script.onerror = () => {
54
- if (optional) {
55
- resolve(); // Non-blocking for optional SDKs
56
- } else {
57
- resolve(); // Still resolve caller handles missing global
58
- }
59
- };
60
- document.head.appendChild(script);
61
- });
62
- }
63
-
64
- /**
65
- * Wait for window.growPayment to become available (set by the SDK script).
66
- */
67
- function waitForGrowGlobal(timeoutMs = 5000): Promise<boolean> {
68
- return new Promise((resolve) => {
69
- if (window.growPayment) {
70
- resolve(true);
71
- return;
72
- }
73
- const start = Date.now();
74
- const check = setInterval(() => {
75
- if (window.growPayment) {
76
- clearInterval(check);
77
- resolve(true);
78
- } else if (Date.now() - start > timeoutMs) {
79
- clearInterval(check);
80
- resolve(false);
81
- }
82
- }, 50);
83
- });
84
- }
85
-
86
- export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
87
- const t = useTranslations('checkout');
88
- const [paymentIntent, setPaymentIntent] = useState<PaymentIntent | null>(null);
89
- const [loading, setLoading] = useState(true);
90
- const [error, setError] = useState<string | null>(null);
91
- const [growReady, setGrowReady] = useState(false);
92
- const [sdkScriptLoaded, setSdkScriptLoaded] = useState(false);
93
- const renderAttempted = useRef(false);
94
-
95
- const handleGrowSuccess = useCallback(
96
- async (response: unknown) => {
97
- console.info('Grow payment success:', response);
98
- try {
99
- // Notify backend to process the order
100
- // Pass full SDK response data for payment metadata storage (transactionId, transactionToken, etc.)
101
- const client = getClient();
102
- const data = (response as { data?: Record<string, unknown> })?.data;
103
- await client.confirmSdkPayment(checkoutId, data || undefined);
104
- } catch (err) {
105
- console.warn('Failed to confirm payment with backend:', err);
106
- }
107
- window.location.href = `/order-confirmation?checkout_id=${checkoutId}`;
108
- },
109
- [checkoutId]
110
- );
111
-
112
- const handleGrowFailure = useCallback((response: unknown) => {
113
- console.error('Grow payment failure:', response);
114
- const msg = (response as { message?: string })?.message || t('paymentError');
115
- setError(msg);
116
- }, []);
117
-
118
- const handleGrowError = useCallback((response: unknown) => {
119
- console.error('Grow payment error:', response);
120
- const msg = (response as { message?: string })?.message || t('paymentError');
121
- setError(msg);
122
- }, []);
123
-
124
- // Step 1: Load SDK scripts (Apple Pay + Grow) just load, don't init yet
125
- useEffect(() => {
126
- let cancelled = false;
127
-
128
- async function loadSdkScripts() {
129
- // Load Apple Pay SDK first (optional needed for Apple Pay in Grow wallet)
130
- await loadScript(APPLE_PAY_SDK_URL, true);
131
-
132
- // Load Grow SDK script
133
- await loadScript(GROW_SDK_URL);
134
-
135
- // Wait for growPayment global to be set by the script
136
- const available = await waitForGrowGlobal();
137
-
138
- if (cancelled) return;
139
-
140
- if (!available) {
141
- setError(t('failedToLoadPaymentSdk'));
142
- return;
143
- }
144
-
145
- setSdkScriptLoaded(true);
146
- }
147
-
148
- loadSdkScripts();
149
-
150
- return () => {
151
- cancelled = true;
152
- };
153
- }, []);
154
-
155
- // Step 2: Create payment intent (API call)
156
- // Use ref to prevent double-creation in React StrictMode
157
- const intentCreated = useRef(false);
158
- useEffect(() => {
159
- if (intentCreated.current) return;
160
- intentCreated.current = true;
161
-
162
- async function createIntent() {
163
- try {
164
- setLoading(true);
165
- setError(null);
166
- const client = getClient();
167
-
168
- const successUrl = `${window.location.origin}/order-confirmation?checkout_id=${checkoutId}`;
169
- const cancelUrl = `${window.location.origin}/checkout?checkout_id=${checkoutId}&canceled=true`;
170
-
171
- const intent = await client.createPaymentIntent(checkoutId, {
172
- successUrl,
173
- cancelUrl,
174
- });
175
-
176
- setPaymentIntent(intent);
177
-
178
- // Auto-redirect for Stripe
179
- if (intent.provider === 'stripe') {
180
- window.location.href = intent.clientSecret;
181
- }
182
- } catch (err) {
183
- const message = err instanceof Error ? err.message : t('paymentError');
184
- setError(message);
185
- } finally {
186
- setLoading(false);
187
- }
188
- }
189
-
190
- createIntent();
191
- }, [checkoutId]);
192
-
193
- // Step 3: When BOTH SDK script is loaded AND payment intent is ready:
194
- // - init() with the CORRECT environment from the payment intent
195
- // - wait for onWalletChange signal before calling renderPaymentOptions()
196
- useEffect(() => {
197
- if (!sdkScriptLoaded) return;
198
- if (!paymentIntent || paymentIntent.provider !== 'grow') return;
199
- if (!window.growPayment) return;
200
- if (renderAttempted.current) return;
201
-
202
- renderAttempted.current = true;
203
-
204
- // Parse environment and authCode from clientSecret (format: "ENV|authCode")
205
- const pipeIndex = paymentIntent.clientSecret.indexOf('|');
206
- const env = pipeIndex !== -1 ? paymentIntent.clientSecret.substring(0, pipeIndex) : 'DEV';
207
- const authCode =
208
- pipeIndex !== -1
209
- ? paymentIntent.clientSecret.substring(pipeIndex + 1)
210
- : paymentIntent.clientSecret;
211
-
212
- let rendered = false;
213
- let timeoutId: ReturnType<typeof setTimeout>;
214
- let pollId: ReturnType<typeof setInterval>;
215
-
216
- function tryRenderPaymentOptions() {
217
- if (rendered) return;
218
- try {
219
- window.growPayment?.renderPaymentOptions(authCode);
220
- rendered = true;
221
- clearTimeout(timeoutId);
222
- clearInterval(pollId);
223
- setGrowReady(true);
224
- console.info('Grow SDK: renderPaymentOptions succeeded');
225
- } catch (err) {
226
- console.info('Grow SDK: renderPaymentOptions not ready yet', err);
227
- }
228
- }
229
-
230
- // Init SDK with the correct environment (must match the authCode's origin)
231
- console.info('Grow SDK: init with environment:', env);
232
- window.growPayment.init({
233
- environment: env,
234
- version: 1,
235
- events: {
236
- onSuccess: handleGrowSuccess,
237
- onFailure: handleGrowFailure,
238
- onError: handleGrowError,
239
- onWalletChange: (state: string) => {
240
- console.info('Grow wallet state:', state);
241
- // Wallet ready try to render immediately
242
- tryRenderPaymentOptions();
243
- },
244
- },
245
- });
246
-
247
- // Also poll as fallback: wait 1s for init() to finish, then poll every 500ms
248
- timeoutId = setTimeout(() => {
249
- tryRenderPaymentOptions();
250
- if (!rendered) {
251
- let attempts = 0;
252
- const maxAttempts = 16; // 16 * 500ms = 8 seconds after initial delay
253
- pollId = setInterval(() => {
254
- attempts++;
255
- tryRenderPaymentOptions();
256
- if (!rendered && attempts >= maxAttempts) {
257
- clearInterval(pollId);
258
- console.error('Grow SDK: renderPaymentOptions failed after max attempts');
259
- setError(t('paymentError'));
260
- }
261
- }, 500);
262
- }
263
- }, 1000);
264
-
265
- return () => {
266
- clearTimeout(timeoutId);
267
- clearInterval(pollId);
268
- };
269
- }, [sdkScriptLoaded, paymentIntent, handleGrowSuccess, handleGrowFailure, handleGrowError]);
270
-
271
- if (loading) {
272
- return (
273
- <div className={cn('flex flex-col items-center justify-center py-12', className)}>
274
- <LoadingSpinner size="lg" />
275
- <p className="text-muted-foreground mt-4 text-sm">{t('preparingPayment')}</p>
276
- </div>
277
- );
278
- }
279
-
280
- if (error) {
281
- const isNotConfigured =
282
- error.toLowerCase().includes('not configured') ||
283
- error.toLowerCase().includes('no payment') ||
284
- error.toLowerCase().includes('provider');
285
-
286
- return (
287
- <div className={cn('py-12 text-center', className)}>
288
- <svg
289
- className="text-muted-foreground mx-auto mb-4 h-12 w-12"
290
- fill="none"
291
- viewBox="0 0 24 24"
292
- stroke="currentColor"
293
- >
294
- <path
295
- strokeLinecap="round"
296
- strokeLinejoin="round"
297
- strokeWidth={1.5}
298
- d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
299
- />
300
- </svg>
301
- <h3 className="text-foreground mb-2 text-lg font-semibold">
302
- {isNotConfigured ? t('paymentNotConfigured') : t('paymentError')}
303
- </h3>
304
- <p className="text-muted-foreground mx-auto max-w-md text-sm">
305
- {isNotConfigured ? t('paymentNotConfiguredDesc') : error}
306
- </p>
307
- </div>
308
- );
309
- }
310
-
311
- // Grow: render the SDK wallet container
312
- if (paymentIntent?.provider === 'grow') {
313
- return (
314
- <div className={cn('py-4', className)}>
315
- {!growReady && (
316
- <div className="flex flex-col items-center justify-center py-8">
317
- <LoadingSpinner size="lg" />
318
- <p className="text-muted-foreground mt-4 text-sm">{t('loadingPaymentOptions')}</p>
319
- </div>
320
- )}
321
- <div id="grow-payment-container" />
322
- </div>
323
- );
324
- }
325
-
326
- // Stripe/other redirect-based: show redirecting state
327
- if (paymentIntent) {
328
- return (
329
- <div className={cn('flex flex-col items-center justify-center py-12', className)}>
330
- <LoadingSpinner size="lg" />
331
- <p className="text-muted-foreground mt-4 text-sm">{t('redirectingToPayment')}</p>
332
- <p className="text-muted-foreground mt-2 text-xs">
333
- {t('redirectingHint')}
334
- <a href={paymentIntent.clientSecret} className="text-primary hover:underline">
335
- {t('clickHere')}
336
- </a>
337
- .
338
- </p>
339
- </div>
340
- );
341
- }
342
-
343
- return null;
344
- }
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useRef, useCallback } from 'react';
4
+ import type { PaymentIntent } from 'brainerce';
5
+ import { getClient } from '@/lib/brainerce';
6
+ import { useTranslations } from '@/lib/translations';
7
+ import { LoadingSpinner } from '@/components/shared/loading-spinner';
8
+ import { cn } from '@/lib/utils';
9
+
10
+ // SDK-specific globals injected by external payment scripts
11
+ declare global {
12
+ interface Window {
13
+ growPayment?: {
14
+ init: (config: {
15
+ environment: string;
16
+ version: number;
17
+ events: {
18
+ onSuccess?: (response: unknown) => void;
19
+ onFailure?: (response: unknown) => void;
20
+ onError?: (response: unknown) => void;
21
+ onTimeout?: (response: unknown) => void;
22
+ onWalletChange?: (state: string) => void;
23
+ onPaymentStart?: (response: unknown) => void;
24
+ onPaymentCancel?: (response: unknown) => void;
25
+ };
26
+ }) => void;
27
+ renderPaymentOptions: (authCode: string) => void;
28
+ };
29
+ }
30
+ }
31
+
32
+ // Payment SDK script URLs — resolved per provider
33
+ const PAYMENT_SDK_URL = 'https://cdn.meshulam.co.il/sdk/gs.min.js';
34
+ const APPLE_PAY_SDK_URL = 'https://meshulam.co.il/_media/js/apple_pay_sdk/sdk.min.js';
35
+
36
+ interface PaymentStepProps {
37
+ checkoutId: string;
38
+ className?: string;
39
+ }
40
+
41
+ /**
42
+ * Load a script tag if not already present in the DOM.
43
+ * Resolves when the script loads (or immediately if already present).
44
+ */
45
+ function loadScript(src: string, optional = false): Promise<void> {
46
+ return new Promise((resolve) => {
47
+ if (document.querySelector(`script[src="${src}"]`)) {
48
+ resolve();
49
+ return;
50
+ }
51
+ const script = document.createElement('script');
52
+ script.src = src;
53
+ script.async = true;
54
+ script.onload = () => resolve();
55
+ script.onerror = () => {
56
+ if (optional) {
57
+ resolve(); // Non-blocking for optional SDKs
58
+ } else {
59
+ resolve(); // Still resolve — caller handles missing global
60
+ }
61
+ };
62
+ document.head.appendChild(script);
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Wait for the payment SDK global to become available (set by the SDK script).
68
+ */
69
+ function waitForPaymentSdkGlobal(timeoutMs = 5000): Promise<boolean> {
70
+ return new Promise((resolve) => {
71
+ if (window.growPayment) {
72
+ resolve(true);
73
+ return;
74
+ }
75
+ const start = Date.now();
76
+ const check = setInterval(() => {
77
+ if (window.growPayment) {
78
+ clearInterval(check);
79
+ resolve(true);
80
+ } else if (Date.now() - start > timeoutMs) {
81
+ clearInterval(check);
82
+ resolve(false);
83
+ }
84
+ }, 50);
85
+ });
86
+ }
87
+
88
+ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
89
+ const t = useTranslations('checkout');
90
+ const [paymentIntent, setPaymentIntent] = useState<PaymentIntent | null>(null);
91
+ const [loading, setLoading] = useState(true);
92
+ const [error, setError] = useState<string | null>(null);
93
+ const [sdkReady, setSdkReady] = useState(false);
94
+ const [sdkScriptLoaded, setSdkScriptLoaded] = useState(false);
95
+ const renderAttempted = useRef(false);
96
+
97
+ const handleSdkPaymentSuccess = useCallback(
98
+ async (response: unknown) => {
99
+ console.info('Payment SDK success:', JSON.stringify(response));
100
+ try {
101
+ const client = getClient();
102
+ // Try response.data first, fall back to response itself
103
+ const resp = response as Record<string, unknown>;
104
+ const data = (resp?.data && typeof resp.data === 'object' ? resp.data : resp) as
105
+ | Record<string, unknown>
106
+ | undefined;
107
+ await client.confirmSdkPayment(checkoutId, data || undefined);
108
+ } catch (err) {
109
+ console.warn('Failed to confirm payment with backend:', err);
110
+ }
111
+ window.location.href = `/order-confirmation?checkout_id=${checkoutId}`;
112
+ },
113
+ [checkoutId]
114
+ );
115
+
116
+ const handleSdkPaymentFailure = useCallback((response: unknown) => {
117
+ console.error('Payment SDK failure:', response);
118
+ const msg = (response as { message?: string })?.message || t('paymentError');
119
+ setError(msg);
120
+ }, []);
121
+
122
+ const handleSdkPaymentError = useCallback((response: unknown) => {
123
+ console.error('Payment SDK error:', response);
124
+ const msg = (response as { message?: string })?.message || t('paymentError');
125
+ setError(msg);
126
+ }, []);
127
+
128
+ // Step 1: Load SDK scripts — just load, don't init yet
129
+ useEffect(() => {
130
+ let cancelled = false;
131
+
132
+ async function loadSdkScripts() {
133
+ // Load Apple Pay SDK (optional)
134
+ await loadScript(APPLE_PAY_SDK_URL, true);
135
+
136
+ // Load payment SDK script
137
+ await loadScript(PAYMENT_SDK_URL);
138
+
139
+ // Wait for SDK global to be set by the script
140
+ const available = await waitForPaymentSdkGlobal();
141
+
142
+ if (cancelled) return;
143
+
144
+ if (!available) {
145
+ setError(t('failedToLoadPaymentSdk'));
146
+ return;
147
+ }
148
+
149
+ setSdkScriptLoaded(true);
150
+ }
151
+
152
+ loadSdkScripts();
153
+
154
+ return () => {
155
+ cancelled = true;
156
+ };
157
+ }, []);
158
+
159
+ // Step 2: Create payment intent (API call)
160
+ // Use ref to prevent double-creation in React StrictMode
161
+ const intentCreated = useRef(false);
162
+ useEffect(() => {
163
+ if (intentCreated.current) return;
164
+ intentCreated.current = true;
165
+
166
+ async function createIntent() {
167
+ try {
168
+ setLoading(true);
169
+ setError(null);
170
+ const client = getClient();
171
+
172
+ const successUrl = `${window.location.origin}/order-confirmation?checkout_id=${checkoutId}`;
173
+ const cancelUrl = `${window.location.origin}/checkout?checkout_id=${checkoutId}&canceled=true`;
174
+
175
+ const intent = await client.createPaymentIntent(checkoutId, {
176
+ successUrl,
177
+ cancelUrl,
178
+ });
179
+
180
+ setPaymentIntent(intent);
181
+
182
+ // Auto-redirect for Stripe
183
+ if (intent.provider === 'stripe') {
184
+ window.location.href = intent.clientSecret;
185
+ }
186
+ } catch (err) {
187
+ const message = err instanceof Error ? err.message : t('paymentError');
188
+ setError(message);
189
+ } finally {
190
+ setLoading(false);
191
+ }
192
+ }
193
+
194
+ createIntent();
195
+ }, [checkoutId]);
196
+
197
+ // Step 3: When BOTH SDK script is loaded AND payment intent is ready:
198
+ // - init() with the CORRECT environment from the payment intent
199
+ // - wait for onWalletChange signal before calling renderPaymentOptions()
200
+ useEffect(() => {
201
+ if (!sdkScriptLoaded) return;
202
+ if (!paymentIntent || paymentIntent.provider !== 'grow') return;
203
+ if (!window.growPayment) return;
204
+ if (renderAttempted.current) return;
205
+
206
+ renderAttempted.current = true;
207
+
208
+ // Parse environment and authCode from clientSecret (format: "ENV|authCode")
209
+ const pipeIndex = paymentIntent.clientSecret.indexOf('|');
210
+ const env = pipeIndex !== -1 ? paymentIntent.clientSecret.substring(0, pipeIndex) : 'DEV';
211
+ const authCode =
212
+ pipeIndex !== -1
213
+ ? paymentIntent.clientSecret.substring(pipeIndex + 1)
214
+ : paymentIntent.clientSecret;
215
+
216
+ let rendered = false;
217
+ let pollId: ReturnType<typeof setInterval>;
218
+
219
+ function tryRenderPaymentOptions() {
220
+ if (rendered) return;
221
+ try {
222
+ window.growPayment?.renderPaymentOptions(authCode);
223
+ rendered = true;
224
+ clearInterval(pollId);
225
+ setSdkReady(true);
226
+ console.info('Payment SDK: renderPaymentOptions succeeded');
227
+ } catch (err) {
228
+ console.info('Payment SDK: renderPaymentOptions not ready yet', err);
229
+ }
230
+ }
231
+
232
+ console.info('Payment SDK: init with environment:', env);
233
+ window.growPayment.init({
234
+ environment: env,
235
+ version: 1,
236
+ events: {
237
+ onSuccess: handleSdkPaymentSuccess,
238
+ onFailure: handleSdkPaymentFailure,
239
+ onError: handleSdkPaymentError,
240
+ onWalletChange: (state: string) => {
241
+ console.info('Payment SDK wallet state:', state);
242
+ tryRenderPaymentOptions();
243
+ },
244
+ },
245
+ });
246
+
247
+ // Poll as fallback: wait 1s for init() to finish, then poll every 500ms
248
+ const timeoutId = setTimeout(() => {
249
+ tryRenderPaymentOptions();
250
+ if (!rendered) {
251
+ let attempts = 0;
252
+ const maxAttempts = 16; // 16 * 500ms = 8 seconds after initial delay
253
+ pollId = setInterval(() => {
254
+ attempts++;
255
+ tryRenderPaymentOptions();
256
+ if (!rendered && attempts >= maxAttempts) {
257
+ clearInterval(pollId);
258
+ console.error('Payment SDK: renderPaymentOptions failed after max attempts');
259
+ setError(t('paymentError'));
260
+ }
261
+ }, 500);
262
+ }
263
+ }, 1000);
264
+
265
+ return () => {
266
+ clearTimeout(timeoutId);
267
+ clearInterval(pollId);
268
+ };
269
+ }, [
270
+ sdkScriptLoaded,
271
+ paymentIntent,
272
+ handleSdkPaymentSuccess,
273
+ handleSdkPaymentFailure,
274
+ handleSdkPaymentError,
275
+ ]);
276
+
277
+ if (loading) {
278
+ return (
279
+ <div className={cn('flex flex-col items-center justify-center py-12', className)}>
280
+ <LoadingSpinner size="lg" />
281
+ <p className="text-muted-foreground mt-4 text-sm">{t('preparingPayment')}</p>
282
+ </div>
283
+ );
284
+ }
285
+
286
+ if (error) {
287
+ const isNotConfigured =
288
+ error.toLowerCase().includes('not configured') ||
289
+ error.toLowerCase().includes('no payment') ||
290
+ error.toLowerCase().includes('provider');
291
+
292
+ return (
293
+ <div className={cn('py-12 text-center', className)}>
294
+ <svg
295
+ className="text-muted-foreground mx-auto mb-4 h-12 w-12"
296
+ fill="none"
297
+ viewBox="0 0 24 24"
298
+ stroke="currentColor"
299
+ >
300
+ <path
301
+ strokeLinecap="round"
302
+ strokeLinejoin="round"
303
+ strokeWidth={1.5}
304
+ d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
305
+ />
306
+ </svg>
307
+ <h3 className="text-foreground mb-2 text-lg font-semibold">
308
+ {isNotConfigured ? t('paymentNotConfigured') : t('paymentError')}
309
+ </h3>
310
+ <p className="text-muted-foreground mx-auto max-w-md text-sm">
311
+ {isNotConfigured ? t('paymentNotConfiguredDesc') : error}
312
+ </p>
313
+ </div>
314
+ );
315
+ }
316
+
317
+ // SDK-based provider: render the payment widget container
318
+ if (paymentIntent?.provider === 'grow') {
319
+ return (
320
+ <div className={cn('py-4', className)}>
321
+ {!sdkReady && (
322
+ <div className="flex flex-col items-center justify-center py-8">
323
+ <LoadingSpinner size="lg" />
324
+ <p className="text-muted-foreground mt-4 text-sm">{t('loadingPaymentOptions')}</p>
325
+ </div>
326
+ )}
327
+ <div id="grow-payment-container" />
328
+ </div>
329
+ );
330
+ }
331
+
332
+ // Stripe/other redirect-based: show redirecting state
333
+ if (paymentIntent) {
334
+ return (
335
+ <div className={cn('flex flex-col items-center justify-center py-12', className)}>
336
+ <LoadingSpinner size="lg" />
337
+ <p className="text-muted-foreground mt-4 text-sm">{t('redirectingToPayment')}</p>
338
+ <p className="text-muted-foreground mt-2 text-xs">
339
+ {t('redirectingHint')}
340
+ <a href={paymentIntent.clientSecret} className="text-primary hover:underline">
341
+ {t('clickHere')}
342
+ </a>
343
+ .
344
+ </p>
345
+ </div>
346
+ );
347
+ }
348
+
349
+ return null;
350
+ }