@tagadapay/plugin-sdk 3.1.2 → 3.1.8
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/README.md +1129 -1129
- package/build-cdn.js +113 -113
- package/dist/external-tracker.js +1104 -491
- package/dist/external-tracker.min.js +2 -2
- package/dist/external-tracker.min.js.map +4 -4
- package/dist/react/hooks/useApplePay.js +25 -36
- package/dist/react/hooks/usePaymentPolling.d.ts +9 -3
- package/dist/react/providers/TagadaProvider.js +5 -5
- package/dist/react/utils/money.d.ts +4 -3
- package/dist/react/utils/money.js +39 -6
- package/dist/react/utils/trackingUtils.js +1 -0
- package/dist/v2/core/client.js +34 -2
- package/dist/v2/core/config/environment.js +9 -2
- package/dist/v2/core/funnelClient.d.ts +92 -1
- package/dist/v2/core/funnelClient.js +247 -3
- package/dist/v2/core/resources/apiClient.js +1 -1
- package/dist/v2/core/resources/checkout.d.ts +68 -0
- package/dist/v2/core/resources/funnel.d.ts +15 -0
- package/dist/v2/core/resources/payments.d.ts +50 -3
- package/dist/v2/core/resources/payments.js +38 -7
- package/dist/v2/core/utils/pluginConfig.js +40 -5
- package/dist/v2/core/utils/previewMode.d.ts +3 -0
- package/dist/v2/core/utils/previewMode.js +44 -14
- package/dist/v2/core/utils/previewModeIndicator.d.ts +19 -0
- package/dist/v2/core/utils/previewModeIndicator.js +414 -0
- package/dist/v2/core/utils/tokenStorage.d.ts +4 -0
- package/dist/v2/core/utils/tokenStorage.js +15 -1
- package/dist/v2/index.d.ts +6 -1
- package/dist/v2/index.js +6 -1
- package/dist/v2/react/components/ApplePayButton.d.ts +21 -121
- package/dist/v2/react/components/ApplePayButton.js +221 -290
- package/dist/v2/react/components/FunnelScriptInjector.d.ts +3 -1
- package/dist/v2/react/components/FunnelScriptInjector.js +128 -24
- package/dist/v2/react/components/PreviewModeIndicator.d.ts +46 -0
- package/dist/v2/react/components/PreviewModeIndicator.js +113 -0
- package/dist/v2/react/hooks/useApplePayCheckout.d.ts +16 -0
- package/dist/v2/react/hooks/useApplePayCheckout.js +193 -0
- package/dist/v2/react/hooks/useFunnel.d.ts +42 -6
- package/dist/v2/react/hooks/useFunnel.js +25 -5
- package/dist/v2/react/hooks/usePaymentPolling.d.ts +9 -3
- package/dist/v2/react/hooks/usePaymentPolling.js +31 -9
- package/dist/v2/react/hooks/usePaymentQuery.d.ts +32 -2
- package/dist/v2/react/hooks/usePaymentQuery.js +304 -7
- package/dist/v2/react/hooks/usePaymentRetrieve.d.ts +26 -0
- package/dist/v2/react/hooks/usePaymentRetrieve.js +175 -0
- package/dist/v2/react/hooks/useStepConfig.d.ts +62 -0
- package/dist/v2/react/hooks/useStepConfig.js +52 -0
- package/dist/v2/react/index.d.ts +9 -3
- package/dist/v2/react/index.js +5 -1
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +27 -19
- package/dist/v2/react/providers/TagadaProvider.js +7 -7
- package/dist/v2/standalone/external-tracker.d.ts +2 -0
- package/dist/v2/standalone/external-tracker.js +6 -3
- package/package.json +112 -112
- package/dist/v2/react/hooks/useApplePay.d.ts +0 -16
- package/dist/v2/react/hooks/useApplePay.js +0 -247
|
@@ -2,14 +2,14 @@ export interface Payment {
|
|
|
2
2
|
id: string;
|
|
3
3
|
status: string;
|
|
4
4
|
subStatus: string;
|
|
5
|
-
requireAction: 'none' | 'redirect' | 'error';
|
|
5
|
+
requireAction: 'none' | 'redirect' | 'error' | 'radar';
|
|
6
6
|
requireActionData?: {
|
|
7
|
-
type: 'redirect' | 'threeds_auth' | 'processor_auth' | 'error';
|
|
7
|
+
type: 'redirect' | 'threeds_auth' | 'processor_auth' | 'error' | 'stripe_radar' | 'finix_radar';
|
|
8
8
|
url?: string;
|
|
9
9
|
processed: boolean;
|
|
10
10
|
processorId?: string;
|
|
11
11
|
metadata?: {
|
|
12
|
-
type: 'redirect';
|
|
12
|
+
type: 'redirect' | 'stripe_radar' | 'finix_radar';
|
|
13
13
|
redirect?: {
|
|
14
14
|
redirectUrl: string;
|
|
15
15
|
returnUrl: string;
|
|
@@ -20,6 +20,12 @@ export interface Payment {
|
|
|
20
20
|
acsTransID: string;
|
|
21
21
|
messageVersion: string;
|
|
22
22
|
};
|
|
23
|
+
radar?: {
|
|
24
|
+
merchantId?: string;
|
|
25
|
+
environment?: 'sandbox' | 'live';
|
|
26
|
+
orderId?: string;
|
|
27
|
+
publishableKey?: string;
|
|
28
|
+
};
|
|
23
29
|
};
|
|
24
30
|
redirectUrl?: string;
|
|
25
31
|
resumeToken?: string;
|
|
@@ -54,6 +54,11 @@ export function usePaymentPolling() {
|
|
|
54
54
|
isPollingRef.current = true;
|
|
55
55
|
currentPaymentIdRef.current = paymentId;
|
|
56
56
|
const { onRequireAction, onSuccess, onFailure, maxAttempts = 20, pollInterval = 1500 } = options;
|
|
57
|
+
console.log('🔄 [usePaymentPolling] Starting polling...', {
|
|
58
|
+
paymentId,
|
|
59
|
+
maxAttempts,
|
|
60
|
+
pollInterval: `${pollInterval}ms`,
|
|
61
|
+
});
|
|
57
62
|
const checkPaymentStatus = async () => {
|
|
58
63
|
// Stop if component was unmounted or polling was stopped
|
|
59
64
|
if (!isMountedRef.current || !isPollingRef.current) {
|
|
@@ -61,7 +66,14 @@ export function usePaymentPolling() {
|
|
|
61
66
|
}
|
|
62
67
|
try {
|
|
63
68
|
attemptsRef.current++;
|
|
69
|
+
console.log(`🔄 [usePaymentPolling] Polling attempt ${attemptsRef.current}/${maxAttempts} for payment ${paymentId}...`);
|
|
64
70
|
const payment = await paymentsResource.getPaymentStatus(paymentId);
|
|
71
|
+
console.log(`📊 [usePaymentPolling] Payment status:`, {
|
|
72
|
+
id: payment.id,
|
|
73
|
+
status: payment.status,
|
|
74
|
+
subStatus: payment.subStatus,
|
|
75
|
+
requireAction: payment.requireAction,
|
|
76
|
+
});
|
|
65
77
|
// Check again after async operation
|
|
66
78
|
if (!isMountedRef.current || !isPollingRef.current) {
|
|
67
79
|
return;
|
|
@@ -70,25 +82,31 @@ export function usePaymentPolling() {
|
|
|
70
82
|
if (!payment?.id) {
|
|
71
83
|
return;
|
|
72
84
|
}
|
|
73
|
-
// Check
|
|
74
|
-
if
|
|
75
|
-
|
|
76
|
-
if (isMountedRef.current && onRequireAction) {
|
|
77
|
-
onRequireAction(payment, stopPolling);
|
|
78
|
-
}
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
// Check for successful payment
|
|
85
|
+
// 🔒 CRITICAL: Check for successful payment FIRST
|
|
86
|
+
// Even if requireAction is set, if payment succeeded, we should call onSuccess
|
|
87
|
+
// This handles the case where payment succeeds after redirect (3DS, etc.)
|
|
82
88
|
if (payment.status === 'succeeded' ||
|
|
83
89
|
(payment.status === 'pending' && payment.subStatus === 'authorized')) {
|
|
90
|
+
console.log('✅ [usePaymentPolling] Payment succeeded! Stopping polling.');
|
|
84
91
|
stopPolling();
|
|
85
92
|
if (isMountedRef.current && onSuccess) {
|
|
86
93
|
onSuccess(payment);
|
|
87
94
|
}
|
|
88
95
|
return;
|
|
89
96
|
}
|
|
97
|
+
// Check if payment requires action (and hasn't been processed yet)
|
|
98
|
+
// Only if payment is NOT yet succeeded
|
|
99
|
+
if (payment.requireAction !== 'none' && payment.requireActionData && !payment.requireActionData.processed) {
|
|
100
|
+
console.log('⚠️ [usePaymentPolling] Payment requires NEW action (not yet processed) - stopping polling');
|
|
101
|
+
stopPolling();
|
|
102
|
+
if (isMountedRef.current && onRequireAction) {
|
|
103
|
+
onRequireAction(payment, stopPolling);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
90
107
|
// Check for failed payment (non-succeeded and not pending)
|
|
91
108
|
if (payment.status !== 'succeeded' && payment.status !== 'pending') {
|
|
109
|
+
console.error('❌ [usePaymentPolling] Payment failed - stopping polling');
|
|
92
110
|
stopPolling();
|
|
93
111
|
if (isMountedRef.current && onFailure) {
|
|
94
112
|
onFailure(payment.status || 'Payment failed');
|
|
@@ -97,11 +115,15 @@ export function usePaymentPolling() {
|
|
|
97
115
|
}
|
|
98
116
|
// Stop after max attempts
|
|
99
117
|
if (attemptsRef.current >= maxAttempts) {
|
|
118
|
+
console.warn('⏱️ [usePaymentPolling] Max attempts reached - stopping polling');
|
|
100
119
|
stopPolling();
|
|
101
120
|
if (isMountedRef.current && onFailure) {
|
|
102
121
|
onFailure('Payment verification timeout');
|
|
103
122
|
}
|
|
104
123
|
}
|
|
124
|
+
else {
|
|
125
|
+
console.log(`⏳ [usePaymentPolling] Payment still pending - will retry in ${pollInterval}ms...`);
|
|
126
|
+
}
|
|
105
127
|
}
|
|
106
128
|
catch (_error) {
|
|
107
129
|
// Stop polling on repeated errors to prevent infinite loops
|
|
@@ -2,8 +2,38 @@
|
|
|
2
2
|
* Payment Hook using TanStack Query (V2)
|
|
3
3
|
* Matches the old usePayment.ts implementation exactly for easy migration
|
|
4
4
|
*/
|
|
5
|
-
import type { PaymentResponse, PaymentOptions, CardPaymentMethod, ApplePayToken, PaymentInstrumentResponse, PaymentInstrumentCustomerResponse } from '../../core/resources/payments';
|
|
5
|
+
import type { Payment, PaymentResponse, PaymentOptions, CardPaymentMethod, ApplePayToken, PaymentInstrumentResponse, PaymentInstrumentCustomerResponse } from '../../core/resources/payments';
|
|
6
6
|
export type { Payment as PaymentType, PaymentResponse, PaymentOptions, CardPaymentMethod, ApplePayToken, PaymentInstrumentResponse, PaymentInstrumentCustomerResponse, PaymentInstrumentCustomer } from '../../core/resources/payments';
|
|
7
|
+
/**
|
|
8
|
+
* Metadata provided with payment callbacks
|
|
9
|
+
*/
|
|
10
|
+
export interface PaymentCompletionMetadata {
|
|
11
|
+
/** True if payment completed after external redirect (3DS, PayPal, etc.) */
|
|
12
|
+
isRedirectReturn: boolean;
|
|
13
|
+
/** Order associated with the payment (if available) */
|
|
14
|
+
order?: any;
|
|
15
|
+
/** Checkout session ID (if available) */
|
|
16
|
+
checkoutSessionId?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Hook-level options for universal payment handling
|
|
20
|
+
*/
|
|
21
|
+
export interface UsePaymentOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Called when payment completes successfully
|
|
24
|
+
* Works for BOTH immediate success AND post-redirect success (3DS, PayPal, etc.)
|
|
25
|
+
* @param payment - The completed payment
|
|
26
|
+
* @param metadata - Additional context about how payment completed
|
|
27
|
+
*/
|
|
28
|
+
onPaymentCompleted?: (payment: Payment, metadata: PaymentCompletionMetadata) => void | Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Called when payment fails
|
|
31
|
+
* Works for BOTH immediate failure AND post-redirect failure
|
|
32
|
+
* @param error - Error message
|
|
33
|
+
* @param metadata - Additional context about the failure
|
|
34
|
+
*/
|
|
35
|
+
onPaymentFailed?: (error: string, metadata: PaymentCompletionMetadata) => void | Promise<void>;
|
|
36
|
+
}
|
|
7
37
|
export interface PaymentHook {
|
|
8
38
|
processCardPayment: (checkoutSessionId: string, cardData: CardPaymentMethod, options?: PaymentOptions) => Promise<PaymentResponse>;
|
|
9
39
|
processApplePayPayment: (checkoutSessionId: string, applePayToken: ApplePayToken, options?: PaymentOptions) => Promise<PaymentResponse>;
|
|
@@ -16,4 +46,4 @@ export interface PaymentHook {
|
|
|
16
46
|
clearError: () => void;
|
|
17
47
|
currentPaymentId: string | null;
|
|
18
48
|
}
|
|
19
|
-
export declare function usePaymentQuery(): PaymentHook;
|
|
49
|
+
export declare function usePaymentQuery(hookOptions?: UsePaymentOptions): PaymentHook;
|
|
@@ -10,8 +10,12 @@ import { PaymentsResource } from '../../core/resources/payments';
|
|
|
10
10
|
import { usePaymentPolling } from './usePaymentPolling';
|
|
11
11
|
import { useThreeds } from './useThreeds';
|
|
12
12
|
import { getGlobalApiClient } from './useApiQuery';
|
|
13
|
-
|
|
13
|
+
import { useStoreConfigQuery } from './useStoreConfigQuery';
|
|
14
|
+
import { getAssignedPaymentFlowId } from '../../core/funnelClient';
|
|
15
|
+
export function usePaymentQuery(hookOptions) {
|
|
14
16
|
const { environment } = useTagadaContext();
|
|
17
|
+
// Get store config to auto-detect 3DS setting from payment flow
|
|
18
|
+
const { storeConfig } = useStoreConfigQuery();
|
|
15
19
|
// Create payments resource client
|
|
16
20
|
const paymentsResource = useMemo(() => {
|
|
17
21
|
try {
|
|
@@ -24,6 +28,11 @@ export function usePaymentQuery() {
|
|
|
24
28
|
const [isLoading, setIsLoading] = useState(false);
|
|
25
29
|
const [error, setError] = useState(null);
|
|
26
30
|
const [currentPaymentId, setCurrentPaymentId] = useState(null);
|
|
31
|
+
// Store hook-level callbacks in ref to avoid re-renders
|
|
32
|
+
const hookOptionsRef = useRef(hookOptions);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
hookOptionsRef.current = hookOptions;
|
|
35
|
+
}, [hookOptions]);
|
|
27
36
|
const { startPolling, stopPolling } = usePaymentPolling();
|
|
28
37
|
const { createSession, startChallenge } = useThreeds();
|
|
29
38
|
// Track challenge in progress to prevent multiple challenges
|
|
@@ -83,7 +92,7 @@ export function usePaymentQuery() {
|
|
|
83
92
|
onRequireAction: (updatedPayment) => {
|
|
84
93
|
void handlePaymentAction(updatedPayment, options);
|
|
85
94
|
},
|
|
86
|
-
onSuccess: (successPayment) => {
|
|
95
|
+
onSuccess: async (successPayment) => {
|
|
87
96
|
setIsLoading(false);
|
|
88
97
|
const response = {
|
|
89
98
|
paymentId: successPayment.id,
|
|
@@ -91,14 +100,27 @@ export function usePaymentQuery() {
|
|
|
91
100
|
// Extract order from payment if available (for funnel path resolution)
|
|
92
101
|
order: successPayment.order,
|
|
93
102
|
};
|
|
103
|
+
// Hook-level callback (universal handler)
|
|
104
|
+
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
105
|
+
await hookOptionsRef.current.onPaymentCompleted(successPayment, {
|
|
106
|
+
isRedirectReturn: false,
|
|
107
|
+
order: response.order,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
94
110
|
// Legacy callback (backwards compatibility)
|
|
95
111
|
options.onSuccess?.(response);
|
|
96
112
|
// Funnel-aligned callback (recommended)
|
|
97
113
|
options.onPaymentSuccess?.(response);
|
|
98
114
|
},
|
|
99
|
-
onFailure: (errorMsg) => {
|
|
115
|
+
onFailure: async (errorMsg) => {
|
|
100
116
|
setError(errorMsg);
|
|
101
117
|
setIsLoading(false);
|
|
118
|
+
// Hook-level callback (universal handler)
|
|
119
|
+
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
120
|
+
await hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
121
|
+
isRedirectReturn: false,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
102
124
|
// Legacy callback (backwards compatibility)
|
|
103
125
|
options.onFailure?.(errorMsg);
|
|
104
126
|
// Funnel-aligned callback (recommended)
|
|
@@ -150,6 +172,13 @@ export function usePaymentQuery() {
|
|
|
150
172
|
// Extract order from payment if available (for funnel path resolution)
|
|
151
173
|
order: payment.order,
|
|
152
174
|
};
|
|
175
|
+
// Hook-level callback (universal handler)
|
|
176
|
+
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
177
|
+
await hookOptionsRef.current.onPaymentCompleted(payment, {
|
|
178
|
+
isRedirectReturn: false,
|
|
179
|
+
order: response.order,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
153
182
|
// Legacy callback (backwards compatibility)
|
|
154
183
|
options.onSuccess?.(response);
|
|
155
184
|
// Funnel-aligned callback (recommended)
|
|
@@ -172,9 +201,251 @@ export function usePaymentQuery() {
|
|
|
172
201
|
});
|
|
173
202
|
break;
|
|
174
203
|
}
|
|
204
|
+
case 'finix_radar': {
|
|
205
|
+
// Handle Finix fraud detection - collect device fingerprint
|
|
206
|
+
const radarConfig = actionData.metadata?.radar;
|
|
207
|
+
if (!radarConfig) {
|
|
208
|
+
console.error('Finix radar config missing from payment action');
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
// Dynamically load Finix SDK if not already loaded
|
|
213
|
+
if (typeof window !== 'undefined' && typeof window.Finix?.Auth !== 'function') {
|
|
214
|
+
const existingScript = document.querySelector('script[src="https://js.finix.com/v/1/finix.js"]');
|
|
215
|
+
if (!existingScript) {
|
|
216
|
+
const script = document.createElement('script');
|
|
217
|
+
script.src = 'https://js.finix.com/v/1/finix.js';
|
|
218
|
+
script.async = true;
|
|
219
|
+
document.head.appendChild(script);
|
|
220
|
+
await new Promise((resolve, reject) => {
|
|
221
|
+
script.onload = () => {
|
|
222
|
+
console.log('Finix SDK loaded successfully');
|
|
223
|
+
resolve();
|
|
224
|
+
};
|
|
225
|
+
script.onerror = () => reject(new Error('Failed to load Finix SDK'));
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Wait for existing script to load
|
|
230
|
+
await new Promise((resolve, reject) => {
|
|
231
|
+
const checkFinix = () => {
|
|
232
|
+
if (typeof window.Finix?.Auth === 'function') {
|
|
233
|
+
resolve();
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
setTimeout(checkFinix, 100);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
checkFinix();
|
|
240
|
+
setTimeout(() => reject(new Error('Timeout waiting for Finix SDK')), 10000);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Get session key from Finix using callback to ensure initialization is complete
|
|
245
|
+
const sessionKey = await new Promise((resolve, reject) => {
|
|
246
|
+
const timeoutId = setTimeout(() => {
|
|
247
|
+
reject(new Error('Timeout waiting for Finix Auth initialization'));
|
|
248
|
+
}, 10000);
|
|
249
|
+
// Initialize Finix Auth with callback
|
|
250
|
+
const FinixAuth = window.Finix.Auth(radarConfig.environment, radarConfig.merchantId, () => {
|
|
251
|
+
// Callback fired when Finix Auth is ready
|
|
252
|
+
clearTimeout(timeoutId);
|
|
253
|
+
const key = FinixAuth.getSessionKey();
|
|
254
|
+
console.log('Finix Auth initialized, session key:', key);
|
|
255
|
+
if (key) {
|
|
256
|
+
resolve(key);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
reject(new Error('No session key returned from Finix after initialization'));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
// Also try getting session key immediately in case it's already ready
|
|
263
|
+
const immediateKey = FinixAuth.getSessionKey();
|
|
264
|
+
if (immediateKey) {
|
|
265
|
+
clearTimeout(timeoutId);
|
|
266
|
+
console.log('Finix session key obtained immediately:', immediateKey);
|
|
267
|
+
resolve(immediateKey);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
console.log('Finix fraud session key obtained:', sessionKey);
|
|
271
|
+
// Save radar session to database
|
|
272
|
+
await paymentsResource.saveRadarSession({
|
|
273
|
+
orderId: radarConfig.orderId,
|
|
274
|
+
finixRadarSessionId: sessionKey,
|
|
275
|
+
finixRadarSessionData: {
|
|
276
|
+
sessionKey,
|
|
277
|
+
merchantId: radarConfig.merchantId,
|
|
278
|
+
environment: radarConfig.environment,
|
|
279
|
+
createdAt: new Date().toISOString(),
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
console.log('Finix radar session saved to database');
|
|
283
|
+
// Resume payment by calling completePaymentAfterAction
|
|
284
|
+
const resumedPayment = await paymentsResource.completePaymentAfterAction(payment.id);
|
|
285
|
+
console.log('Payment resumed after Finix radar:', resumedPayment);
|
|
286
|
+
// Handle the resumed payment response
|
|
287
|
+
if (resumedPayment.requireAction !== 'none' && resumedPayment.requireActionData) {
|
|
288
|
+
// Payment requires another action (e.g., 3DS)
|
|
289
|
+
await handlePaymentAction(resumedPayment, options);
|
|
290
|
+
}
|
|
291
|
+
else if (resumedPayment.status === 'succeeded') {
|
|
292
|
+
// Payment succeeded
|
|
293
|
+
setIsLoading(false);
|
|
294
|
+
const response = {
|
|
295
|
+
paymentId: resumedPayment.id,
|
|
296
|
+
payment: resumedPayment,
|
|
297
|
+
order: resumedPayment.order,
|
|
298
|
+
};
|
|
299
|
+
options.onSuccess?.(response);
|
|
300
|
+
options.onPaymentSuccess?.(response);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// Start polling for final status
|
|
304
|
+
startPolling(resumedPayment.id, {
|
|
305
|
+
onRequireAction: (updatedPayment) => {
|
|
306
|
+
void handlePaymentAction(updatedPayment, options);
|
|
307
|
+
},
|
|
308
|
+
onSuccess: (successPayment) => {
|
|
309
|
+
setIsLoading(false);
|
|
310
|
+
const response = {
|
|
311
|
+
paymentId: successPayment.id,
|
|
312
|
+
payment: successPayment,
|
|
313
|
+
order: successPayment.order,
|
|
314
|
+
};
|
|
315
|
+
options.onSuccess?.(response);
|
|
316
|
+
options.onPaymentSuccess?.(response);
|
|
317
|
+
},
|
|
318
|
+
onFailure: (errorMsg) => {
|
|
319
|
+
setError(errorMsg);
|
|
320
|
+
setIsLoading(false);
|
|
321
|
+
options.onFailure?.(errorMsg);
|
|
322
|
+
options.onPaymentFailed?.({
|
|
323
|
+
code: 'PAYMENT_FAILED',
|
|
324
|
+
message: errorMsg,
|
|
325
|
+
payment,
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (radarError) {
|
|
332
|
+
const errorMsg = radarError instanceof Error ? radarError.message : 'Finix fraud detection failed';
|
|
333
|
+
console.error('Finix radar error:', radarError);
|
|
334
|
+
setError(errorMsg);
|
|
335
|
+
setIsLoading(false);
|
|
336
|
+
options.onFailure?.(errorMsg);
|
|
337
|
+
options.onPaymentFailed?.({
|
|
338
|
+
code: 'FINIX_RADAR_FAILED',
|
|
339
|
+
message: errorMsg,
|
|
340
|
+
payment,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
175
345
|
}
|
|
176
346
|
options.onRequireAction?.(payment);
|
|
177
347
|
}, [paymentsResource, startPolling, startChallenge]);
|
|
348
|
+
// Auto-detect payment action from URL parameters (after redirect from Stripe, PayPal, etc.)
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (typeof window === 'undefined')
|
|
351
|
+
return;
|
|
352
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
353
|
+
const paymentAction = urlParams.get('paymentAction');
|
|
354
|
+
const paymentActionStatus = urlParams.get('paymentActionStatus');
|
|
355
|
+
const paymentIdFromUrl = urlParams.get('paymentId');
|
|
356
|
+
const paymentMode = urlParams.get('mode');
|
|
357
|
+
console.log('🔍 [usePayment] Checking for payment redirect return...', {
|
|
358
|
+
paymentAction,
|
|
359
|
+
paymentActionStatus,
|
|
360
|
+
paymentId: paymentIdFromUrl,
|
|
361
|
+
mode: paymentMode,
|
|
362
|
+
url: window.location.href,
|
|
363
|
+
});
|
|
364
|
+
// Skip if in retrieve mode (handled by separate hook)
|
|
365
|
+
if (paymentMode === 'retrieve') {
|
|
366
|
+
console.log('⏭️ [usePayment] Skipping - retrieve mode detected');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
// Check if returning from a payment redirect
|
|
370
|
+
if (paymentAction === 'requireAction' && paymentActionStatus === 'completed' && paymentIdFromUrl) {
|
|
371
|
+
console.log('✅ [usePayment] Payment redirect return detected! Starting auto-polling...', {
|
|
372
|
+
paymentId: paymentIdFromUrl,
|
|
373
|
+
});
|
|
374
|
+
setIsLoading(true);
|
|
375
|
+
setCurrentPaymentId(paymentIdFromUrl);
|
|
376
|
+
// Start polling for the payment status
|
|
377
|
+
startPolling(paymentIdFromUrl, {
|
|
378
|
+
onRequireAction: (payment) => {
|
|
379
|
+
console.log('⚠️ [usePayment] Payment requires new action', payment);
|
|
380
|
+
void handlePaymentAction(payment, {});
|
|
381
|
+
},
|
|
382
|
+
onSuccess: async (payment) => {
|
|
383
|
+
console.log('✅ [usePayment] Payment succeeded after redirect!', {
|
|
384
|
+
paymentId: payment.id,
|
|
385
|
+
status: payment.status,
|
|
386
|
+
hasOrder: !!payment.order,
|
|
387
|
+
});
|
|
388
|
+
setIsLoading(false);
|
|
389
|
+
// Clean up ONLY payment-related query parameters (preserve funnel/checkout params)
|
|
390
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
391
|
+
const paymentParams = ['paymentAction', 'paymentActionStatus', 'paymentId', 'payment_intent', 'payment_intent_client_secret', 'source_type', 'redirect_status'];
|
|
392
|
+
paymentParams.forEach(param => urlParams.delete(param));
|
|
393
|
+
const newUrl = urlParams.toString()
|
|
394
|
+
? `${window.location.pathname}?${urlParams.toString()}`
|
|
395
|
+
: window.location.pathname;
|
|
396
|
+
window.history.replaceState({}, document.title, newUrl);
|
|
397
|
+
console.log('🧹 [usePayment] Payment URL parameters cleaned up (preserved funnel/checkout params)');
|
|
398
|
+
// Call hook-level onPaymentCompleted callback (if provided)
|
|
399
|
+
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
400
|
+
console.log('📞 [usePayment] Calling onPaymentCompleted callback...', {
|
|
401
|
+
isRedirectReturn: true,
|
|
402
|
+
});
|
|
403
|
+
try {
|
|
404
|
+
await hookOptionsRef.current.onPaymentCompleted(payment, {
|
|
405
|
+
isRedirectReturn: true,
|
|
406
|
+
order: payment.order,
|
|
407
|
+
});
|
|
408
|
+
console.log('✅ [usePayment] onPaymentCompleted callback completed successfully');
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
console.error('❌ [usePayment] Error in onPaymentCompleted callback:', error);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
console.warn('⚠️ [usePayment] No onPaymentCompleted callback provided');
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
onFailure: async (errorMsg) => {
|
|
419
|
+
console.error('❌ [usePayment] Payment failed after redirect:', errorMsg);
|
|
420
|
+
setError(errorMsg);
|
|
421
|
+
setIsLoading(false);
|
|
422
|
+
// Clean up ONLY payment-related query parameters (preserve funnel/checkout params)
|
|
423
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
424
|
+
const paymentParams = ['paymentAction', 'paymentActionStatus', 'paymentId', 'payment_intent', 'payment_intent_client_secret', 'source_type', 'redirect_status'];
|
|
425
|
+
paymentParams.forEach(param => urlParams.delete(param));
|
|
426
|
+
const newUrl = urlParams.toString()
|
|
427
|
+
? `${window.location.pathname}?${urlParams.toString()}`
|
|
428
|
+
: window.location.pathname;
|
|
429
|
+
window.history.replaceState({}, document.title, newUrl);
|
|
430
|
+
// Call hook-level onPaymentFailed callback (if provided)
|
|
431
|
+
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
432
|
+
console.log('📞 [usePayment] Calling onPaymentFailed callback...');
|
|
433
|
+
try {
|
|
434
|
+
await hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
435
|
+
isRedirectReturn: true,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
console.error('❌ [usePayment] Error in onPaymentFailed callback:', error);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
console.log('⏭️ [usePayment] No payment redirect detected - normal page load');
|
|
447
|
+
}
|
|
448
|
+
}, [startPolling, handlePaymentAction, setIsLoading, setError, setCurrentPaymentId]);
|
|
178
449
|
// Create card payment instrument - matches old implementation
|
|
179
450
|
const createCardPaymentInstrument = useCallback((cardData) => {
|
|
180
451
|
return paymentsResource.createCardPaymentInstrument(basisTheory, cardData);
|
|
@@ -186,9 +457,12 @@ export function usePaymentQuery() {
|
|
|
186
457
|
// Process payment directly with checkout session - matches old implementation
|
|
187
458
|
const processPaymentDirect = useCallback(async (checkoutSessionId, paymentInstrumentId, threedsSessionId, options = {}) => {
|
|
188
459
|
try {
|
|
460
|
+
// Get paymentFlowId: priority is options > stepConfig > undefined (uses store default)
|
|
461
|
+
const paymentFlowId = options.paymentFlowId || getAssignedPaymentFlowId();
|
|
189
462
|
const response = await paymentsResource.processPaymentDirect(checkoutSessionId, paymentInstrumentId, threedsSessionId, {
|
|
190
463
|
initiatedBy: options.initiatedBy,
|
|
191
464
|
source: options.source,
|
|
465
|
+
paymentFlowId,
|
|
192
466
|
});
|
|
193
467
|
setCurrentPaymentId(response.payment?.id);
|
|
194
468
|
if (response.payment.requireAction !== 'none') {
|
|
@@ -201,6 +475,14 @@ export function usePaymentQuery() {
|
|
|
201
475
|
...response,
|
|
202
476
|
order: response.order || response.payment.order,
|
|
203
477
|
};
|
|
478
|
+
// Hook-level callback (universal handler)
|
|
479
|
+
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
480
|
+
await hookOptionsRef.current.onPaymentCompleted(response.payment, {
|
|
481
|
+
isRedirectReturn: false,
|
|
482
|
+
order: successResponse.order,
|
|
483
|
+
checkoutSessionId,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
204
486
|
// Legacy callback (backwards compatibility)
|
|
205
487
|
options.onSuccess?.(successResponse);
|
|
206
488
|
// Funnel-aligned callback (recommended)
|
|
@@ -212,7 +494,7 @@ export function usePaymentQuery() {
|
|
|
212
494
|
onRequireAction: (payment) => {
|
|
213
495
|
void handlePaymentAction(payment, options);
|
|
214
496
|
},
|
|
215
|
-
onSuccess: (payment) => {
|
|
497
|
+
onSuccess: async (payment) => {
|
|
216
498
|
setIsLoading(false);
|
|
217
499
|
const successResponse = {
|
|
218
500
|
paymentId: payment.id,
|
|
@@ -220,14 +502,28 @@ export function usePaymentQuery() {
|
|
|
220
502
|
// Extract order from payment if available (for funnel path resolution)
|
|
221
503
|
order: payment.order,
|
|
222
504
|
};
|
|
505
|
+
// Hook-level callback (universal handler)
|
|
506
|
+
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
507
|
+
await hookOptionsRef.current.onPaymentCompleted(payment, {
|
|
508
|
+
isRedirectReturn: false,
|
|
509
|
+
order: successResponse.order,
|
|
510
|
+
checkoutSessionId,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
223
513
|
// Legacy callback (backwards compatibility)
|
|
224
514
|
options.onSuccess?.(successResponse);
|
|
225
515
|
// Funnel-aligned callback (recommended)
|
|
226
516
|
options.onPaymentSuccess?.(successResponse);
|
|
227
517
|
},
|
|
228
|
-
onFailure: (errorMsg) => {
|
|
518
|
+
onFailure: async (errorMsg) => {
|
|
229
519
|
setError(errorMsg);
|
|
230
520
|
setIsLoading(false);
|
|
521
|
+
// Hook-level callback (universal handler)
|
|
522
|
+
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
523
|
+
await hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
524
|
+
isRedirectReturn: false,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
231
527
|
// Legacy callback (backwards compatibility)
|
|
232
528
|
options.onFailure?.(errorMsg);
|
|
233
529
|
// Funnel-aligned callback (recommended)
|
|
@@ -262,9 +558,10 @@ export function usePaymentQuery() {
|
|
|
262
558
|
try {
|
|
263
559
|
// 1. Create payment instrument
|
|
264
560
|
const paymentInstrument = await createCardPaymentInstrument(cardData);
|
|
265
|
-
// 2. Create 3DS session if enabled
|
|
561
|
+
// 2. Create 3DS session if enabled (use payment flow setting if not explicitly provided)
|
|
562
|
+
const shouldCreateThreedsSession = storeConfig?.computed?.threedsEnabled;
|
|
266
563
|
let threedsSessionId;
|
|
267
|
-
if (
|
|
564
|
+
if (shouldCreateThreedsSession) {
|
|
268
565
|
try {
|
|
269
566
|
const threedsSession = await createSession(paymentInstrument, {
|
|
270
567
|
provider: options.threedsProvider || 'basis_theory',
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Retrieve Hook using TanStack Query (V2)
|
|
3
|
+
* Handles payment status retrieval after external redirects
|
|
4
|
+
* This is useful for processors that require server-side status checks (e.g., some 3DS flows)
|
|
5
|
+
*/
|
|
6
|
+
export interface RetrieveResult {
|
|
7
|
+
retrieveResult?: {
|
|
8
|
+
status?: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
success?: boolean;
|
|
11
|
+
};
|
|
12
|
+
paymentId: string;
|
|
13
|
+
transactionCreated?: boolean;
|
|
14
|
+
status?: string;
|
|
15
|
+
transactionId?: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
success?: boolean;
|
|
18
|
+
error?: any;
|
|
19
|
+
}
|
|
20
|
+
export interface PaymentRetrieveHook {
|
|
21
|
+
startRetrievePolling: (paymentId: string) => Promise<void>;
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
error: string | null;
|
|
24
|
+
isPolling: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function usePaymentRetrieve(): PaymentRetrieveHook;
|