create-brainerce-store 1.11.2 → 1.12.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.
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/nextjs/base/next.config.ts +31 -31
- package/templates/nextjs/base/src/app/api/auth/me/route.ts +8 -1
- package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +12 -3
- package/templates/nextjs/base/src/app/globals.css +0 -1
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +444 -534
- package/templates/nextjs/base/src/components/layout/footer.tsx +41 -38
- package/templates/nextjs/base/src/components/layout/header.tsx +336 -332
|
@@ -1,534 +1,444 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
4
|
-
import type { PaymentIntent, PaymentClientSdk } 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
|
-
/**
|
|
11
|
-
* Backward-compat defaults
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
if (!
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return (
|
|
446
|
-
<div className={cn('flex flex-col items-center justify-center py-12', className)}>
|
|
447
|
-
<LoadingSpinner size="lg" />
|
|
448
|
-
<p className="text-muted-foreground mt-4 text-sm">{t('preparingPayment')}</p>
|
|
449
|
-
</div>
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (error) {
|
|
454
|
-
const isNotConfigured =
|
|
455
|
-
error.toLowerCase().includes('not configured') ||
|
|
456
|
-
error.toLowerCase().includes('no payment') ||
|
|
457
|
-
error.toLowerCase().includes('provider');
|
|
458
|
-
|
|
459
|
-
return (
|
|
460
|
-
<div className={cn('py-12 text-center', className)}>
|
|
461
|
-
<svg
|
|
462
|
-
className="text-muted-foreground mx-auto mb-4 h-12 w-12"
|
|
463
|
-
fill="none"
|
|
464
|
-
viewBox="0 0 24 24"
|
|
465
|
-
stroke="currentColor"
|
|
466
|
-
>
|
|
467
|
-
<path
|
|
468
|
-
strokeLinecap="round"
|
|
469
|
-
strokeLinejoin="round"
|
|
470
|
-
strokeWidth={1.5}
|
|
471
|
-
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"
|
|
472
|
-
/>
|
|
473
|
-
</svg>
|
|
474
|
-
<h3 className="text-foreground mb-2 text-lg font-semibold">
|
|
475
|
-
{isNotConfigured ? t('paymentNotConfigured') : t('paymentError')}
|
|
476
|
-
</h3>
|
|
477
|
-
<p className="text-muted-foreground mx-auto max-w-md text-sm">
|
|
478
|
-
{isNotConfigured ? t('paymentNotConfiguredDesc') : error}
|
|
479
|
-
</p>
|
|
480
|
-
</div>
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (!paymentIntent) return null;
|
|
485
|
-
|
|
486
|
-
const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
|
|
487
|
-
|
|
488
|
-
// SDK-widget provider: render the payment widget container
|
|
489
|
-
if (sdk.renderType === 'sdk-widget') {
|
|
490
|
-
const containerId =
|
|
491
|
-
sdk.containerId || `${paymentIntent.provider || 'payment'}-payment-container`;
|
|
492
|
-
return (
|
|
493
|
-
<div className={cn('py-4', className)}>
|
|
494
|
-
{!sdkReady && (
|
|
495
|
-
<div className="flex flex-col items-center justify-center py-8">
|
|
496
|
-
<LoadingSpinner size="lg" />
|
|
497
|
-
<p className="text-muted-foreground mt-4 text-sm">{t('loadingPaymentOptions')}</p>
|
|
498
|
-
</div>
|
|
499
|
-
)}
|
|
500
|
-
<div id={containerId} />
|
|
501
|
-
</div>
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Iframe provider: embed the payment URL
|
|
506
|
-
if (sdk.renderType === 'iframe') {
|
|
507
|
-
return (
|
|
508
|
-
<div className={cn('py-4', className)}>
|
|
509
|
-
<iframe
|
|
510
|
-
src={paymentIntent.clientSecret}
|
|
511
|
-
className="w-full border-0"
|
|
512
|
-
style={{ minHeight: '500px' }}
|
|
513
|
-
title={t('payment')}
|
|
514
|
-
allow="payment"
|
|
515
|
-
/>
|
|
516
|
-
</div>
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Redirect provider: show redirecting state (auto-redirect already triggered in Step 0+1)
|
|
521
|
-
return (
|
|
522
|
-
<div className={cn('flex flex-col items-center justify-center py-12', className)}>
|
|
523
|
-
<LoadingSpinner size="lg" />
|
|
524
|
-
<p className="text-muted-foreground mt-4 text-sm">{t('redirectingToPayment')}</p>
|
|
525
|
-
<p className="text-muted-foreground mt-2 text-xs">
|
|
526
|
-
{t('redirectingHint')}
|
|
527
|
-
<a href={paymentIntent.clientSecret} className="text-primary hover:underline">
|
|
528
|
-
{t('clickHere')}
|
|
529
|
-
</a>
|
|
530
|
-
.
|
|
531
|
-
</p>
|
|
532
|
-
</div>
|
|
533
|
-
);
|
|
534
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
4
|
+
import type { PaymentIntent, PaymentClientSdk } 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
|
+
/**
|
|
11
|
+
* Backward-compat defaults when backend doesn't return clientSdk.
|
|
12
|
+
*/
|
|
13
|
+
const LEGACY_GROW_SDK: PaymentClientSdk = {
|
|
14
|
+
renderType: 'sdk-widget',
|
|
15
|
+
scriptUrl: 'https://cdn.meshulam.co.il/sdk/gs.min.js',
|
|
16
|
+
globalName: 'growPayment',
|
|
17
|
+
initMethod: 'init',
|
|
18
|
+
renderMethod: 'renderPaymentOptions',
|
|
19
|
+
containerId: 'grow-payment-container',
|
|
20
|
+
initConfig: { version: 1, environment: 'DEV' },
|
|
21
|
+
additionalScripts: [
|
|
22
|
+
{ url: 'https://meshulam.co.il/_media/js/apple_pay_sdk/sdk.min.js', optional: true },
|
|
23
|
+
],
|
|
24
|
+
bodyStyles:
|
|
25
|
+
'[id*="Gr0W8-"],[id*="Gr0W8-"] *,[class*="Gr0W8-"],[class*="Gr0W8-"] *{direction:ltr !important;text-align:left}',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
interface PaymentStepProps {
|
|
29
|
+
checkoutId: string;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveClientSdk(
|
|
34
|
+
intent: PaymentIntent | null,
|
|
35
|
+
preloadedSdk?: PaymentClientSdk | null
|
|
36
|
+
): PaymentClientSdk {
|
|
37
|
+
const fullSdk = [preloadedSdk, intent?.clientSdk].find((s) => s?.renderType);
|
|
38
|
+
const runtimeSdk = intent?.clientSdk;
|
|
39
|
+
if (fullSdk) {
|
|
40
|
+
if (!runtimeSdk || runtimeSdk === fullSdk) return fullSdk;
|
|
41
|
+
return {
|
|
42
|
+
...fullSdk,
|
|
43
|
+
...(runtimeSdk.renderArg ? { renderArg: runtimeSdk.renderArg } : {}),
|
|
44
|
+
...(runtimeSdk.initConfig
|
|
45
|
+
? { initConfig: { ...fullSdk.initConfig, ...runtimeSdk.initConfig } }
|
|
46
|
+
: {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const legacy = intent?.provider === 'grow' ? LEGACY_GROW_SDK : null;
|
|
50
|
+
if (legacy && runtimeSdk) {
|
|
51
|
+
return {
|
|
52
|
+
...legacy,
|
|
53
|
+
...(runtimeSdk.renderArg ? { renderArg: runtimeSdk.renderArg } : {}),
|
|
54
|
+
...(runtimeSdk.initConfig
|
|
55
|
+
? { initConfig: { ...legacy.initConfig, ...runtimeSdk.initConfig } }
|
|
56
|
+
: {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (legacy) return legacy;
|
|
60
|
+
return { renderType: 'redirect' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractMessage(response: unknown): string {
|
|
64
|
+
if (typeof response === 'string') return response;
|
|
65
|
+
return (response as { message?: string })?.message || '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
69
|
+
const t = useTranslations('checkout');
|
|
70
|
+
const [paymentIntent, setPaymentIntent] = useState<PaymentIntent | null>(null);
|
|
71
|
+
const [preloadedSdk, setPreloadedSdk] = useState<PaymentClientSdk | null>(null);
|
|
72
|
+
const [loading, setLoading] = useState(true);
|
|
73
|
+
const [error, setError] = useState<string | null>(null);
|
|
74
|
+
const [sdkReady, setSdkReady] = useState(false);
|
|
75
|
+
const walletOpenRef = useRef(false);
|
|
76
|
+
const initialized = useRef(false);
|
|
77
|
+
|
|
78
|
+
// Stable refs for SDK event callbacks (avoids stale closures in onload)
|
|
79
|
+
const cbRef = useRef({
|
|
80
|
+
onSuccess: (_r: unknown) => {},
|
|
81
|
+
onFailure: (_r: unknown) => {},
|
|
82
|
+
onError: (_r: unknown) => {},
|
|
83
|
+
onTimeout: () => {},
|
|
84
|
+
onWalletChange: (_s: string) => {},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const handleSuccess = useCallback(
|
|
88
|
+
async (response: unknown) => {
|
|
89
|
+
console.info('Payment SDK success:', JSON.stringify(response));
|
|
90
|
+
try {
|
|
91
|
+
const client = getClient();
|
|
92
|
+
const resp = response as Record<string, unknown>;
|
|
93
|
+
const data = (resp?.data && typeof resp.data === 'object' ? resp.data : resp) as
|
|
94
|
+
| Record<string, unknown>
|
|
95
|
+
| undefined;
|
|
96
|
+
await client.confirmSdkPayment(checkoutId, data || undefined);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.warn('Failed to confirm payment with backend:', err);
|
|
99
|
+
}
|
|
100
|
+
window.location.href = `/order-confirmation?checkout_id=${checkoutId}`;
|
|
101
|
+
},
|
|
102
|
+
[checkoutId]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
cbRef.current = {
|
|
106
|
+
onSuccess: handleSuccess,
|
|
107
|
+
onFailure: (response: unknown) => {
|
|
108
|
+
console.error('Payment SDK failure:', response);
|
|
109
|
+
setError(extractMessage(response) || t('paymentError'));
|
|
110
|
+
},
|
|
111
|
+
onError: (response: unknown) => {
|
|
112
|
+
const TRANSIENT = [
|
|
113
|
+
'Wallet not initialized',
|
|
114
|
+
"SDK was not loaded as needed and therefore can't run",
|
|
115
|
+
];
|
|
116
|
+
const msg = extractMessage(response);
|
|
117
|
+
if (TRANSIENT.some((e) => msg.includes(e))) {
|
|
118
|
+
console.info('Payment SDK: transient startup error (will retry):', msg);
|
|
119
|
+
return; // Don't show error — renderWhenReady will retry
|
|
120
|
+
}
|
|
121
|
+
console.error('Payment SDK error:', response);
|
|
122
|
+
setError(msg || t('paymentError'));
|
|
123
|
+
},
|
|
124
|
+
onTimeout: () => {
|
|
125
|
+
console.warn('Payment SDK: wallet timed out');
|
|
126
|
+
setError(t('paymentTimedOut'));
|
|
127
|
+
},
|
|
128
|
+
onWalletChange: (state: string) => {
|
|
129
|
+
console.info('Payment SDK wallet state:', state);
|
|
130
|
+
if (state === 'open') {
|
|
131
|
+
walletOpenRef.current = true;
|
|
132
|
+
setSdkReady(true);
|
|
133
|
+
}
|
|
134
|
+
if (state === 'close') setSdkReady(false);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// =========================================================================
|
|
139
|
+
// MAIN EFFECT — Follows Grow SDK docs exactly:
|
|
140
|
+
//
|
|
141
|
+
// Step 1: Load gs.min.js (insertBefore, as docs show)
|
|
142
|
+
// Step 2: s.onload → growPayment.init({ environment, version, events })
|
|
143
|
+
// This triggers the SDK to load mp.min.js → CSS, HTML, params, services
|
|
144
|
+
// Step 3: createPaymentIntent (starts wallet timer — should be AFTER init)
|
|
145
|
+
// Step 4: growPayment.renderPaymentOptions(authCode)
|
|
146
|
+
//
|
|
147
|
+
// "call createPaymentProcess right before you need to render the wallet"
|
|
148
|
+
// =========================================================================
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (initialized.current) return;
|
|
151
|
+
initialized.current = true;
|
|
152
|
+
|
|
153
|
+
const client = getClient();
|
|
154
|
+
const successUrl = `${window.location.origin}/order-confirmation?checkout_id=${checkoutId}`;
|
|
155
|
+
const cancelUrl = `${window.location.origin}/checkout?checkout_id=${checkoutId}&canceled=true`;
|
|
156
|
+
|
|
157
|
+
let sdkInitDone = false;
|
|
158
|
+
let currentSdk: PaymentClientSdk | null = null;
|
|
159
|
+
const cleanups: (() => void)[] = [];
|
|
160
|
+
|
|
161
|
+
// --- Load SDK script exactly as Grow docs show ---
|
|
162
|
+
function loadScript(sdk: PaymentClientSdk) {
|
|
163
|
+
if (!sdk.scriptUrl || !sdk.globalName) return;
|
|
164
|
+
|
|
165
|
+
// Inject bodyStyles
|
|
166
|
+
if (sdk.bodyStyles && !document.querySelector('style[data-payment-sdk]')) {
|
|
167
|
+
const style = document.createElement('style');
|
|
168
|
+
style.setAttribute('data-payment-sdk', 'true');
|
|
169
|
+
style.textContent = sdk.bodyStyles;
|
|
170
|
+
document.head.appendChild(style);
|
|
171
|
+
cleanups.push(() => style.remove());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Additional scripts (Apple Pay etc.) — fire and forget
|
|
175
|
+
if (sdk.additionalScripts) {
|
|
176
|
+
for (const extra of sdk.additionalScripts) {
|
|
177
|
+
if (document.querySelector(`script[src="${extra.url}"]`)) continue;
|
|
178
|
+
const s = document.createElement('script');
|
|
179
|
+
s.type = 'text/javascript';
|
|
180
|
+
s.async = true;
|
|
181
|
+
s.src = extra.url;
|
|
182
|
+
const ref = document.getElementsByTagName('script')[0];
|
|
183
|
+
if (ref?.parentNode) ref.parentNode.insertBefore(s, ref);
|
|
184
|
+
else document.head.appendChild(s);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Already loaded? Init immediately
|
|
189
|
+
if ((window as any)[sdk.globalName]) {
|
|
190
|
+
initSdk(sdk);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Load main SDK — insertBefore first <script> as Grow docs show
|
|
195
|
+
const s = document.createElement('script');
|
|
196
|
+
s.type = 'text/javascript';
|
|
197
|
+
s.async = true;
|
|
198
|
+
s.src = sdk.scriptUrl;
|
|
199
|
+
s.onload = () => initSdk(sdk); // init DIRECTLY in onload
|
|
200
|
+
s.onerror = () => {
|
|
201
|
+
console.error('Payment SDK: script load failed');
|
|
202
|
+
setError(t('failedToLoadPaymentSdk'));
|
|
203
|
+
};
|
|
204
|
+
const ref = document.getElementsByTagName('script')[0];
|
|
205
|
+
if (ref?.parentNode) ref.parentNode.insertBefore(s, ref);
|
|
206
|
+
else document.head.appendChild(s);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- Init: called in s.onload (as Grow docs require) ---
|
|
210
|
+
function initSdk(sdk: PaymentClientSdk) {
|
|
211
|
+
const global = (window as any)[sdk.globalName!];
|
|
212
|
+
if (!global) {
|
|
213
|
+
setError(t('failedToLoadPaymentSdk'));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const method = sdk.initMethod || 'init';
|
|
218
|
+
const config = {
|
|
219
|
+
...(sdk.initConfig || {}),
|
|
220
|
+
events: {
|
|
221
|
+
onSuccess: (r: unknown) => cbRef.current.onSuccess(r),
|
|
222
|
+
onFailure: (r: unknown) => cbRef.current.onFailure(r),
|
|
223
|
+
onError: (r: unknown) => cbRef.current.onError(r),
|
|
224
|
+
onTimeout: () => cbRef.current.onTimeout(),
|
|
225
|
+
onWalletChange: (s: string) => cbRef.current.onWalletChange(s),
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
console.info(
|
|
230
|
+
`Payment SDK: ${method}({ environment: "${config.environment}", version: ${config.version} })`
|
|
231
|
+
);
|
|
232
|
+
global[method](config);
|
|
233
|
+
sdkInitDone = true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- Render: retry until wallet actually opens (onWalletChange) ---
|
|
237
|
+
// No artificial timeout — the SDK fires onTimeout when the session expires.
|
|
238
|
+
function renderWhenReady(sdk: PaymentClientSdk, intent: PaymentIntent) {
|
|
239
|
+
const global = (window as any)[sdk.globalName!];
|
|
240
|
+
if (!global) return;
|
|
241
|
+
|
|
242
|
+
const renderMethod = sdk.renderMethod || 'renderPaymentOptions';
|
|
243
|
+
const renderArg = sdk.renderArg || intent.clientSecret;
|
|
244
|
+
let renderCalled = false;
|
|
245
|
+
|
|
246
|
+
function attempt() {
|
|
247
|
+
if (walletOpenRef.current) return;
|
|
248
|
+
try {
|
|
249
|
+
global[renderMethod](renderArg);
|
|
250
|
+
renderCalled = true;
|
|
251
|
+
console.info('Payment SDK: render call accepted');
|
|
252
|
+
} catch (err) {
|
|
253
|
+
console.info('Payment SDK: render not ready yet, retrying...');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Try immediately, then keep retrying until onWalletChange('open') fires
|
|
258
|
+
attempt();
|
|
259
|
+
const id = setInterval(() => {
|
|
260
|
+
if (walletOpenRef.current) {
|
|
261
|
+
clearInterval(id);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!renderCalled) attempt();
|
|
265
|
+
}, 500);
|
|
266
|
+
cleanups.push(() => clearInterval(id));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// =============================================
|
|
270
|
+
// Execution flow
|
|
271
|
+
// =============================================
|
|
272
|
+
|
|
273
|
+
// A) Get SDK config from providers (fast, no wallet timer)
|
|
274
|
+
const providerPromise = client
|
|
275
|
+
.getPaymentProviders()
|
|
276
|
+
.then((res) => {
|
|
277
|
+
const sdk = res.defaultProvider?.clientSdk;
|
|
278
|
+
if (sdk) setPreloadedSdk(sdk);
|
|
279
|
+
return sdk || null;
|
|
280
|
+
})
|
|
281
|
+
.catch(() => null);
|
|
282
|
+
|
|
283
|
+
// B) Load + init SDK as early as possible
|
|
284
|
+
providerPromise.then((providerSdk) => {
|
|
285
|
+
if (providerSdk?.renderType === 'sdk-widget' && providerSdk.scriptUrl) {
|
|
286
|
+
currentSdk = providerSdk;
|
|
287
|
+
loadScript(providerSdk);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// C) Create payment intent (starts wallet timer)
|
|
292
|
+
const intentPromise = client
|
|
293
|
+
.createPaymentIntent(checkoutId, { successUrl, cancelUrl })
|
|
294
|
+
.then((intent) => {
|
|
295
|
+
setPaymentIntent(intent);
|
|
296
|
+
return intent;
|
|
297
|
+
})
|
|
298
|
+
.catch((err) => {
|
|
299
|
+
setError(err instanceof Error ? err.message : t('paymentError'));
|
|
300
|
+
return null;
|
|
301
|
+
})
|
|
302
|
+
.finally(() => setLoading(false));
|
|
303
|
+
|
|
304
|
+
// D) When both ready: resolve final SDK config and render
|
|
305
|
+
Promise.all([providerPromise, intentPromise]).then(([providerSdk, intent]) => {
|
|
306
|
+
if (!intent) return;
|
|
307
|
+
|
|
308
|
+
const sdk = resolveClientSdk(intent, providerSdk);
|
|
309
|
+
currentSdk = sdk;
|
|
310
|
+
|
|
311
|
+
if (sdk.renderType === 'redirect') {
|
|
312
|
+
window.location.href = intent.clientSecret;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (sdk.renderType !== 'sdk-widget' || !sdk.globalName) return;
|
|
316
|
+
|
|
317
|
+
// If SDK wasn't loaded from providers, load + init now
|
|
318
|
+
if (!sdkInitDone) {
|
|
319
|
+
loadScript(sdk);
|
|
320
|
+
// Wait for init to complete, then render
|
|
321
|
+
const id = setInterval(() => {
|
|
322
|
+
if (sdkInitDone) {
|
|
323
|
+
clearInterval(id);
|
|
324
|
+
renderWhenReady(sdk, intent);
|
|
325
|
+
}
|
|
326
|
+
}, 100);
|
|
327
|
+
cleanups.push(() => clearInterval(id));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Re-init with final config if environment changed
|
|
332
|
+
if (sdk.initConfig?.environment && currentSdk) {
|
|
333
|
+
const global = (window as any)[sdk.globalName];
|
|
334
|
+
if (global) {
|
|
335
|
+
const method = sdk.initMethod || 'init';
|
|
336
|
+
global[method]({
|
|
337
|
+
...(sdk.initConfig || {}),
|
|
338
|
+
events: {
|
|
339
|
+
onSuccess: (r: unknown) => cbRef.current.onSuccess(r),
|
|
340
|
+
onFailure: (r: unknown) => cbRef.current.onFailure(r),
|
|
341
|
+
onError: (r: unknown) => cbRef.current.onError(r),
|
|
342
|
+
onTimeout: () => cbRef.current.onTimeout(),
|
|
343
|
+
onWalletChange: (s: string) => cbRef.current.onWalletChange(s),
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// SDK ready — render
|
|
350
|
+
renderWhenReady(sdk, intent);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return () => cleanups.forEach((fn) => fn());
|
|
354
|
+
}, [checkoutId]);
|
|
355
|
+
|
|
356
|
+
// --- UI ---
|
|
357
|
+
|
|
358
|
+
if (loading) {
|
|
359
|
+
return (
|
|
360
|
+
<div className={cn('flex flex-col items-center justify-center py-12', className)}>
|
|
361
|
+
<LoadingSpinner size="lg" />
|
|
362
|
+
<p className="text-muted-foreground mt-4 text-sm">{t('preparingPayment')}</p>
|
|
363
|
+
</div>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (error) {
|
|
368
|
+
const isNotConfigured =
|
|
369
|
+
error.toLowerCase().includes('not configured') ||
|
|
370
|
+
error.toLowerCase().includes('no payment') ||
|
|
371
|
+
error.toLowerCase().includes('provider');
|
|
372
|
+
return (
|
|
373
|
+
<div className={cn('py-12 text-center', className)}>
|
|
374
|
+
<svg
|
|
375
|
+
className="text-muted-foreground mx-auto mb-4 h-12 w-12"
|
|
376
|
+
fill="none"
|
|
377
|
+
viewBox="0 0 24 24"
|
|
378
|
+
stroke="currentColor"
|
|
379
|
+
>
|
|
380
|
+
<path
|
|
381
|
+
strokeLinecap="round"
|
|
382
|
+
strokeLinejoin="round"
|
|
383
|
+
strokeWidth={1.5}
|
|
384
|
+
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"
|
|
385
|
+
/>
|
|
386
|
+
</svg>
|
|
387
|
+
<h3 className="text-foreground mb-2 text-lg font-semibold">
|
|
388
|
+
{isNotConfigured ? t('paymentNotConfigured') : t('paymentError')}
|
|
389
|
+
</h3>
|
|
390
|
+
<p className="text-muted-foreground mx-auto max-w-md text-sm">
|
|
391
|
+
{isNotConfigured ? t('paymentNotConfiguredDesc') : error}
|
|
392
|
+
</p>
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!paymentIntent) return null;
|
|
398
|
+
|
|
399
|
+
const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
|
|
400
|
+
|
|
401
|
+
if (sdk.renderType === 'sdk-widget') {
|
|
402
|
+
const containerId =
|
|
403
|
+
sdk.containerId || `${paymentIntent.provider || 'payment'}-payment-container`;
|
|
404
|
+
return (
|
|
405
|
+
<div className={cn('py-4', className)}>
|
|
406
|
+
{!sdkReady && (
|
|
407
|
+
<div className="flex flex-col items-center justify-center py-8">
|
|
408
|
+
<LoadingSpinner size="lg" />
|
|
409
|
+
<p className="text-muted-foreground mt-4 text-sm">{t('loadingPaymentOptions')}</p>
|
|
410
|
+
</div>
|
|
411
|
+
)}
|
|
412
|
+
<div id={containerId} />
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (sdk.renderType === 'iframe') {
|
|
418
|
+
return (
|
|
419
|
+
<div className={cn('py-4', className)}>
|
|
420
|
+
<iframe
|
|
421
|
+
src={paymentIntent.clientSecret}
|
|
422
|
+
className="w-full border-0"
|
|
423
|
+
style={{ minHeight: '500px' }}
|
|
424
|
+
title={t('payment')}
|
|
425
|
+
allow="payment"
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<div className={cn('flex flex-col items-center justify-center py-12', className)}>
|
|
433
|
+
<LoadingSpinner size="lg" />
|
|
434
|
+
<p className="text-muted-foreground mt-4 text-sm">{t('redirectingToPayment')}</p>
|
|
435
|
+
<p className="text-muted-foreground mt-2 text-xs">
|
|
436
|
+
{t('redirectingHint')}
|
|
437
|
+
<a href={paymentIntent.clientSecret} className="text-primary hover:underline">
|
|
438
|
+
{t('clickHere')}
|
|
439
|
+
</a>
|
|
440
|
+
.
|
|
441
|
+
</p>
|
|
442
|
+
</div>
|
|
443
|
+
);
|
|
444
|
+
}
|