create-brainerce-store 1.11.1 → 1.11.2
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/messages/en.json +2 -1
- package/messages/he.json +2 -1
- package/package.json +1 -1
- package/templates/nextjs/base/next.config.ts +31 -31
- package/templates/nextjs/base/src/app/checkout/page.tsx +666 -666
- package/templates/nextjs/base/src/app/globals.css +1 -0
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +534 -379
|
@@ -1,379 +1,534 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
resolve(); //
|
|
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
|
-
|
|
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
|
-
const
|
|
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
|
-
const
|
|
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
|
-
window.
|
|
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
|
-
if (
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
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 for stores running an older backend that doesn't
|
|
12
|
+
* return `clientSdk` in the PaymentIntent response.
|
|
13
|
+
* Remove once all backends are updated.
|
|
14
|
+
*/
|
|
15
|
+
const LEGACY_GROW_SDK: PaymentClientSdk = {
|
|
16
|
+
renderType: 'sdk-widget',
|
|
17
|
+
scriptUrl: 'https://cdn.meshulam.co.il/sdk/gs.min.js',
|
|
18
|
+
scriptIntegrity:
|
|
19
|
+
'sha384-e1OYzZERZLgbR8Zw1I4Ww/J7qXGyEsZb7LF0z5W/sbnTsFwtxop7sKZsLGNp6CB1',
|
|
20
|
+
globalName: 'growPayment',
|
|
21
|
+
initMethod: 'init',
|
|
22
|
+
renderMethod: 'renderPaymentOptions',
|
|
23
|
+
containerId: 'grow-payment-container',
|
|
24
|
+
initConfig: { version: 1 },
|
|
25
|
+
additionalScripts: [
|
|
26
|
+
{
|
|
27
|
+
url: 'https://meshulam.co.il/_media/js/apple_pay_sdk/sdk.min.js',
|
|
28
|
+
integrity:
|
|
29
|
+
'sha384-CG67HXmWBeyuRR/jqFsYr6dGEMFyuZw3/hRmcyACJRYHGb/1ebEkQZyzdJXDc9/u',
|
|
30
|
+
optional: true,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
bodyStyles:
|
|
34
|
+
'[id*="Gr0W8-"],[id*="Gr0W8-"] *,[class*="Gr0W8-"],[class*="Gr0W8-"] *{direction:ltr !important;text-align:left}',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
interface PaymentStepProps {
|
|
38
|
+
checkoutId: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load a script tag if not already present in the DOM.
|
|
44
|
+
* Resolves when the script loads (or immediately if already present).
|
|
45
|
+
*/
|
|
46
|
+
function loadScript(src: string, optional = false, integrity?: string): Promise<void> {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
if (document.querySelector(`script[src="${src}"]`)) {
|
|
49
|
+
resolve();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const script = document.createElement('script');
|
|
53
|
+
script.src = src;
|
|
54
|
+
script.async = true;
|
|
55
|
+
if (integrity) {
|
|
56
|
+
script.integrity = integrity;
|
|
57
|
+
script.crossOrigin = 'anonymous';
|
|
58
|
+
}
|
|
59
|
+
script.onload = () => resolve();
|
|
60
|
+
script.onerror = () => {
|
|
61
|
+
if (optional) {
|
|
62
|
+
resolve(); // Non-blocking for optional SDKs
|
|
63
|
+
} else {
|
|
64
|
+
resolve(); // Still resolve — caller handles missing global
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
document.head.appendChild(script);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load a stylesheet if not already present in the DOM.
|
|
73
|
+
*/
|
|
74
|
+
function loadStylesheet(href: string): void {
|
|
75
|
+
if (document.querySelector(`link[href="${href}"]`)) return;
|
|
76
|
+
const link = document.createElement('link');
|
|
77
|
+
link.rel = 'stylesheet';
|
|
78
|
+
link.href = href;
|
|
79
|
+
document.head.appendChild(link);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* After the SDK runtime script loads, ensure its CSS is also loaded.
|
|
84
|
+
* Some SDKs load CSS dynamically but may fail silently if the CDN path
|
|
85
|
+
* doesn't match. Detect the runtime version from loaded scripts and
|
|
86
|
+
* force-load the CSS if missing.
|
|
87
|
+
*/
|
|
88
|
+
function ensureSdkCss(sdkScriptUrl: string): void {
|
|
89
|
+
const scripts = document.querySelectorAll('script[src]');
|
|
90
|
+
const cdnBase = new URL(sdkScriptUrl).origin;
|
|
91
|
+
|
|
92
|
+
for (const el of scripts) {
|
|
93
|
+
const src = (el as HTMLScriptElement).src;
|
|
94
|
+
const match = src.match(
|
|
95
|
+
new RegExp(`${cdnBase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/sdk/([\\d.]+)/js/`)
|
|
96
|
+
);
|
|
97
|
+
if (match) {
|
|
98
|
+
const version = match[1];
|
|
99
|
+
const cssUrl = `${cdnBase}/sdk/${version}/css/w.css`;
|
|
100
|
+
if (!document.querySelector(`link[href*="/sdk/${version}/css/"]`)) {
|
|
101
|
+
console.info(`Payment SDK: force-loading CSS from ${cssUrl}`);
|
|
102
|
+
loadStylesheet(cssUrl);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Wait for a global variable to become available on `window`.
|
|
111
|
+
*/
|
|
112
|
+
function waitForGlobal(globalName: string, timeoutMs = 5000): Promise<boolean> {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
if ((window as any)[globalName]) {
|
|
115
|
+
resolve(true);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const start = Date.now();
|
|
119
|
+
const check = setInterval(() => {
|
|
120
|
+
if ((window as any)[globalName]) {
|
|
121
|
+
clearInterval(check);
|
|
122
|
+
resolve(true);
|
|
123
|
+
} else if (Date.now() - start > timeoutMs) {
|
|
124
|
+
clearInterval(check);
|
|
125
|
+
resolve(false);
|
|
126
|
+
}
|
|
127
|
+
}, 50);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the clientSdk config from multiple sources (in priority order):
|
|
133
|
+
* 1. Preloaded provider config (from getPaymentProviders)
|
|
134
|
+
* 2. Intent's clientSdk (returned by createPaymentIntent)
|
|
135
|
+
* 3. Legacy hardcoded defaults (backward compat)
|
|
136
|
+
*/
|
|
137
|
+
function resolveClientSdk(
|
|
138
|
+
intent: PaymentIntent | null,
|
|
139
|
+
preloadedSdk?: PaymentClientSdk | null
|
|
140
|
+
): PaymentClientSdk {
|
|
141
|
+
if (preloadedSdk) return preloadedSdk;
|
|
142
|
+
if (intent?.clientSdk) return intent.clientSdk;
|
|
143
|
+
|
|
144
|
+
// Legacy fallback for older backends that don't return clientSdk
|
|
145
|
+
if (intent?.provider === 'grow') return LEGACY_GROW_SDK;
|
|
146
|
+
|
|
147
|
+
// Default: treat as redirect (Stripe, PayPal, unknown)
|
|
148
|
+
return { renderType: 'redirect' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extract message string from an SDK callback response.
|
|
153
|
+
* Handles both string responses and object responses with a `message` field.
|
|
154
|
+
*/
|
|
155
|
+
function extractMessage(response: unknown): string {
|
|
156
|
+
if (typeof response === 'string') return response;
|
|
157
|
+
return (response as { message?: string })?.message || '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Load all SDK scripts for a given clientSdk config.
|
|
162
|
+
* Returns true if the global became available, false otherwise.
|
|
163
|
+
*/
|
|
164
|
+
async function loadSdkScripts(sdkConfig: PaymentClientSdk): Promise<boolean> {
|
|
165
|
+
if (!sdkConfig.scriptUrl || !sdkConfig.globalName) return false;
|
|
166
|
+
|
|
167
|
+
// Load additional scripts first (e.g., Apple Pay SDK)
|
|
168
|
+
if (sdkConfig.additionalScripts) {
|
|
169
|
+
await Promise.all(
|
|
170
|
+
sdkConfig.additionalScripts.map((s) =>
|
|
171
|
+
loadScript(s.url, s.optional ?? true, s.integrity)
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Load main SDK script
|
|
177
|
+
await loadScript(sdkConfig.scriptUrl, false, sdkConfig.scriptIntegrity);
|
|
178
|
+
|
|
179
|
+
// Wait for the global to be set by the script
|
|
180
|
+
const available = await waitForGlobal(sdkConfig.globalName);
|
|
181
|
+
|
|
182
|
+
// Ensure CSS is loaded
|
|
183
|
+
if (available) {
|
|
184
|
+
ensureSdkCss(sdkConfig.scriptUrl);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return available;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
191
|
+
const t = useTranslations('checkout');
|
|
192
|
+
const [paymentIntent, setPaymentIntent] = useState<PaymentIntent | null>(null);
|
|
193
|
+
const [preloadedSdk, setPreloadedSdk] = useState<PaymentClientSdk | null>(null);
|
|
194
|
+
const [loading, setLoading] = useState(true);
|
|
195
|
+
const [error, setError] = useState<string | null>(null);
|
|
196
|
+
const [sdkReady, setSdkReady] = useState(false);
|
|
197
|
+
const [sdkScriptLoaded, setSdkScriptLoaded] = useState(false);
|
|
198
|
+
const renderAttempted = useRef(false);
|
|
199
|
+
const initialized = useRef(false);
|
|
200
|
+
|
|
201
|
+
const handleSdkPaymentSuccess = useCallback(
|
|
202
|
+
async (response: unknown) => {
|
|
203
|
+
console.info('Payment SDK success:', JSON.stringify(response));
|
|
204
|
+
try {
|
|
205
|
+
const client = getClient();
|
|
206
|
+
const resp = response as Record<string, unknown>;
|
|
207
|
+
const data = (resp?.data && typeof resp.data === 'object' ? resp.data : resp) as
|
|
208
|
+
| Record<string, unknown>
|
|
209
|
+
| undefined;
|
|
210
|
+
await client.confirmSdkPayment(checkoutId, data || undefined);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.warn('Failed to confirm payment with backend:', err);
|
|
213
|
+
}
|
|
214
|
+
window.location.href = `/order-confirmation?checkout_id=${checkoutId}`;
|
|
215
|
+
},
|
|
216
|
+
[checkoutId]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const handleSdkPaymentFailure = useCallback((response: unknown) => {
|
|
220
|
+
console.error('Payment SDK failure:', response);
|
|
221
|
+
const msg = extractMessage(response) || t('paymentError');
|
|
222
|
+
setError(msg);
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
const handleSdkPaymentError = useCallback((response: unknown) => {
|
|
226
|
+
const TRANSIENT_SDK_ERRORS = [
|
|
227
|
+
'Wallet not initialized',
|
|
228
|
+
"SDK was not loaded as needed and therefore can't run",
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const msg = extractMessage(response);
|
|
232
|
+
if (TRANSIENT_SDK_ERRORS.some((e) => msg.includes(e))) {
|
|
233
|
+
console.info('Payment SDK: transient startup error, waiting for retry...', msg);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
console.error('Payment SDK error:', response);
|
|
237
|
+
setError(msg || t('paymentError'));
|
|
238
|
+
}, []);
|
|
239
|
+
|
|
240
|
+
const handleSdkPaymentTimeout = useCallback(() => {
|
|
241
|
+
console.warn('Payment SDK: wallet timed out');
|
|
242
|
+
setError(t('paymentTimedOut'));
|
|
243
|
+
}, []);
|
|
244
|
+
|
|
245
|
+
// Step 0+1: On mount, fire BOTH getPaymentProviders() and createPaymentIntent() in parallel.
|
|
246
|
+
// getPaymentProviders() resolves the clientSdk config so we can start loading scripts
|
|
247
|
+
// immediately, without waiting for createPaymentIntent() (which triggers the wallet timer).
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (initialized.current) return;
|
|
250
|
+
initialized.current = true;
|
|
251
|
+
|
|
252
|
+
const client = getClient();
|
|
253
|
+
const successUrl = `${window.location.origin}/order-confirmation?checkout_id=${checkoutId}`;
|
|
254
|
+
const cancelUrl = `${window.location.origin}/checkout?checkout_id=${checkoutId}&canceled=true`;
|
|
255
|
+
|
|
256
|
+
// Fire both requests in parallel
|
|
257
|
+
const providerPromise = client
|
|
258
|
+
.getPaymentProviders()
|
|
259
|
+
.then((config) => {
|
|
260
|
+
const sdk = config.defaultProvider?.clientSdk;
|
|
261
|
+
if (sdk) setPreloadedSdk(sdk);
|
|
262
|
+
return sdk || null;
|
|
263
|
+
})
|
|
264
|
+
.catch((err) => {
|
|
265
|
+
console.warn('Failed to preload payment providers:', err);
|
|
266
|
+
return null;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const intentPromise = client
|
|
270
|
+
.createPaymentIntent(checkoutId, { successUrl, cancelUrl })
|
|
271
|
+
.then((intent) => {
|
|
272
|
+
setPaymentIntent(intent);
|
|
273
|
+
return intent;
|
|
274
|
+
})
|
|
275
|
+
.catch((err) => {
|
|
276
|
+
const message = err instanceof Error ? err.message : t('paymentError');
|
|
277
|
+
setError(message);
|
|
278
|
+
return null;
|
|
279
|
+
})
|
|
280
|
+
.finally(() => setLoading(false));
|
|
281
|
+
|
|
282
|
+
// Start loading SDK scripts as soon as we know the provider config
|
|
283
|
+
providerPromise.then(async (sdk) => {
|
|
284
|
+
if (!sdk || sdk.renderType !== 'sdk-widget' || !sdk.scriptUrl) return;
|
|
285
|
+
|
|
286
|
+
const available = await loadSdkScripts(sdk);
|
|
287
|
+
if (!available) {
|
|
288
|
+
// Don't error yet — wait for the intent to confirm provider type
|
|
289
|
+
console.warn('Payment SDK: preload failed, will retry from intent');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
setSdkScriptLoaded(true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Handle redirect providers — need to wait for the intent
|
|
296
|
+
intentPromise.then((intent) => {
|
|
297
|
+
if (!intent) return;
|
|
298
|
+
// We don't have preloaded SDK yet — check intent
|
|
299
|
+
const sdk = intent.clientSdk || (intent.provider === 'grow' ? LEGACY_GROW_SDK : null);
|
|
300
|
+
if (!sdk || sdk.renderType === 'redirect') {
|
|
301
|
+
window.location.href = intent.clientSecret;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}, [checkoutId]);
|
|
305
|
+
|
|
306
|
+
// Step 2: Fallback — if preloading didn't work (no providers endpoint, or different provider
|
|
307
|
+
// than expected), load scripts from the intent's clientSdk.
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (sdkScriptLoaded || !paymentIntent) return;
|
|
310
|
+
|
|
311
|
+
const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
|
|
312
|
+
if (sdk.renderType !== 'sdk-widget' || !sdk.scriptUrl) return;
|
|
313
|
+
|
|
314
|
+
let cancelled = false;
|
|
315
|
+
|
|
316
|
+
loadSdkScripts(sdk).then((available) => {
|
|
317
|
+
if (cancelled) return;
|
|
318
|
+
if (!available) {
|
|
319
|
+
setError(t('failedToLoadPaymentSdk'));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
setSdkScriptLoaded(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return () => {
|
|
326
|
+
cancelled = true;
|
|
327
|
+
};
|
|
328
|
+
}, [paymentIntent, sdkScriptLoaded, preloadedSdk]);
|
|
329
|
+
|
|
330
|
+
// Step 3: Inject bodyStyles CSS when SDK is active (for RTL overlay fixes)
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
|
|
333
|
+
if (!sdk.bodyStyles) return;
|
|
334
|
+
|
|
335
|
+
const style = document.createElement('style');
|
|
336
|
+
style.setAttribute('data-payment-sdk', 'true');
|
|
337
|
+
style.textContent = sdk.bodyStyles;
|
|
338
|
+
document.head.appendChild(style);
|
|
339
|
+
|
|
340
|
+
return () => {
|
|
341
|
+
style.remove();
|
|
342
|
+
};
|
|
343
|
+
}, [paymentIntent, preloadedSdk]);
|
|
344
|
+
|
|
345
|
+
// Step 4: When BOTH SDK script is loaded AND payment intent is ready:
|
|
346
|
+
// - call initMethod with events + initConfig
|
|
347
|
+
// - poll/wait then call renderMethod
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
if (!sdkScriptLoaded || !paymentIntent) return;
|
|
350
|
+
|
|
351
|
+
const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
|
|
352
|
+
if (sdk.renderType !== 'sdk-widget') return;
|
|
353
|
+
if (!sdk.globalName) return;
|
|
354
|
+
if (renderAttempted.current) return;
|
|
355
|
+
|
|
356
|
+
const sdkGlobal = (window as any)[sdk.globalName];
|
|
357
|
+
if (!sdkGlobal) return;
|
|
358
|
+
|
|
359
|
+
renderAttempted.current = true;
|
|
360
|
+
|
|
361
|
+
const initMethod = sdk.initMethod || 'init';
|
|
362
|
+
const renderMethod = sdk.renderMethod || 'renderPaymentOptions';
|
|
363
|
+
const renderArg = sdk.renderArg || paymentIntent.clientSecret;
|
|
364
|
+
|
|
365
|
+
let rendered = false;
|
|
366
|
+
let walletReady = false;
|
|
367
|
+
let pollId: ReturnType<typeof setInterval>;
|
|
368
|
+
|
|
369
|
+
function tryRender() {
|
|
370
|
+
if (rendered) return;
|
|
371
|
+
try {
|
|
372
|
+
sdkGlobal[renderMethod](renderArg);
|
|
373
|
+
rendered = true;
|
|
374
|
+
clearInterval(pollId);
|
|
375
|
+
setSdkReady(true);
|
|
376
|
+
console.info(`Payment SDK: ${renderMethod} succeeded`);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.info(`Payment SDK: ${renderMethod} not ready yet`, err);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Build init config: merge static manifest config with runtime events
|
|
383
|
+
// Environment is already in sdk.initConfig (merged by backend from provider's runtime override)
|
|
384
|
+
const initConfig: Record<string, unknown> = {
|
|
385
|
+
...(sdk.initConfig || {}),
|
|
386
|
+
events: {
|
|
387
|
+
onSuccess: handleSdkPaymentSuccess,
|
|
388
|
+
onFailure: handleSdkPaymentFailure,
|
|
389
|
+
onError: handleSdkPaymentError,
|
|
390
|
+
onTimeout: handleSdkPaymentTimeout,
|
|
391
|
+
onWalletChange: (state: string) => {
|
|
392
|
+
console.info('Payment SDK wallet state:', state);
|
|
393
|
+
walletReady = true;
|
|
394
|
+
tryRender();
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
console.info(`Payment SDK: ${initMethod} with config:`, initConfig);
|
|
400
|
+
sdkGlobal[initMethod](initConfig);
|
|
401
|
+
|
|
402
|
+
// Poll as fallback — respect SDK's readiness signal:
|
|
403
|
+
// - First 4s: only render if onWalletChange has fired
|
|
404
|
+
// - After 4s: force-attempt even without wallet signal
|
|
405
|
+
const WALLET_GRACE_PERIOD = 4000;
|
|
406
|
+
const initTime = Date.now();
|
|
407
|
+
|
|
408
|
+
const timeoutId = setTimeout(() => {
|
|
409
|
+
if (walletReady) tryRender();
|
|
410
|
+
if (!rendered) {
|
|
411
|
+
let attempts = 0;
|
|
412
|
+
const maxAttempts = 16; // 16 * 500ms = 8s after initial 1s delay
|
|
413
|
+
pollId = setInterval(() => {
|
|
414
|
+
attempts++;
|
|
415
|
+
const elapsed = Date.now() - initTime;
|
|
416
|
+
if (walletReady || elapsed > WALLET_GRACE_PERIOD) {
|
|
417
|
+
tryRender();
|
|
418
|
+
}
|
|
419
|
+
if (!rendered && attempts >= maxAttempts) {
|
|
420
|
+
clearInterval(pollId);
|
|
421
|
+
console.error(`Payment SDK: ${renderMethod} failed after max attempts`);
|
|
422
|
+
setError(t('paymentError'));
|
|
423
|
+
}
|
|
424
|
+
}, 500);
|
|
425
|
+
}
|
|
426
|
+
}, 1000);
|
|
427
|
+
|
|
428
|
+
return () => {
|
|
429
|
+
clearTimeout(timeoutId);
|
|
430
|
+
clearInterval(pollId);
|
|
431
|
+
};
|
|
432
|
+
}, [
|
|
433
|
+
sdkScriptLoaded,
|
|
434
|
+
paymentIntent,
|
|
435
|
+
preloadedSdk,
|
|
436
|
+
handleSdkPaymentSuccess,
|
|
437
|
+
handleSdkPaymentFailure,
|
|
438
|
+
handleSdkPaymentError,
|
|
439
|
+
handleSdkPaymentTimeout,
|
|
440
|
+
]);
|
|
441
|
+
|
|
442
|
+
// --- Render ---
|
|
443
|
+
|
|
444
|
+
if (loading) {
|
|
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
|
+
}
|