create-brainerce-store 1.6.1 → 1.7.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.
- package/package.json +1 -1
- package/templates/nextjs/base/.env.local.ejs +3 -1
- package/templates/nextjs/base/src/app/api/auth/logout/route.ts +14 -0
- package/templates/nextjs/base/src/app/api/auth/me/route.ts +49 -0
- package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +59 -0
- package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +41 -0
- package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +68 -0
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +190 -0
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +92 -90
- package/templates/nextjs/base/src/app/forgot-password/page.tsx +112 -111
- package/templates/nextjs/base/src/app/login/page.tsx +59 -58
- package/templates/nextjs/base/src/app/register/page.tsx +64 -68
- package/templates/nextjs/base/src/app/reset-password/page.tsx +132 -161
- package/templates/nextjs/base/src/app/verify-email/page.tsx +258 -293
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +379 -372
- package/templates/nextjs/base/src/lib/auth.ts +148 -0
- package/templates/nextjs/base/src/lib/brainerce.ts.ejs +6 -26
- package/templates/nextjs/base/src/middleware.ts +25 -0
- package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +50 -27
|
@@ -1,372 +1,379 @@
|
|
|
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
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
script
|
|
55
|
-
script.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
</p>
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
+
// SRI hashes must be regenerated when the provider updates their SDK
|
|
34
|
+
const PAYMENT_SDK_URL = 'https://cdn.meshulam.co.il/sdk/gs.min.js';
|
|
35
|
+
const PAYMENT_SDK_SRI = 'sha384-e1OYzZERZLgbR8Zw1I4Ww/J7qXGyEsZb7LF0z5W/sbnTsFwtxop7sKZsLGNp6CB1';
|
|
36
|
+
const APPLE_PAY_SDK_URL = 'https://meshulam.co.il/_media/js/apple_pay_sdk/sdk.min.js';
|
|
37
|
+
const APPLE_PAY_SDK_SRI = 'sha384-CG67HXmWBeyuRR/jqFsYr6dGEMFyuZw3/hRmcyACJRYHGb/1ebEkQZyzdJXDc9/u';
|
|
38
|
+
|
|
39
|
+
interface PaymentStepProps {
|
|
40
|
+
checkoutId: string;
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load a script tag if not already present in the DOM.
|
|
46
|
+
* Resolves when the script loads (or immediately if already present).
|
|
47
|
+
*/
|
|
48
|
+
function loadScript(src: string, optional = false, integrity?: string): Promise<void> {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
if (document.querySelector(`script[src="${src}"]`)) {
|
|
51
|
+
resolve();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const script = document.createElement('script');
|
|
55
|
+
script.src = src;
|
|
56
|
+
script.async = true;
|
|
57
|
+
if (integrity) {
|
|
58
|
+
script.integrity = integrity;
|
|
59
|
+
script.crossOrigin = 'anonymous';
|
|
60
|
+
}
|
|
61
|
+
script.onload = () => resolve();
|
|
62
|
+
script.onerror = () => {
|
|
63
|
+
if (optional) {
|
|
64
|
+
resolve(); // Non-blocking for optional SDKs
|
|
65
|
+
} else {
|
|
66
|
+
resolve(); // Still resolve — caller handles missing global
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
document.head.appendChild(script);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Wait for the payment SDK global to become available (set by the SDK script).
|
|
75
|
+
*/
|
|
76
|
+
function waitForPaymentSdkGlobal(timeoutMs = 5000): Promise<boolean> {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
if (window.growPayment) {
|
|
79
|
+
resolve(true);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const start = Date.now();
|
|
83
|
+
const check = setInterval(() => {
|
|
84
|
+
if (window.growPayment) {
|
|
85
|
+
clearInterval(check);
|
|
86
|
+
resolve(true);
|
|
87
|
+
} else if (Date.now() - start > timeoutMs) {
|
|
88
|
+
clearInterval(check);
|
|
89
|
+
resolve(false);
|
|
90
|
+
}
|
|
91
|
+
}, 50);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
96
|
+
const t = useTranslations('checkout');
|
|
97
|
+
const [paymentIntent, setPaymentIntent] = useState<PaymentIntent | null>(null);
|
|
98
|
+
const [loading, setLoading] = useState(true);
|
|
99
|
+
const [error, setError] = useState<string | null>(null);
|
|
100
|
+
const [sdkReady, setSdkReady] = useState(false);
|
|
101
|
+
const [sdkScriptLoaded, setSdkScriptLoaded] = useState(false);
|
|
102
|
+
const renderAttempted = useRef(false);
|
|
103
|
+
|
|
104
|
+
const handleSdkPaymentSuccess = useCallback(
|
|
105
|
+
async (response: unknown) => {
|
|
106
|
+
console.info('Payment SDK success:', JSON.stringify(response));
|
|
107
|
+
try {
|
|
108
|
+
const client = getClient();
|
|
109
|
+
// Try response.data first, fall back to response itself
|
|
110
|
+
const resp = response as Record<string, unknown>;
|
|
111
|
+
const data = (resp?.data && typeof resp.data === 'object' ? resp.data : resp) as
|
|
112
|
+
| Record<string, unknown>
|
|
113
|
+
| undefined;
|
|
114
|
+
await client.confirmSdkPayment(checkoutId, data || undefined);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.warn('Failed to confirm payment with backend:', err);
|
|
117
|
+
}
|
|
118
|
+
window.location.href = `/order-confirmation?checkout_id=${checkoutId}`;
|
|
119
|
+
},
|
|
120
|
+
[checkoutId]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const handleSdkPaymentFailure = useCallback((response: unknown) => {
|
|
124
|
+
console.error('Payment SDK failure:', response);
|
|
125
|
+
const msg = (response as { message?: string })?.message || t('paymentError');
|
|
126
|
+
setError(msg);
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const handleSdkPaymentError = useCallback((response: unknown) => {
|
|
130
|
+
const TRANSIENT_SDK_ERRORS = [
|
|
131
|
+
'Wallet not initialized',
|
|
132
|
+
"SDK was not loaded as needed and therefore can't run",
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const msg = (response as { message?: string })?.message || '';
|
|
136
|
+
// Grow SDK fires transient errors during startup before its internal
|
|
137
|
+
// state is ready. The polling mechanism in Step 3 retries
|
|
138
|
+
// renderPaymentOptions() until the SDK is fully initialized.
|
|
139
|
+
if (TRANSIENT_SDK_ERRORS.some((e) => msg.includes(e))) {
|
|
140
|
+
console.info('Payment SDK: transient startup error, waiting for retry...', msg);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
console.error('Payment SDK error:', response);
|
|
144
|
+
setError(msg || t('paymentError'));
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
// Step 1: Load SDK scripts — just load, don't init yet
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
let cancelled = false;
|
|
150
|
+
|
|
151
|
+
async function loadSdkScripts() {
|
|
152
|
+
// Load Apple Pay SDK (optional)
|
|
153
|
+
await loadScript(APPLE_PAY_SDK_URL, true, APPLE_PAY_SDK_SRI);
|
|
154
|
+
|
|
155
|
+
// Load payment SDK script
|
|
156
|
+
await loadScript(PAYMENT_SDK_URL, false, PAYMENT_SDK_SRI);
|
|
157
|
+
|
|
158
|
+
// Wait for SDK global to be set by the script
|
|
159
|
+
const available = await waitForPaymentSdkGlobal();
|
|
160
|
+
|
|
161
|
+
if (cancelled) return;
|
|
162
|
+
|
|
163
|
+
if (!available) {
|
|
164
|
+
setError(t('failedToLoadPaymentSdk'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setSdkScriptLoaded(true);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
loadSdkScripts();
|
|
172
|
+
|
|
173
|
+
return () => {
|
|
174
|
+
cancelled = true;
|
|
175
|
+
};
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
// Step 2: Create payment intent (API call)
|
|
179
|
+
// Use ref to prevent double-creation in React StrictMode
|
|
180
|
+
const intentCreated = useRef(false);
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (intentCreated.current) return;
|
|
183
|
+
intentCreated.current = true;
|
|
184
|
+
|
|
185
|
+
async function createIntent() {
|
|
186
|
+
try {
|
|
187
|
+
setLoading(true);
|
|
188
|
+
setError(null);
|
|
189
|
+
const client = getClient();
|
|
190
|
+
|
|
191
|
+
const successUrl = `${window.location.origin}/order-confirmation?checkout_id=${checkoutId}`;
|
|
192
|
+
const cancelUrl = `${window.location.origin}/checkout?checkout_id=${checkoutId}&canceled=true`;
|
|
193
|
+
|
|
194
|
+
const intent = await client.createPaymentIntent(checkoutId, {
|
|
195
|
+
successUrl,
|
|
196
|
+
cancelUrl,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
setPaymentIntent(intent);
|
|
200
|
+
|
|
201
|
+
// Auto-redirect for Stripe
|
|
202
|
+
if (intent.provider === 'stripe') {
|
|
203
|
+
window.location.href = intent.clientSecret;
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
const message = err instanceof Error ? err.message : t('paymentError');
|
|
207
|
+
setError(message);
|
|
208
|
+
} finally {
|
|
209
|
+
setLoading(false);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
createIntent();
|
|
214
|
+
}, [checkoutId]);
|
|
215
|
+
|
|
216
|
+
// Step 3: When BOTH SDK script is loaded AND payment intent is ready:
|
|
217
|
+
// - init() with the CORRECT environment from the payment intent
|
|
218
|
+
// - wait for onWalletChange signal before calling renderPaymentOptions()
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (!sdkScriptLoaded) return;
|
|
221
|
+
if (!paymentIntent || paymentIntent.provider !== 'grow') return;
|
|
222
|
+
if (!window.growPayment) return;
|
|
223
|
+
if (renderAttempted.current) return;
|
|
224
|
+
|
|
225
|
+
renderAttempted.current = true;
|
|
226
|
+
|
|
227
|
+
// Parse environment and authCode from clientSecret (format: "ENV|authCode")
|
|
228
|
+
const pipeIndex = paymentIntent.clientSecret.indexOf('|');
|
|
229
|
+
const env = pipeIndex !== -1 ? paymentIntent.clientSecret.substring(0, pipeIndex) : 'DEV';
|
|
230
|
+
const authCode =
|
|
231
|
+
pipeIndex !== -1
|
|
232
|
+
? paymentIntent.clientSecret.substring(pipeIndex + 1)
|
|
233
|
+
: paymentIntent.clientSecret;
|
|
234
|
+
|
|
235
|
+
let rendered = false;
|
|
236
|
+
let walletReady = false;
|
|
237
|
+
let pollId: ReturnType<typeof setInterval>;
|
|
238
|
+
|
|
239
|
+
function tryRenderPaymentOptions() {
|
|
240
|
+
if (rendered) return;
|
|
241
|
+
try {
|
|
242
|
+
window.growPayment?.renderPaymentOptions(authCode);
|
|
243
|
+
rendered = true;
|
|
244
|
+
clearInterval(pollId);
|
|
245
|
+
setSdkReady(true);
|
|
246
|
+
console.info('Payment SDK: renderPaymentOptions succeeded');
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.info('Payment SDK: renderPaymentOptions not ready yet', err);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.info('Payment SDK: init with environment:', env);
|
|
253
|
+
window.growPayment.init({
|
|
254
|
+
environment: env,
|
|
255
|
+
version: 1,
|
|
256
|
+
events: {
|
|
257
|
+
onSuccess: handleSdkPaymentSuccess,
|
|
258
|
+
onFailure: handleSdkPaymentFailure,
|
|
259
|
+
onError: handleSdkPaymentError,
|
|
260
|
+
onWalletChange: (state: string) => {
|
|
261
|
+
console.info('Payment SDK wallet state:', state);
|
|
262
|
+
walletReady = true;
|
|
263
|
+
tryRenderPaymentOptions();
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Poll as fallback — but respect the SDK's readiness signal:
|
|
269
|
+
// - First 4s: only render if onWalletChange has fired (SDK says it's ready)
|
|
270
|
+
// - After 4s: force-attempt even without wallet signal (onWalletChange may not fire)
|
|
271
|
+
const WALLET_GRACE_PERIOD = 4000;
|
|
272
|
+
const initTime = Date.now();
|
|
273
|
+
|
|
274
|
+
const timeoutId = setTimeout(() => {
|
|
275
|
+
if (walletReady) tryRenderPaymentOptions();
|
|
276
|
+
if (!rendered) {
|
|
277
|
+
let attempts = 0;
|
|
278
|
+
const maxAttempts = 16; // 16 * 500ms = 8s after initial 1s delay
|
|
279
|
+
pollId = setInterval(() => {
|
|
280
|
+
attempts++;
|
|
281
|
+
const elapsed = Date.now() - initTime;
|
|
282
|
+
if (walletReady || elapsed > WALLET_GRACE_PERIOD) {
|
|
283
|
+
tryRenderPaymentOptions();
|
|
284
|
+
}
|
|
285
|
+
if (!rendered && attempts >= maxAttempts) {
|
|
286
|
+
clearInterval(pollId);
|
|
287
|
+
console.error('Payment SDK: renderPaymentOptions failed after max attempts');
|
|
288
|
+
setError(t('paymentError'));
|
|
289
|
+
}
|
|
290
|
+
}, 500);
|
|
291
|
+
}
|
|
292
|
+
}, 1000);
|
|
293
|
+
|
|
294
|
+
return () => {
|
|
295
|
+
clearTimeout(timeoutId);
|
|
296
|
+
clearInterval(pollId);
|
|
297
|
+
};
|
|
298
|
+
}, [
|
|
299
|
+
sdkScriptLoaded,
|
|
300
|
+
paymentIntent,
|
|
301
|
+
handleSdkPaymentSuccess,
|
|
302
|
+
handleSdkPaymentFailure,
|
|
303
|
+
handleSdkPaymentError,
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
if (loading) {
|
|
307
|
+
return (
|
|
308
|
+
<div className={cn('flex flex-col items-center justify-center py-12', className)}>
|
|
309
|
+
<LoadingSpinner size="lg" />
|
|
310
|
+
<p className="text-muted-foreground mt-4 text-sm">{t('preparingPayment')}</p>
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (error) {
|
|
316
|
+
const isNotConfigured =
|
|
317
|
+
error.toLowerCase().includes('not configured') ||
|
|
318
|
+
error.toLowerCase().includes('no payment') ||
|
|
319
|
+
error.toLowerCase().includes('provider');
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div className={cn('py-12 text-center', className)}>
|
|
323
|
+
<svg
|
|
324
|
+
className="text-muted-foreground mx-auto mb-4 h-12 w-12"
|
|
325
|
+
fill="none"
|
|
326
|
+
viewBox="0 0 24 24"
|
|
327
|
+
stroke="currentColor"
|
|
328
|
+
>
|
|
329
|
+
<path
|
|
330
|
+
strokeLinecap="round"
|
|
331
|
+
strokeLinejoin="round"
|
|
332
|
+
strokeWidth={1.5}
|
|
333
|
+
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"
|
|
334
|
+
/>
|
|
335
|
+
</svg>
|
|
336
|
+
<h3 className="text-foreground mb-2 text-lg font-semibold">
|
|
337
|
+
{isNotConfigured ? t('paymentNotConfigured') : t('paymentError')}
|
|
338
|
+
</h3>
|
|
339
|
+
<p className="text-muted-foreground mx-auto max-w-md text-sm">
|
|
340
|
+
{isNotConfigured ? t('paymentNotConfiguredDesc') : error}
|
|
341
|
+
</p>
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// SDK-based provider: render the payment widget container
|
|
347
|
+
if (paymentIntent?.provider === 'grow') {
|
|
348
|
+
return (
|
|
349
|
+
<div className={cn('py-4', className)}>
|
|
350
|
+
{!sdkReady && (
|
|
351
|
+
<div className="flex flex-col items-center justify-center py-8">
|
|
352
|
+
<LoadingSpinner size="lg" />
|
|
353
|
+
<p className="text-muted-foreground mt-4 text-sm">{t('loadingPaymentOptions')}</p>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
<div id="grow-payment-container" />
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Stripe/other redirect-based: show redirecting state
|
|
362
|
+
if (paymentIntent) {
|
|
363
|
+
return (
|
|
364
|
+
<div className={cn('flex flex-col items-center justify-center py-12', className)}>
|
|
365
|
+
<LoadingSpinner size="lg" />
|
|
366
|
+
<p className="text-muted-foreground mt-4 text-sm">{t('redirectingToPayment')}</p>
|
|
367
|
+
<p className="text-muted-foreground mt-2 text-xs">
|
|
368
|
+
{t('redirectingHint')}
|
|
369
|
+
<a href={paymentIntent.clientSecret} className="text-primary hover:underline">
|
|
370
|
+
{t('clickHere')}
|
|
371
|
+
</a>
|
|
372
|
+
.
|
|
373
|
+
</p>
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return null;
|
|
379
|
+
}
|