@tagadapay/plugin-sdk 3.1.22 → 3.1.25
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/build-cdn.js +274 -6
- package/dist/external-tracker.js +476 -6774
- package/dist/external-tracker.min.js +2 -25
- package/dist/external-tracker.min.js.map +4 -4
- package/dist/react/config/payment.d.ts +14 -4
- package/dist/react/config/payment.js +47 -9
- package/dist/react/hooks/useCheckout.d.ts +3 -0
- package/dist/react/hooks/useCheckout.js +11 -3
- package/dist/react/hooks/usePluginConfig.js +9 -10
- package/dist/react/providers/TagadaProvider.js +1 -1
- package/dist/tagada-react-sdk-minimal.min.js +36 -0
- package/dist/tagada-react-sdk-minimal.min.js.map +7 -0
- package/dist/tagada-react-sdk.js +37988 -0
- package/dist/tagada-react-sdk.min.js +78 -0
- package/dist/tagada-react-sdk.min.js.map +7 -0
- package/dist/tagada-sdk.js +7847 -6420
- package/dist/tagada-sdk.min.js +4 -22
- package/dist/tagada-sdk.min.js.map +4 -4
- package/dist/v2/cdn-react-minimal.d.ts +23 -0
- package/dist/v2/cdn-react-minimal.js +26 -0
- package/dist/v2/core/client.js +2 -1
- package/dist/v2/core/config/environment.js +2 -1
- package/dist/v2/core/funnelClient.d.ts +106 -10
- package/dist/v2/core/funnelClient.js +122 -28
- package/dist/v2/core/index.d.ts +0 -1
- package/dist/v2/core/index.js +0 -2
- package/dist/v2/core/isoData.d.ts +4 -4
- package/dist/v2/core/isoData.js +7 -7
- package/dist/v2/core/pixelMapping.js +64 -26
- package/dist/v2/core/resources/apiClient.d.ts +18 -14
- package/dist/v2/core/resources/apiClient.js +151 -109
- package/dist/v2/core/resources/checkout.d.ts +10 -0
- package/dist/v2/core/resources/checkout.js +6 -0
- package/dist/v2/core/resources/expressPaymentMethods.d.ts +1 -0
- package/dist/v2/core/resources/index.d.ts +1 -1
- package/dist/v2/core/resources/index.js +1 -1
- package/dist/v2/core/resources/offers.js +4 -4
- package/dist/v2/core/resources/payments.d.ts +8 -2
- package/dist/v2/core/resources/payments.js +1 -0
- package/dist/v2/core/resources/postPurchases.d.ts +17 -0
- package/dist/v2/core/resources/postPurchases.js +20 -0
- package/dist/v2/core/utils/currency.d.ts +3 -0
- package/dist/v2/core/utils/currency.js +40 -2
- package/dist/v2/core/utils/deviceInfo.d.ts +1 -10
- package/dist/v2/core/utils/deviceInfo.js +153 -76
- package/dist/v2/core/utils/order.d.ts +2 -0
- package/dist/v2/core/utils/pluginConfig.js +18 -22
- package/dist/v2/core/utils/previewMode.js +12 -0
- package/dist/v2/index.d.ts +4 -3
- package/dist/v2/index.js +4 -2
- package/dist/v2/react/components/ApplePayButton.js +39 -16
- package/dist/v2/react/components/FunnelScriptInjector.js +145 -77
- package/dist/v2/react/components/StripeExpressButton.d.ts +13 -0
- package/dist/v2/react/components/StripeExpressButton.js +170 -0
- package/dist/v2/react/components/WhopCheckout.js +7 -1
- package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +1 -0
- package/dist/v2/react/hooks/payment-actions/useProcessorAuthAction.js +21 -3
- package/dist/v2/react/hooks/useApiQuery.d.ts +1 -1
- package/dist/v2/react/hooks/useApiQuery.js +1 -1
- package/dist/v2/react/hooks/useApplePayCheckout.js +8 -8
- package/dist/v2/react/hooks/useCheckoutQuery.d.ts +10 -0
- package/dist/v2/react/hooks/useCheckoutQuery.js +27 -15
- package/dist/v2/react/hooks/useFunnel.d.ts +15 -4
- package/dist/v2/react/hooks/useFunnel.js +8 -4
- package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +2 -0
- package/dist/v2/react/hooks/useGoogleAutocomplete.js +29 -15
- package/dist/v2/react/hooks/useISOData.d.ts +2 -5
- package/dist/v2/react/hooks/useISOData.js +25 -26
- package/dist/v2/react/hooks/usePaymentPolling.d.ts +2 -2
- package/dist/v2/react/hooks/usePixelTracking.js +151 -70
- package/dist/v2/react/hooks/usePostPurchasesQuery.js +34 -2
- package/dist/v2/react/hooks/usePreviewOffer.js +1 -1
- package/dist/v2/react/hooks/useRemappableParams.d.ts +2 -6
- package/dist/v2/react/hooks/useRemappableParams.js +23 -23
- package/dist/v2/react/hooks/useSetPaymentMethod.d.ts +16 -0
- package/dist/v2/react/hooks/useSetPaymentMethod.js +33 -0
- package/dist/v2/react/hooks/useStepConfig.d.ts +23 -6
- package/dist/v2/react/hooks/useStepConfig.js +14 -7
- package/dist/v2/react/hooks/useTranslation.js +23 -8
- package/dist/v2/react/index.d.ts +8 -1
- package/dist/v2/react/index.js +3 -0
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +8 -0
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +106 -10
- package/dist/v2/react/providers/TagadaProvider.js +5 -5
- package/dist/v2/standalone/index.d.ts +21 -3
- package/dist/v2/standalone/index.js +25 -3
- package/dist/v2/standalone/payment-service.d.ts +134 -0
- package/dist/v2/standalone/payment-service.js +929 -0
- package/package.json +4 -2
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PaymentService (Standalone / Vanilla JS)
|
|
3
|
+
*
|
|
4
|
+
* Complete standalone equivalent of the React SDK's payment system:
|
|
5
|
+
* - usePaymentQuery (orchestration)
|
|
6
|
+
* - usePaymentPolling (status polling)
|
|
7
|
+
* - usePaymentActionHandler (action routing)
|
|
8
|
+
* - usePaymentProcessors (card, APM, Apple Pay, Google Pay, saved instruments)
|
|
9
|
+
* - usePaymentInstruments (instrument creation)
|
|
10
|
+
* - useThreeds (3DS session + challenge)
|
|
11
|
+
* - useGenericPaymentReturn (generic redirect return)
|
|
12
|
+
* - useAirwallex3dsReturn (Airwallex-specific redirect return)
|
|
13
|
+
* - All action handlers: redirect, threeds_auth, processor_auth, error,
|
|
14
|
+
* kesspay_auth, trustflow_auth, finix_radar, stripe_radar, airwallex radar,
|
|
15
|
+
* mastercard_auth
|
|
16
|
+
*
|
|
17
|
+
* No React. No hooks. No DOM framework dependency.
|
|
18
|
+
*/
|
|
19
|
+
import { PaymentsResource, } from '../core/resources/payments';
|
|
20
|
+
import { ThreedsResource } from '../core/resources/threeds';
|
|
21
|
+
import { StoreConfigResource } from '../core/resources/storeConfig';
|
|
22
|
+
import { getAssignedPaymentFlowId } from '../core/funnelClient';
|
|
23
|
+
import { getBasisTheoryApiKey } from '../../react/config/payment';
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// PAYMENT SERVICE
|
|
26
|
+
// =============================================================================
|
|
27
|
+
export class PaymentService {
|
|
28
|
+
constructor(config) {
|
|
29
|
+
this.callbacks = {};
|
|
30
|
+
// BasisTheory (lazy initialized)
|
|
31
|
+
this.basisTheory = null;
|
|
32
|
+
this.btInitPromise = null;
|
|
33
|
+
// BasisTheory 3DS (lazy initialized)
|
|
34
|
+
this.bt3dsClass = null;
|
|
35
|
+
this.bt3dsInitPromise = null;
|
|
36
|
+
// Store config (cached)
|
|
37
|
+
this.storeConfig = null;
|
|
38
|
+
this.storeConfigPromise = null;
|
|
39
|
+
// Polling state
|
|
40
|
+
this.pollTimer = null;
|
|
41
|
+
this.pollAttempts = 0;
|
|
42
|
+
this.isPollingActive = false;
|
|
43
|
+
// Redirect return guard
|
|
44
|
+
this.redirectReturnProcessed = false;
|
|
45
|
+
this.paymentsResource = new PaymentsResource(config.apiClient);
|
|
46
|
+
this.threedsResource = new ThreedsResource(config.apiClient);
|
|
47
|
+
this.storeConfigResource = new StoreConfigResource(config.apiClient);
|
|
48
|
+
this.storeId = config.storeId;
|
|
49
|
+
}
|
|
50
|
+
/** Expose paymentsResource for advanced consumers */
|
|
51
|
+
get payments() {
|
|
52
|
+
return this.paymentsResource;
|
|
53
|
+
}
|
|
54
|
+
// ==========================================================================
|
|
55
|
+
// LIFECYCLE
|
|
56
|
+
// ==========================================================================
|
|
57
|
+
setCallbacks(callbacks) {
|
|
58
|
+
this.callbacks = callbacks;
|
|
59
|
+
}
|
|
60
|
+
/** Pre-warm BasisTheory + store config (non-blocking). */
|
|
61
|
+
warmup() {
|
|
62
|
+
void this.initBasisTheory();
|
|
63
|
+
void this.initBt3ds();
|
|
64
|
+
if (this.storeId)
|
|
65
|
+
void this.fetchStoreConfig();
|
|
66
|
+
}
|
|
67
|
+
destroy() {
|
|
68
|
+
this.stopPolling();
|
|
69
|
+
this.basisTheory = null;
|
|
70
|
+
this.btInitPromise = null;
|
|
71
|
+
this.bt3dsClass = null;
|
|
72
|
+
this.bt3dsInitPromise = null;
|
|
73
|
+
this.storeConfig = null;
|
|
74
|
+
this.storeConfigPromise = null;
|
|
75
|
+
}
|
|
76
|
+
// ==========================================================================
|
|
77
|
+
// BASIS THEORY
|
|
78
|
+
// ==========================================================================
|
|
79
|
+
initBasisTheory() {
|
|
80
|
+
if (this.basisTheory)
|
|
81
|
+
return Promise.resolve(this.basisTheory);
|
|
82
|
+
if (this.btInitPromise)
|
|
83
|
+
return this.btInitPromise;
|
|
84
|
+
this.btInitPromise = (async () => {
|
|
85
|
+
try {
|
|
86
|
+
const apiKey = getBasisTheoryApiKey();
|
|
87
|
+
const { BasisTheory } = await import('@basis-theory/basis-theory-js');
|
|
88
|
+
const bt = await new BasisTheory().init(apiKey, { elements: false });
|
|
89
|
+
this.basisTheory = bt;
|
|
90
|
+
console.log('[PaymentService] BasisTheory initialized');
|
|
91
|
+
return this.basisTheory;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error('[PaymentService] BasisTheory init failed:', error);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
return this.btInitPromise;
|
|
99
|
+
}
|
|
100
|
+
getBtApiKey() {
|
|
101
|
+
return getBasisTheoryApiKey();
|
|
102
|
+
}
|
|
103
|
+
/** Lazy-load @basis-theory/web-threeds (BasisTheory3ds class) */
|
|
104
|
+
initBt3ds() {
|
|
105
|
+
if (this.bt3dsClass)
|
|
106
|
+
return Promise.resolve(this.bt3dsClass);
|
|
107
|
+
if (this.bt3dsInitPromise)
|
|
108
|
+
return this.bt3dsInitPromise;
|
|
109
|
+
this.bt3dsInitPromise = (async () => {
|
|
110
|
+
try {
|
|
111
|
+
const { BasisTheory3ds } = await import('@basis-theory/web-threeds');
|
|
112
|
+
this.bt3dsClass = BasisTheory3ds;
|
|
113
|
+
console.log('[PaymentService] BasisTheory 3DS loaded');
|
|
114
|
+
return this.bt3dsClass;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('[PaymentService] BasisTheory 3DS load failed:', error);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
return this.bt3dsInitPromise;
|
|
122
|
+
}
|
|
123
|
+
// ==========================================================================
|
|
124
|
+
// STORE CONFIG
|
|
125
|
+
// ==========================================================================
|
|
126
|
+
fetchStoreConfig() {
|
|
127
|
+
if (this.storeConfig)
|
|
128
|
+
return Promise.resolve(this.storeConfig);
|
|
129
|
+
if (this.storeConfigPromise)
|
|
130
|
+
return this.storeConfigPromise;
|
|
131
|
+
if (!this.storeId)
|
|
132
|
+
return Promise.resolve(null);
|
|
133
|
+
this.storeConfigPromise = (async () => {
|
|
134
|
+
try {
|
|
135
|
+
this.storeConfig = await this.storeConfigResource.getStoreConfig(this.storeId);
|
|
136
|
+
return this.storeConfig;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
})();
|
|
142
|
+
return this.storeConfigPromise;
|
|
143
|
+
}
|
|
144
|
+
// ==========================================================================
|
|
145
|
+
// POLLING
|
|
146
|
+
// ==========================================================================
|
|
147
|
+
stopPolling() {
|
|
148
|
+
if (this.pollTimer) {
|
|
149
|
+
clearInterval(this.pollTimer);
|
|
150
|
+
this.pollTimer = null;
|
|
151
|
+
}
|
|
152
|
+
this.isPollingActive = false;
|
|
153
|
+
this.pollAttempts = 0;
|
|
154
|
+
}
|
|
155
|
+
startPolling(paymentId, callbacks, maxAttempts = 20, interval = 1500) {
|
|
156
|
+
this.stopPolling();
|
|
157
|
+
this.isPollingActive = true;
|
|
158
|
+
this.pollAttempts = 0;
|
|
159
|
+
console.log('[PaymentService] Starting payment polling:', paymentId);
|
|
160
|
+
const check = async () => {
|
|
161
|
+
if (!this.isPollingActive)
|
|
162
|
+
return;
|
|
163
|
+
this.pollAttempts++;
|
|
164
|
+
try {
|
|
165
|
+
const payment = await this.paymentsResource.getPaymentStatus(paymentId);
|
|
166
|
+
console.log(`[PaymentService] Poll #${this.pollAttempts}: ${payment.status} / ${payment.subStatus}`);
|
|
167
|
+
if (!this.isPollingActive)
|
|
168
|
+
return;
|
|
169
|
+
if (payment.status === 'succeeded' ||
|
|
170
|
+
(payment.status === 'pending' && payment.subStatus === 'authorized')) {
|
|
171
|
+
this.stopPolling();
|
|
172
|
+
callbacks.onSuccess(payment);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (payment.requireAction !== 'none' && payment.requireActionData && !payment.requireActionData.processed) {
|
|
176
|
+
this.stopPolling();
|
|
177
|
+
callbacks.onRequireAction?.(payment);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (payment.status !== 'succeeded' && payment.status !== 'pending') {
|
|
181
|
+
this.stopPolling();
|
|
182
|
+
callbacks.onFailure(payment.status || 'Payment failed');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (this.pollAttempts >= maxAttempts) {
|
|
186
|
+
this.stopPolling();
|
|
187
|
+
callbacks.onFailure('Payment verification timeout');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
if (this.pollAttempts >= 3) {
|
|
192
|
+
this.stopPolling();
|
|
193
|
+
callbacks.onFailure('Payment verification failed due to network errors');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
void check();
|
|
198
|
+
this.pollTimer = setInterval(() => void check(), interval);
|
|
199
|
+
}
|
|
200
|
+
// ==========================================================================
|
|
201
|
+
// COMMON: processPaymentDirect + result handling
|
|
202
|
+
// ==========================================================================
|
|
203
|
+
/**
|
|
204
|
+
* Internal: call processPaymentDirect and handle requireAction / polling.
|
|
205
|
+
* Shared by processCardPayment, processApplePayPayment, etc.
|
|
206
|
+
*/
|
|
207
|
+
async processAndHandle(checkoutSessionId, paymentInstrumentId, threedsSessionId, extra) {
|
|
208
|
+
const paymentFlowId = getAssignedPaymentFlowId();
|
|
209
|
+
const response = await this.paymentsResource.processPaymentDirect(checkoutSessionId, paymentInstrumentId, threedsSessionId, {
|
|
210
|
+
initiatedBy: extra?.initiatedBy || 'customer',
|
|
211
|
+
source: extra?.source || 'checkout',
|
|
212
|
+
paymentFlowId,
|
|
213
|
+
processorId: extra?.processorId,
|
|
214
|
+
paymentMethod: extra?.paymentMethod,
|
|
215
|
+
});
|
|
216
|
+
console.log('[PaymentService] Payment response:', {
|
|
217
|
+
paymentId: response.payment?.id,
|
|
218
|
+
status: response.payment?.status,
|
|
219
|
+
requireAction: response.payment?.requireAction,
|
|
220
|
+
});
|
|
221
|
+
this.callbacks.onCurrentPaymentId?.(response.payment?.id || null);
|
|
222
|
+
if (response.payment.requireAction !== 'none') {
|
|
223
|
+
await this.handlePaymentAction(response.payment);
|
|
224
|
+
return { success: true, payment: response.payment, order: response.order, redirecting: true };
|
|
225
|
+
}
|
|
226
|
+
if (response.payment.status === 'succeeded') {
|
|
227
|
+
this.callbacks.onProcessing?.(false);
|
|
228
|
+
return { success: true, payment: response.payment, order: response.order };
|
|
229
|
+
}
|
|
230
|
+
return new Promise((resolve) => {
|
|
231
|
+
this.startPolling(response.payment.id, {
|
|
232
|
+
onSuccess: (payment) => {
|
|
233
|
+
this.callbacks.onProcessing?.(false);
|
|
234
|
+
resolve({ success: true, payment, order: response.order });
|
|
235
|
+
},
|
|
236
|
+
onFailure: (error) => {
|
|
237
|
+
this.callbacks.onError?.(error);
|
|
238
|
+
this.callbacks.onProcessing?.(false);
|
|
239
|
+
resolve({ success: false, error });
|
|
240
|
+
},
|
|
241
|
+
onRequireAction: (payment) => {
|
|
242
|
+
void this.handlePaymentAction(payment);
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* After radar / completePaymentAfterAction, handle the resumed payment.
|
|
249
|
+
*/
|
|
250
|
+
async handleResumedPayment(resumedPayment) {
|
|
251
|
+
if (resumedPayment.status === 'declined' || resumedPayment.status === 'failed') {
|
|
252
|
+
const errorMsg = resumedPayment.error?.message
|
|
253
|
+
|| resumedPayment.error?.processorMessage
|
|
254
|
+
|| 'Payment declined';
|
|
255
|
+
this.callbacks.onError?.(errorMsg);
|
|
256
|
+
this.callbacks.onProcessing?.(false);
|
|
257
|
+
this.callbacks.onFailure?.(errorMsg);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (resumedPayment.status === 'succeeded') {
|
|
261
|
+
this.callbacks.onProcessing?.(false);
|
|
262
|
+
this.callbacks.onSuccess?.(resumedPayment);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (resumedPayment.requireAction !== 'none' && resumedPayment.requireActionData) {
|
|
266
|
+
await this.handlePaymentAction(resumedPayment);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Start polling for final status
|
|
270
|
+
this.startPolling(resumedPayment.id, {
|
|
271
|
+
onSuccess: (p) => {
|
|
272
|
+
this.callbacks.onProcessing?.(false);
|
|
273
|
+
this.callbacks.onSuccess?.(p);
|
|
274
|
+
},
|
|
275
|
+
onFailure: (e) => {
|
|
276
|
+
this.callbacks.onError?.(e);
|
|
277
|
+
this.callbacks.onProcessing?.(false);
|
|
278
|
+
},
|
|
279
|
+
onRequireAction: (p) => { void this.handlePaymentAction(p); },
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// ==========================================================================
|
|
283
|
+
// PAYMENT ACTION HANDLER (mirrors usePaymentActionHandler)
|
|
284
|
+
// ==========================================================================
|
|
285
|
+
async handlePaymentAction(payment) {
|
|
286
|
+
if (payment.requireAction === 'none')
|
|
287
|
+
return;
|
|
288
|
+
if (payment.requireActionData?.processed)
|
|
289
|
+
return;
|
|
290
|
+
const actionData = payment.requireActionData;
|
|
291
|
+
if (!actionData)
|
|
292
|
+
return;
|
|
293
|
+
try {
|
|
294
|
+
await this.paymentsResource.markPaymentActionProcessed(payment.id);
|
|
295
|
+
}
|
|
296
|
+
catch { /* best-effort */ }
|
|
297
|
+
switch (actionData.type) {
|
|
298
|
+
case 'redirect':
|
|
299
|
+
case 'redirect_to_payment':
|
|
300
|
+
case 'processor_auth': {
|
|
301
|
+
const redirectUrl = actionData.metadata?.redirect?.redirectUrl || actionData.redirectUrl || actionData.url;
|
|
302
|
+
if (redirectUrl) {
|
|
303
|
+
console.log('[PaymentService] Redirecting:', redirectUrl);
|
|
304
|
+
window.location.href = redirectUrl;
|
|
305
|
+
}
|
|
306
|
+
else if (payment.status === 'succeeded') {
|
|
307
|
+
this.callbacks.onProcessing?.(false);
|
|
308
|
+
this.callbacks.onSuccess?.(payment);
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case 'threeds_auth': {
|
|
313
|
+
const session = actionData.metadata?.threedsSession;
|
|
314
|
+
if (session?.acsChallengeUrl) {
|
|
315
|
+
console.log('[PaymentService] 3DS challenge redirect:', session.acsChallengeUrl);
|
|
316
|
+
window.location.href = session.acsChallengeUrl;
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
case 'error': {
|
|
321
|
+
const msg = actionData.message || 'Payment action failed';
|
|
322
|
+
this.callbacks.onError?.(msg);
|
|
323
|
+
this.callbacks.onProcessing?.(false);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case 'kesspay_auth':
|
|
327
|
+
this.handleKessPayAuth(actionData);
|
|
328
|
+
break;
|
|
329
|
+
case 'trustflow_auth':
|
|
330
|
+
this.handleTrustFlowAuth(actionData);
|
|
331
|
+
break;
|
|
332
|
+
case 'finix_radar':
|
|
333
|
+
await this.handleFinixRadar(payment, actionData);
|
|
334
|
+
break;
|
|
335
|
+
case 'stripe_radar':
|
|
336
|
+
await this.handleStripeRadar(payment, actionData);
|
|
337
|
+
break;
|
|
338
|
+
case 'radar':
|
|
339
|
+
if (actionData.metadata?.provider === 'airwallex') {
|
|
340
|
+
await this.handleAirwallexRadar(payment, actionData);
|
|
341
|
+
}
|
|
342
|
+
break;
|
|
343
|
+
case 'mastercard_auth':
|
|
344
|
+
await this.handleMasterCardAuth(payment, actionData);
|
|
345
|
+
break;
|
|
346
|
+
default: {
|
|
347
|
+
console.log('[PaymentService] Unhandled action, starting polling:', actionData.type);
|
|
348
|
+
this.startPolling(payment.id, {
|
|
349
|
+
onSuccess: (p) => { this.callbacks.onProcessing?.(false); this.callbacks.onSuccess?.(p); },
|
|
350
|
+
onFailure: (e) => { this.callbacks.onError?.(e); this.callbacks.onProcessing?.(false); },
|
|
351
|
+
onRequireAction: (p) => { void this.handlePaymentAction(p); },
|
|
352
|
+
});
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// --------------------------------------------------------------------------
|
|
358
|
+
// KessPay Auth
|
|
359
|
+
// --------------------------------------------------------------------------
|
|
360
|
+
handleKessPayAuth(actionData) {
|
|
361
|
+
const threeDSData = actionData?.metadata?.threeds;
|
|
362
|
+
if (!threeDSData?.challengeHtml) {
|
|
363
|
+
this.callbacks.onError?.('Missing KessPay 3DS challenge HTML');
|
|
364
|
+
this.callbacks.onProcessing?.(false);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
this.callbacks.onProcessing?.(false);
|
|
369
|
+
const tempDoc = document.implementation.createHTMLDocument('KessPay 3DS');
|
|
370
|
+
tempDoc.body.innerHTML = threeDSData.challengeHtml;
|
|
371
|
+
const form = tempDoc.querySelector('form');
|
|
372
|
+
if (form) {
|
|
373
|
+
const redirectForm = document.createElement('form');
|
|
374
|
+
redirectForm.method = form.method || 'POST';
|
|
375
|
+
redirectForm.action = form.action || '';
|
|
376
|
+
redirectForm.style.display = 'none';
|
|
377
|
+
form.querySelectorAll('input').forEach(input => {
|
|
378
|
+
const newInput = document.createElement('input');
|
|
379
|
+
newInput.type = input.type;
|
|
380
|
+
newInput.name = input.name;
|
|
381
|
+
newInput.value = input.value;
|
|
382
|
+
redirectForm.appendChild(newInput);
|
|
383
|
+
});
|
|
384
|
+
document.body.appendChild(redirectForm);
|
|
385
|
+
redirectForm.submit();
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
document.open();
|
|
389
|
+
document.write(threeDSData.challengeHtml);
|
|
390
|
+
document.close();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
this.callbacks.onError?.(error instanceof Error ? error.message : 'KessPay 3DS failed');
|
|
395
|
+
this.callbacks.onProcessing?.(false);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// --------------------------------------------------------------------------
|
|
399
|
+
// TrustFlow Auth
|
|
400
|
+
// --------------------------------------------------------------------------
|
|
401
|
+
handleTrustFlowAuth(actionData) {
|
|
402
|
+
const authData = actionData?.metadata?.trustflow;
|
|
403
|
+
if (!authData?.appId || !authData?.txnId || !authData?.hash) {
|
|
404
|
+
this.callbacks.onError?.('Missing Trust Flow 3DS data');
|
|
405
|
+
this.callbacks.onProcessing?.(false);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
this.callbacks.onProcessing?.(false);
|
|
410
|
+
const form = document.createElement('form');
|
|
411
|
+
form.method = 'POST';
|
|
412
|
+
form.action = authData.captureUrl;
|
|
413
|
+
form.style.display = 'none';
|
|
414
|
+
for (const [name, value] of [['APP_ID', authData.appId], ['TXN_ID', authData.txnId], ['HASH', authData.hash]]) {
|
|
415
|
+
const input = document.createElement('input');
|
|
416
|
+
input.type = 'hidden';
|
|
417
|
+
input.name = name;
|
|
418
|
+
input.value = value;
|
|
419
|
+
form.appendChild(input);
|
|
420
|
+
}
|
|
421
|
+
document.body.appendChild(form);
|
|
422
|
+
form.submit();
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
this.callbacks.onError?.(error instanceof Error ? error.message : 'Trust Flow 3DS failed');
|
|
426
|
+
this.callbacks.onProcessing?.(false);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// --------------------------------------------------------------------------
|
|
430
|
+
// Finix Radar
|
|
431
|
+
// --------------------------------------------------------------------------
|
|
432
|
+
async handleFinixRadar(payment, actionData) {
|
|
433
|
+
const radarConfig = actionData.metadata?.radar;
|
|
434
|
+
if (!radarConfig) {
|
|
435
|
+
this.callbacks.onError?.('Finix radar config missing');
|
|
436
|
+
this.callbacks.onProcessing?.(false);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
await this.loadScript('https://js.finix.com/v/1/finix.js', () => typeof window.Finix?.Auth === 'function');
|
|
441
|
+
const sessionKey = await new Promise((resolve, reject) => {
|
|
442
|
+
const timeout = setTimeout(() => reject(new Error('Timeout waiting for Finix Auth')), 10000);
|
|
443
|
+
const FinixAuth = window.Finix.Auth(radarConfig.environment, radarConfig.merchantId, () => {
|
|
444
|
+
clearTimeout(timeout);
|
|
445
|
+
const key = FinixAuth.getSessionKey();
|
|
446
|
+
key ? resolve(key) : reject(new Error('No Finix session key'));
|
|
447
|
+
});
|
|
448
|
+
const immediateKey = FinixAuth.getSessionKey();
|
|
449
|
+
if (immediateKey) {
|
|
450
|
+
clearTimeout(timeout);
|
|
451
|
+
resolve(immediateKey);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
await this.paymentsResource.saveRadarSession({
|
|
455
|
+
orderId: radarConfig.orderId,
|
|
456
|
+
finixRadarSessionId: sessionKey,
|
|
457
|
+
finixRadarSessionData: {
|
|
458
|
+
sessionKey,
|
|
459
|
+
merchantId: radarConfig.merchantId,
|
|
460
|
+
environment: radarConfig.environment,
|
|
461
|
+
createdAt: new Date().toISOString(),
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
const resumed = await this.paymentsResource.completePaymentAfterAction(payment.id);
|
|
465
|
+
await this.handleResumedPayment(resumed);
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
this.callbacks.onError?.(error instanceof Error ? error.message : 'Finix radar failed');
|
|
469
|
+
this.callbacks.onProcessing?.(false);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// --------------------------------------------------------------------------
|
|
473
|
+
// Stripe Radar
|
|
474
|
+
// --------------------------------------------------------------------------
|
|
475
|
+
async handleStripeRadar(payment, actionData) {
|
|
476
|
+
const radarConfig = actionData.metadata?.radar;
|
|
477
|
+
if (!radarConfig?.publishableKey) {
|
|
478
|
+
this.callbacks.onError?.('Stripe radar config missing');
|
|
479
|
+
this.callbacks.onProcessing?.(false);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
await this.loadScript('https://js.stripe.com/v3/', () => typeof window.Stripe === 'function');
|
|
484
|
+
const stripe = window.Stripe(radarConfig.publishableKey);
|
|
485
|
+
const result = await stripe.createRadarSession();
|
|
486
|
+
if (result.error)
|
|
487
|
+
throw new Error(result.error.message || 'Failed to create Radar session');
|
|
488
|
+
if (!result.radarSession)
|
|
489
|
+
throw new Error('No radar session returned from Stripe');
|
|
490
|
+
await this.paymentsResource.saveRadarSession({
|
|
491
|
+
orderId: radarConfig.orderId,
|
|
492
|
+
stripeRadarSessionId: result.radarSession.id,
|
|
493
|
+
stripeRadarSessionData: result.radarSession,
|
|
494
|
+
});
|
|
495
|
+
const resumed = await this.paymentsResource.completePaymentAfterAction(payment.id);
|
|
496
|
+
await this.handleResumedPayment(resumed);
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
this.callbacks.onError?.(error instanceof Error ? error.message : 'Stripe radar failed');
|
|
500
|
+
this.callbacks.onProcessing?.(false);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// --------------------------------------------------------------------------
|
|
504
|
+
// Airwallex Radar
|
|
505
|
+
// --------------------------------------------------------------------------
|
|
506
|
+
async handleAirwallexRadar(payment, actionData) {
|
|
507
|
+
const isTest = actionData.metadata?.isTest || false;
|
|
508
|
+
const orderId = payment.order?.id;
|
|
509
|
+
const checkoutSessionId = payment.order?.checkoutSessionId;
|
|
510
|
+
if (!orderId || !checkoutSessionId) {
|
|
511
|
+
this.callbacks.onError?.('Missing order info for Airwallex radar');
|
|
512
|
+
this.callbacks.onProcessing?.(false);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const sessionId = crypto.randomUUID();
|
|
517
|
+
const existingScript = document.getElementById('airwallex-fraud-api');
|
|
518
|
+
if (!existingScript) {
|
|
519
|
+
const baseUrl = isTest ? 'https://static-demo.airwallex.com' : 'https://static.airwallex.com';
|
|
520
|
+
const script = document.createElement('script');
|
|
521
|
+
script.type = 'text/javascript';
|
|
522
|
+
script.async = true;
|
|
523
|
+
script.id = 'airwallex-fraud-api';
|
|
524
|
+
script.setAttribute('data-order-session-id', sessionId);
|
|
525
|
+
script.src = `${baseUrl}/webapp/fraud/device-fingerprint/index.js`;
|
|
526
|
+
await new Promise((resolve, reject) => {
|
|
527
|
+
script.onload = () => resolve();
|
|
528
|
+
script.onerror = () => reject(new Error('Failed to load Airwallex fraud script'));
|
|
529
|
+
document.body.appendChild(script);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
existingScript.setAttribute('data-order-session-id', sessionId);
|
|
534
|
+
}
|
|
535
|
+
await this.paymentsResource.saveRadarSession({
|
|
536
|
+
paymentId: payment.id,
|
|
537
|
+
checkoutSessionId,
|
|
538
|
+
orderId,
|
|
539
|
+
airwallexRadarSessionId: sessionId,
|
|
540
|
+
});
|
|
541
|
+
const resumed = await this.paymentsResource.completePaymentAfterAction(payment.id);
|
|
542
|
+
await this.handleResumedPayment(resumed);
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
this.callbacks.onError?.(error instanceof Error ? error.message : 'Airwallex radar failed');
|
|
546
|
+
this.callbacks.onProcessing?.(false);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// --------------------------------------------------------------------------
|
|
550
|
+
// MasterCard Auth (3DS inline challenge)
|
|
551
|
+
// --------------------------------------------------------------------------
|
|
552
|
+
async handleMasterCardAuth(payment, actionData) {
|
|
553
|
+
const threeDSData = actionData?.metadata?.threeds;
|
|
554
|
+
if (!threeDSData?.sessionId || !threeDSData?.merchantId) {
|
|
555
|
+
this.callbacks.onError?.('Missing MasterCard 3DS data');
|
|
556
|
+
this.callbacks.onProcessing?.(false);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
this.callbacks.onProcessing?.(false);
|
|
561
|
+
await this.loadScript(threeDSData.scriptUrl, () => !!window.ThreeDS);
|
|
562
|
+
const containerId = 'mastercard-3ds-container';
|
|
563
|
+
let container = document.getElementById(containerId);
|
|
564
|
+
if (!container) {
|
|
565
|
+
container = document.createElement('div');
|
|
566
|
+
container.id = containerId;
|
|
567
|
+
container.style.cssText = 'position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;overflow:hidden';
|
|
568
|
+
document.body.appendChild(container);
|
|
569
|
+
}
|
|
570
|
+
await new Promise((resolve, reject) => {
|
|
571
|
+
window.ThreeDS.configure({
|
|
572
|
+
merchantId: threeDSData.merchantId,
|
|
573
|
+
sessionId: threeDSData.sessionId,
|
|
574
|
+
containerId,
|
|
575
|
+
callback: () => {
|
|
576
|
+
window.ThreeDS.isConfigured() ? resolve() : reject(new Error('ThreeDS configuration failed'));
|
|
577
|
+
},
|
|
578
|
+
configuration: { userLanguage: 'en-US', wsVersion: threeDSData.version ? parseInt(threeDSData.version, 10) : 72 },
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
const authResult = await new Promise((resolve) => {
|
|
582
|
+
window.ThreeDS.authenticatePayer(threeDSData.orderId, threeDSData.transactionId, resolve);
|
|
583
|
+
});
|
|
584
|
+
const challengeHtml = authResult?.htmlRedirectCode || authResult?.restApiResponse?.authentication?.redirect?.html;
|
|
585
|
+
if (challengeHtml) {
|
|
586
|
+
// For standalone, redirect via form submission (no modal support without React)
|
|
587
|
+
const tempDoc = document.implementation.createHTMLDocument('MasterCard 3DS');
|
|
588
|
+
tempDoc.body.innerHTML = challengeHtml;
|
|
589
|
+
const form = tempDoc.querySelector('form');
|
|
590
|
+
if (form) {
|
|
591
|
+
const redirectForm = document.createElement('form');
|
|
592
|
+
redirectForm.method = form.method || 'POST';
|
|
593
|
+
redirectForm.action = form.action || '';
|
|
594
|
+
redirectForm.style.display = 'none';
|
|
595
|
+
form.querySelectorAll('input').forEach(input => {
|
|
596
|
+
const newInput = document.createElement('input');
|
|
597
|
+
newInput.type = input.type;
|
|
598
|
+
newInput.name = input.name;
|
|
599
|
+
newInput.value = input.value;
|
|
600
|
+
redirectForm.appendChild(newInput);
|
|
601
|
+
});
|
|
602
|
+
document.body.appendChild(redirectForm);
|
|
603
|
+
redirectForm.submit();
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
document.open();
|
|
607
|
+
document.write(challengeHtml);
|
|
608
|
+
document.close();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
// Frictionless — complete immediately
|
|
613
|
+
if (threeDSData.paymentId) {
|
|
614
|
+
const resumed = await this.paymentsResource.completePaymentAfterAction(threeDSData.paymentId);
|
|
615
|
+
await this.handleResumedPayment(resumed);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const cleanup = document.getElementById(containerId);
|
|
619
|
+
if (cleanup)
|
|
620
|
+
cleanup.remove();
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
this.callbacks.onError?.(error instanceof Error ? error.message : 'MasterCard 3DS failed');
|
|
624
|
+
this.callbacks.onProcessing?.(false);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// --------------------------------------------------------------------------
|
|
628
|
+
// Script loader utility
|
|
629
|
+
// --------------------------------------------------------------------------
|
|
630
|
+
async loadScript(src, isReady) {
|
|
631
|
+
if (typeof window === 'undefined')
|
|
632
|
+
return;
|
|
633
|
+
if (isReady())
|
|
634
|
+
return;
|
|
635
|
+
const existing = document.querySelector(`script[src="${src}"]`);
|
|
636
|
+
if (existing) {
|
|
637
|
+
await this.waitFor(isReady, 10000);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const script = document.createElement('script');
|
|
641
|
+
script.src = src;
|
|
642
|
+
script.async = true;
|
|
643
|
+
await new Promise((resolve, reject) => {
|
|
644
|
+
script.onload = () => resolve();
|
|
645
|
+
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
|
646
|
+
document.head.appendChild(script);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
waitFor(predicate, timeout) {
|
|
650
|
+
return new Promise((resolve, reject) => {
|
|
651
|
+
if (predicate()) {
|
|
652
|
+
resolve();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const t = setTimeout(() => reject(new Error('Timeout')), timeout);
|
|
656
|
+
const check = () => {
|
|
657
|
+
if (predicate()) {
|
|
658
|
+
clearTimeout(t);
|
|
659
|
+
resolve();
|
|
660
|
+
}
|
|
661
|
+
else
|
|
662
|
+
setTimeout(check, 100);
|
|
663
|
+
};
|
|
664
|
+
check();
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
// ==========================================================================
|
|
668
|
+
// REDIRECT RETURN DETECTION
|
|
669
|
+
// ==========================================================================
|
|
670
|
+
/**
|
|
671
|
+
* Detect ALL redirect return types and resume.
|
|
672
|
+
* Handles both generic returns and Airwallex-specific 3DS returns.
|
|
673
|
+
* Call once during initialization. Returns true if a return was detected.
|
|
674
|
+
*/
|
|
675
|
+
detectRedirectReturn() {
|
|
676
|
+
if (typeof window === 'undefined')
|
|
677
|
+
return false;
|
|
678
|
+
if (this.redirectReturnProcessed)
|
|
679
|
+
return false;
|
|
680
|
+
// Try Airwallex 3DS return first
|
|
681
|
+
if (this.detectAirwallex3dsReturn())
|
|
682
|
+
return true;
|
|
683
|
+
// Then generic return
|
|
684
|
+
return this.detectGenericRedirectReturn();
|
|
685
|
+
}
|
|
686
|
+
detectGenericRedirectReturn() {
|
|
687
|
+
const url = new URLSearchParams(window.location.search);
|
|
688
|
+
const paymentAction = url.get('paymentAction');
|
|
689
|
+
const paymentActionStatus = url.get('paymentActionStatus');
|
|
690
|
+
const paymentId = url.get('paymentId');
|
|
691
|
+
const mode = url.get('mode');
|
|
692
|
+
if (mode === 'retrieve')
|
|
693
|
+
return false;
|
|
694
|
+
if (paymentAction !== 'requireAction' || paymentActionStatus !== 'completed' || !paymentId)
|
|
695
|
+
return false;
|
|
696
|
+
console.log('[PaymentService] Generic redirect return detected:', paymentId);
|
|
697
|
+
this.redirectReturnProcessed = true;
|
|
698
|
+
this.callbacks.onProcessing?.(true);
|
|
699
|
+
this.callbacks.onCurrentPaymentId?.(paymentId);
|
|
700
|
+
this.callbacks.onRedirectReturn?.(paymentId);
|
|
701
|
+
this.startPolling(paymentId, {
|
|
702
|
+
onSuccess: (payment) => {
|
|
703
|
+
this.callbacks.onProcessing?.(false);
|
|
704
|
+
this.cleanPaymentUrlParams();
|
|
705
|
+
this.callbacks.onSuccess?.(payment);
|
|
706
|
+
},
|
|
707
|
+
onFailure: (error) => {
|
|
708
|
+
this.callbacks.onError?.(error);
|
|
709
|
+
this.callbacks.onProcessing?.(false);
|
|
710
|
+
this.cleanPaymentUrlParams();
|
|
711
|
+
},
|
|
712
|
+
onRequireAction: (payment) => { void this.handlePaymentAction(payment); },
|
|
713
|
+
});
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
detectAirwallex3dsReturn() {
|
|
717
|
+
const url = new URLSearchParams(window.location.search);
|
|
718
|
+
const paymentIdFromUrl = url.get('paymentId');
|
|
719
|
+
const paymentIntentId = url.get('payment_intent_id');
|
|
720
|
+
const succeeded = url.get('succeeded');
|
|
721
|
+
const processorType = url.get('processorType');
|
|
722
|
+
const hasAirwallexParams = paymentIntentId || succeeded !== null;
|
|
723
|
+
if (!hasAirwallexParams || processorType !== 'airwallex' || !paymentIdFromUrl)
|
|
724
|
+
return false;
|
|
725
|
+
console.log('[PaymentService] Airwallex 3DS return detected:', paymentIdFromUrl);
|
|
726
|
+
this.redirectReturnProcessed = true;
|
|
727
|
+
this.callbacks.onProcessing?.(true);
|
|
728
|
+
this.callbacks.onCurrentPaymentId?.(paymentIdFromUrl);
|
|
729
|
+
void this.handleAirwallex3dsRedirectReturn(paymentIdFromUrl, paymentIntentId, succeeded);
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
async handleAirwallex3dsRedirectReturn(paymentId, paymentIntentId, succeeded) {
|
|
733
|
+
const cleanParams = () => {
|
|
734
|
+
const url = new URLSearchParams(window.location.search);
|
|
735
|
+
['payment_intent_id', 'succeeded', 'processorType', 'paymentId', 'paymentAction', 'paymentActionStatus', 'error_code', 'error_message', 'mode'].forEach(p => url.delete(p));
|
|
736
|
+
const newUrl = url.toString() ? `${window.location.pathname}?${url.toString()}` : window.location.pathname;
|
|
737
|
+
window.history.replaceState({}, document.title, newUrl);
|
|
738
|
+
};
|
|
739
|
+
if (succeeded === 'true') {
|
|
740
|
+
try {
|
|
741
|
+
await this.paymentsResource.updateThreedsStatus({
|
|
742
|
+
paymentId,
|
|
743
|
+
status: 'succeeded',
|
|
744
|
+
paymentIntentId: paymentIntentId || '',
|
|
745
|
+
});
|
|
746
|
+
cleanParams();
|
|
747
|
+
const retrieveResult = await this.paymentsResource.retrievePayment(paymentId);
|
|
748
|
+
const retrieveStatus = retrieveResult?.retrieveResult?.status || retrieveResult?.status;
|
|
749
|
+
if (retrieveResult?.retrieveResult?.success && retrieveStatus === 'succeeded') {
|
|
750
|
+
const payment = await this.paymentsResource.getPaymentStatus(paymentId);
|
|
751
|
+
this.callbacks.onProcessing?.(false);
|
|
752
|
+
this.callbacks.onSuccess?.(payment);
|
|
753
|
+
}
|
|
754
|
+
else if (retrieveStatus === 'declined' || retrieveStatus === 'error') {
|
|
755
|
+
const msg = retrieveResult?.retrieveResult?.message || retrieveResult?.message || 'Payment failed';
|
|
756
|
+
this.callbacks.onError?.(msg);
|
|
757
|
+
this.callbacks.onProcessing?.(false);
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
const payment = await this.paymentsResource.getPaymentStatus(paymentId);
|
|
761
|
+
if (payment.status === 'declined' || payment.status === 'failed') {
|
|
762
|
+
const msg = payment.error?.message || payment.error?.processorMessage || 'Payment declined';
|
|
763
|
+
this.callbacks.onError?.(msg);
|
|
764
|
+
this.callbacks.onProcessing?.(false);
|
|
765
|
+
}
|
|
766
|
+
else if (payment.status === 'succeeded' || (payment.status === 'pending' && payment.subStatus === 'authorized')) {
|
|
767
|
+
this.callbacks.onProcessing?.(false);
|
|
768
|
+
this.callbacks.onSuccess?.(payment);
|
|
769
|
+
}
|
|
770
|
+
else if (payment.requireAction !== 'none' && payment.requireActionData && !payment.requireActionData.processed) {
|
|
771
|
+
void this.handlePaymentAction(payment);
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
this.startPolling(paymentId, {
|
|
775
|
+
onSuccess: (p) => { this.callbacks.onProcessing?.(false); this.callbacks.onSuccess?.(p); },
|
|
776
|
+
onFailure: (e) => { this.callbacks.onError?.(e); this.callbacks.onProcessing?.(false); },
|
|
777
|
+
onRequireAction: (p) => { void this.handlePaymentAction(p); },
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
this.callbacks.onError?.(error instanceof Error ? error.message : 'Failed to process Airwallex 3DS');
|
|
784
|
+
this.callbacks.onProcessing?.(false);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
const errorMsg = new URLSearchParams(window.location.search).get('error_message') || 'Authentication failed';
|
|
789
|
+
this.callbacks.onError?.(errorMsg);
|
|
790
|
+
this.callbacks.onProcessing?.(false);
|
|
791
|
+
cleanParams();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
cleanPaymentUrlParams() {
|
|
795
|
+
if (typeof window === 'undefined')
|
|
796
|
+
return;
|
|
797
|
+
const url = new URLSearchParams(window.location.search);
|
|
798
|
+
['paymentAction', 'paymentActionStatus', 'paymentId', 'payment_intent', 'payment_intent_client_secret', 'source_type', 'redirect_status'].forEach(p => url.delete(p));
|
|
799
|
+
const newUrl = url.toString() ? `${window.location.pathname}?${url.toString()}` : window.location.pathname;
|
|
800
|
+
window.history.replaceState({}, document.title, newUrl);
|
|
801
|
+
}
|
|
802
|
+
// ==========================================================================
|
|
803
|
+
// PAYMENT INSTRUMENT MANAGEMENT
|
|
804
|
+
// ==========================================================================
|
|
805
|
+
async createCardPaymentInstrument(cardData) {
|
|
806
|
+
const bt = await this.initBasisTheory();
|
|
807
|
+
return this.paymentsResource.createCardPaymentInstrument(bt ?? undefined, cardData);
|
|
808
|
+
}
|
|
809
|
+
async createApplePayPaymentInstrument(token) {
|
|
810
|
+
const bt = await this.initBasisTheory();
|
|
811
|
+
return this.paymentsResource.createApplePayPaymentInstrument(bt ?? undefined, token);
|
|
812
|
+
}
|
|
813
|
+
async createGooglePayPaymentInstrument(token) {
|
|
814
|
+
const bt = await this.initBasisTheory();
|
|
815
|
+
return this.paymentsResource.createGooglePayPaymentInstrument(bt ?? undefined, token);
|
|
816
|
+
}
|
|
817
|
+
async getCardPaymentInstruments() {
|
|
818
|
+
return this.paymentsResource.getCardPaymentInstruments();
|
|
819
|
+
}
|
|
820
|
+
// ==========================================================================
|
|
821
|
+
// 3DS SESSION + CHALLENGE (mirrors useThreeds)
|
|
822
|
+
// ==========================================================================
|
|
823
|
+
async createThreedsSession(paymentInstrument) {
|
|
824
|
+
const BasisTheory3ds = await this.initBt3ds();
|
|
825
|
+
if (!BasisTheory3ds)
|
|
826
|
+
throw new Error('BasisTheory 3DS not loaded');
|
|
827
|
+
const bt3ds = BasisTheory3ds(this.getBtApiKey());
|
|
828
|
+
const session = await bt3ds.createSession({ tokenId: paymentInstrument.token });
|
|
829
|
+
return this.threedsResource.createSession({
|
|
830
|
+
provider: 'basis_theory',
|
|
831
|
+
sessionData: session,
|
|
832
|
+
paymentInstrumentId: paymentInstrument.id,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
// ==========================================================================
|
|
836
|
+
// PAYMENT PROCESSING (all methods)
|
|
837
|
+
// ==========================================================================
|
|
838
|
+
async processCardPayment(checkoutSessionId, cardData) {
|
|
839
|
+
this.callbacks.onProcessing?.(true);
|
|
840
|
+
this.callbacks.onError?.(null);
|
|
841
|
+
try {
|
|
842
|
+
const instrument = await this.createCardPaymentInstrument({
|
|
843
|
+
cardNumber: cardData.cardNumber,
|
|
844
|
+
expiryDate: cardData.expiryDate,
|
|
845
|
+
cvc: cardData.cvc,
|
|
846
|
+
});
|
|
847
|
+
if (!instrument?.id)
|
|
848
|
+
throw new Error('Failed to create payment instrument');
|
|
849
|
+
// Auto-detect 3DS from store config
|
|
850
|
+
let threedsSessionId;
|
|
851
|
+
const config = await this.fetchStoreConfig();
|
|
852
|
+
if (config?.computed?.threedsEnabled) {
|
|
853
|
+
try {
|
|
854
|
+
const session = await this.createThreedsSession(instrument);
|
|
855
|
+
threedsSessionId = session.id;
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
// Continue without 3DS
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return await this.processAndHandle(checkoutSessionId, instrument.id, threedsSessionId);
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
865
|
+
this.callbacks.onError?.(msg);
|
|
866
|
+
this.callbacks.onProcessing?.(false);
|
|
867
|
+
return { success: false, error: msg };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async processApplePayPayment(checkoutSessionId, applePayToken) {
|
|
871
|
+
this.callbacks.onProcessing?.(true);
|
|
872
|
+
this.callbacks.onError?.(null);
|
|
873
|
+
try {
|
|
874
|
+
const instrument = await this.createApplePayPaymentInstrument(applePayToken);
|
|
875
|
+
return await this.processAndHandle(checkoutSessionId, instrument.id);
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
879
|
+
this.callbacks.onError?.(msg);
|
|
880
|
+
this.callbacks.onProcessing?.(false);
|
|
881
|
+
return { success: false, error: msg };
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async processGooglePayPayment(checkoutSessionId, googlePayToken) {
|
|
885
|
+
this.callbacks.onProcessing?.(true);
|
|
886
|
+
this.callbacks.onError?.(null);
|
|
887
|
+
try {
|
|
888
|
+
const instrument = await this.createGooglePayPaymentInstrument(googlePayToken);
|
|
889
|
+
return await this.processAndHandle(checkoutSessionId, instrument.id);
|
|
890
|
+
}
|
|
891
|
+
catch (error) {
|
|
892
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
893
|
+
this.callbacks.onError?.(msg);
|
|
894
|
+
this.callbacks.onProcessing?.(false);
|
|
895
|
+
return { success: false, error: msg };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async processPaymentWithInstrument(checkoutSessionId, paymentInstrumentId) {
|
|
899
|
+
this.callbacks.onProcessing?.(true);
|
|
900
|
+
this.callbacks.onError?.(null);
|
|
901
|
+
try {
|
|
902
|
+
return await this.processAndHandle(checkoutSessionId, paymentInstrumentId);
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
906
|
+
this.callbacks.onError?.(msg);
|
|
907
|
+
this.callbacks.onProcessing?.(false);
|
|
908
|
+
return { success: false, error: msg };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async processApmPayment(checkoutSessionId, apmData) {
|
|
912
|
+
this.callbacks.onProcessing?.(true);
|
|
913
|
+
this.callbacks.onError?.(null);
|
|
914
|
+
try {
|
|
915
|
+
return await this.processAndHandle(checkoutSessionId, '', undefined, {
|
|
916
|
+
processorId: apmData.processorId,
|
|
917
|
+
paymentMethod: apmData.paymentMethod,
|
|
918
|
+
initiatedBy: apmData.initiatedBy,
|
|
919
|
+
source: apmData.source,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
924
|
+
this.callbacks.onError?.(msg);
|
|
925
|
+
this.callbacks.onProcessing?.(false);
|
|
926
|
+
return { success: false, error: msg };
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|