@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.
Files changed (56) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +113 -113
  3. package/dist/external-tracker.js +1104 -491
  4. package/dist/external-tracker.min.js +2 -2
  5. package/dist/external-tracker.min.js.map +4 -4
  6. package/dist/react/hooks/useApplePay.js +25 -36
  7. package/dist/react/hooks/usePaymentPolling.d.ts +9 -3
  8. package/dist/react/providers/TagadaProvider.js +5 -5
  9. package/dist/react/utils/money.d.ts +4 -3
  10. package/dist/react/utils/money.js +39 -6
  11. package/dist/react/utils/trackingUtils.js +1 -0
  12. package/dist/v2/core/client.js +34 -2
  13. package/dist/v2/core/config/environment.js +9 -2
  14. package/dist/v2/core/funnelClient.d.ts +92 -1
  15. package/dist/v2/core/funnelClient.js +247 -3
  16. package/dist/v2/core/resources/apiClient.js +1 -1
  17. package/dist/v2/core/resources/checkout.d.ts +68 -0
  18. package/dist/v2/core/resources/funnel.d.ts +15 -0
  19. package/dist/v2/core/resources/payments.d.ts +50 -3
  20. package/dist/v2/core/resources/payments.js +38 -7
  21. package/dist/v2/core/utils/pluginConfig.js +40 -5
  22. package/dist/v2/core/utils/previewMode.d.ts +3 -0
  23. package/dist/v2/core/utils/previewMode.js +44 -14
  24. package/dist/v2/core/utils/previewModeIndicator.d.ts +19 -0
  25. package/dist/v2/core/utils/previewModeIndicator.js +414 -0
  26. package/dist/v2/core/utils/tokenStorage.d.ts +4 -0
  27. package/dist/v2/core/utils/tokenStorage.js +15 -1
  28. package/dist/v2/index.d.ts +6 -1
  29. package/dist/v2/index.js +6 -1
  30. package/dist/v2/react/components/ApplePayButton.d.ts +21 -121
  31. package/dist/v2/react/components/ApplePayButton.js +221 -290
  32. package/dist/v2/react/components/FunnelScriptInjector.d.ts +3 -1
  33. package/dist/v2/react/components/FunnelScriptInjector.js +128 -24
  34. package/dist/v2/react/components/PreviewModeIndicator.d.ts +46 -0
  35. package/dist/v2/react/components/PreviewModeIndicator.js +113 -0
  36. package/dist/v2/react/hooks/useApplePayCheckout.d.ts +16 -0
  37. package/dist/v2/react/hooks/useApplePayCheckout.js +193 -0
  38. package/dist/v2/react/hooks/useFunnel.d.ts +42 -6
  39. package/dist/v2/react/hooks/useFunnel.js +25 -5
  40. package/dist/v2/react/hooks/usePaymentPolling.d.ts +9 -3
  41. package/dist/v2/react/hooks/usePaymentPolling.js +31 -9
  42. package/dist/v2/react/hooks/usePaymentQuery.d.ts +32 -2
  43. package/dist/v2/react/hooks/usePaymentQuery.js +304 -7
  44. package/dist/v2/react/hooks/usePaymentRetrieve.d.ts +26 -0
  45. package/dist/v2/react/hooks/usePaymentRetrieve.js +175 -0
  46. package/dist/v2/react/hooks/useStepConfig.d.ts +62 -0
  47. package/dist/v2/react/hooks/useStepConfig.js +52 -0
  48. package/dist/v2/react/index.d.ts +9 -3
  49. package/dist/v2/react/index.js +5 -1
  50. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +27 -19
  51. package/dist/v2/react/providers/TagadaProvider.js +7 -7
  52. package/dist/v2/standalone/external-tracker.d.ts +2 -0
  53. package/dist/v2/standalone/external-tracker.js +6 -3
  54. package/package.json +112 -112
  55. package/dist/v2/react/hooks/useApplePay.d.ts +0 -16
  56. 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 if payment requires action
74
- if (payment.requireAction !== 'none' && payment.requireActionData) {
75
- stopPolling();
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
- export function usePaymentQuery() {
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 (options.enableThreeds !== false) {
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;