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