@tagadapay/plugin-sdk 3.1.5 → 3.1.9

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 (71) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +220 -113
  3. package/dist/external-tracker.js +1225 -558
  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/tagada-sdk.js +10142 -0
  13. package/dist/tagada-sdk.min.js +43 -0
  14. package/dist/tagada-sdk.min.js.map +7 -0
  15. package/dist/v2/core/client.js +34 -2
  16. package/dist/v2/core/config/environment.js +9 -2
  17. package/dist/v2/core/funnelClient.d.ts +180 -2
  18. package/dist/v2/core/funnelClient.js +289 -6
  19. package/dist/v2/core/resources/apiClient.js +1 -1
  20. package/dist/v2/core/resources/checkout.d.ts +68 -0
  21. package/dist/v2/core/resources/funnel.d.ts +25 -0
  22. package/dist/v2/core/resources/payments.d.ts +70 -3
  23. package/dist/v2/core/resources/payments.js +72 -7
  24. package/dist/v2/core/utils/index.d.ts +1 -0
  25. package/dist/v2/core/utils/index.js +2 -0
  26. package/dist/v2/core/utils/pluginConfig.d.ts +8 -0
  27. package/dist/v2/core/utils/pluginConfig.js +68 -5
  28. package/dist/v2/core/utils/previewMode.d.ts +7 -0
  29. package/dist/v2/core/utils/previewMode.js +72 -14
  30. package/dist/v2/core/utils/previewModeIndicator.d.ts +19 -0
  31. package/dist/v2/core/utils/previewModeIndicator.js +414 -0
  32. package/dist/v2/core/utils/tokenStorage.d.ts +4 -0
  33. package/dist/v2/core/utils/tokenStorage.js +15 -1
  34. package/dist/v2/index.d.ts +9 -3
  35. package/dist/v2/index.js +8 -3
  36. package/dist/v2/react/components/ApplePayButton.d.ts +22 -123
  37. package/dist/v2/react/components/ApplePayButton.js +247 -317
  38. package/dist/v2/react/components/FunnelScriptInjector.d.ts +3 -1
  39. package/dist/v2/react/components/FunnelScriptInjector.js +255 -162
  40. package/dist/v2/react/components/GooglePayButton.d.ts +2 -0
  41. package/dist/v2/react/components/GooglePayButton.js +80 -64
  42. package/dist/v2/react/components/PreviewModeIndicator.d.ts +46 -0
  43. package/dist/v2/react/components/PreviewModeIndicator.js +113 -0
  44. package/dist/v2/react/hooks/useApplePayCheckout.d.ts +16 -0
  45. package/dist/v2/react/hooks/useApplePayCheckout.js +193 -0
  46. package/dist/v2/react/hooks/useFunnel.d.ts +48 -6
  47. package/dist/v2/react/hooks/useFunnel.js +25 -5
  48. package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +10 -0
  49. package/dist/v2/react/hooks/useGoogleAutocomplete.js +48 -0
  50. package/dist/v2/react/hooks/useGooglePayCheckout.d.ts +21 -0
  51. package/dist/v2/react/hooks/useGooglePayCheckout.js +198 -0
  52. package/dist/v2/react/hooks/usePaymentPolling.d.ts +15 -3
  53. package/dist/v2/react/hooks/usePaymentPolling.js +31 -9
  54. package/dist/v2/react/hooks/usePaymentQuery.d.ts +34 -2
  55. package/dist/v2/react/hooks/usePaymentQuery.js +731 -7
  56. package/dist/v2/react/hooks/usePaymentRetrieve.d.ts +26 -0
  57. package/dist/v2/react/hooks/usePaymentRetrieve.js +175 -0
  58. package/dist/v2/react/hooks/usePixelTracking.d.ts +56 -0
  59. package/dist/v2/react/hooks/usePixelTracking.js +508 -0
  60. package/dist/v2/react/hooks/useStepConfig.d.ts +64 -0
  61. package/dist/v2/react/hooks/useStepConfig.js +53 -0
  62. package/dist/v2/react/index.d.ts +15 -5
  63. package/dist/v2/react/index.js +8 -2
  64. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +1 -0
  65. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +41 -13
  66. package/dist/v2/react/providers/TagadaProvider.js +24 -23
  67. package/dist/v2/standalone/external-tracker.d.ts +2 -0
  68. package/dist/v2/standalone/external-tracker.js +6 -3
  69. package/package.json +112 -112
  70. package/dist/v2/react/hooks/useApplePay.d.ts +0 -16
  71. package/dist/v2/react/hooks/useApplePay.js +0 -247
@@ -12,16 +12,58 @@
12
12
  * </TagadaProvider>
13
13
  *
14
14
  * // In any child component:
15
- * const { context, next, isLoading } = useFunnel();
15
+ * const { context, next, isLoading, stepConfig } = useFunnel();
16
+ *
17
+ * // Access step-specific config (payment flows, static resources, etc.)
18
+ * const offerId = stepConfig.staticResources?.offer;
19
+ * const paymentFlowId = stepConfig.paymentFlowId;
16
20
  * ```
17
21
  */
22
+ import { FunnelState, GTMTrackingConfig, MetaConversionTrackingConfig, PixelTrackingConfig, RuntimeStepConfig, SnapchatTrackingConfig, TrackingProvider } from '../../core/funnelClient';
18
23
  import { FunnelAction, FunnelNavigationResult, SimpleFunnelContext } from '../../core/resources/funnel';
19
- import { FunnelState } from '../../core/funnelClient';
24
+ /**
25
+ * Step configuration from HTML injection (for current step/variant)
26
+ */
27
+ export interface StepConfigValue {
28
+ /**
29
+ * Full step configuration object
30
+ */
31
+ raw: RuntimeStepConfig | undefined;
32
+ /**
33
+ * Payment flow ID override for this step
34
+ * If set, this payment flow should be used instead of the store default
35
+ */
36
+ paymentFlowId: string | undefined;
37
+ /**
38
+ * Static resources assigned to this step/variant
39
+ * For A/B tests, this contains the resources for the specific variant
40
+ * e.g., { offer: 'offer_xxx', product: 'product_xxx' }
41
+ */
42
+ staticResources: Record<string, string> | undefined;
43
+ /**
44
+ * Get scripts for a specific injection position
45
+ * Only returns enabled scripts
46
+ */
47
+ getScripts: (position?: 'head-start' | 'head-end' | 'body-start' | 'body-end') => RuntimeStepConfig['scripts'];
48
+ /**
49
+ * Pixel tracking configuration
50
+ */
51
+ pixels?: {
52
+ [TrackingProvider.FACEBOOK]?: PixelTrackingConfig[];
53
+ [TrackingProvider.TIKTOK]?: PixelTrackingConfig[];
54
+ [TrackingProvider.SNAPCHAT]?: SnapchatTrackingConfig[];
55
+ [TrackingProvider.META_CONVERSION]?: MetaConversionTrackingConfig[];
56
+ [TrackingProvider.GTM]?: GTMTrackingConfig[];
57
+ };
58
+ }
20
59
  export interface FunnelContextValue extends FunnelState {
21
60
  currentStep: {
22
61
  id: string;
23
62
  } | null;
24
- next: (event: FunnelAction) => Promise<FunnelNavigationResult>;
63
+ stepConfig: StepConfigValue;
64
+ next: (event: FunnelAction, options?: {
65
+ waitForSession?: boolean;
66
+ }) => Promise<FunnelNavigationResult>;
25
67
  goToStep: (stepId: string) => Promise<FunnelNavigationResult>;
26
68
  updateContext: (updates: Partial<SimpleFunnelContext>) => Promise<void>;
27
69
  initializeSession: (entryStepId?: string) => Promise<void>;
@@ -32,9 +74,9 @@ export interface FunnelContextValue extends FunnelState {
32
74
  /**
33
75
  * Hook to access funnel state and methods
34
76
  *
35
- * This hook simply returns the funnel state from TagadaProvider.
36
- * All complex logic is handled at the provider level.
77
+ * This hook returns the funnel state from TagadaProvider plus step configuration
78
+ * from HTML injection. All complex logic is handled at the provider level.
37
79
  *
38
- * @returns FunnelContextValue with state and methods
80
+ * @returns FunnelContextValue with state, methods, and step config
39
81
  */
40
82
  export declare function useFunnel(): FunnelContextValue;
@@ -12,19 +12,39 @@
12
12
  * </TagadaProvider>
13
13
  *
14
14
  * // In any child component:
15
- * const { context, next, isLoading } = useFunnel();
15
+ * const { context, next, isLoading, stepConfig } = useFunnel();
16
+ *
17
+ * // Access step-specific config (payment flows, static resources, etc.)
18
+ * const offerId = stepConfig.staticResources?.offer;
19
+ * const paymentFlowId = stepConfig.paymentFlowId;
16
20
  * ```
17
21
  */
22
+ import { useMemo } from 'react';
23
+ import { TrackingProvider, getAssignedPaymentFlowId, getAssignedPixels, getAssignedScripts, getAssignedStaticResources, getAssignedStepConfig } from '../../core/funnelClient';
18
24
  import { useTagadaContext } from '../providers/TagadaProvider';
19
25
  /**
20
26
  * Hook to access funnel state and methods
21
27
  *
22
- * This hook simply returns the funnel state from TagadaProvider.
23
- * All complex logic is handled at the provider level.
28
+ * This hook returns the funnel state from TagadaProvider plus step configuration
29
+ * from HTML injection. All complex logic is handled at the provider level.
24
30
  *
25
- * @returns FunnelContextValue with state and methods
31
+ * @returns FunnelContextValue with state, methods, and step config
26
32
  */
27
33
  export function useFunnel() {
28
34
  const { funnel } = useTagadaContext();
29
- return funnel;
35
+ // Compute step config from HTML injection (memoized, computed once on mount)
36
+ const stepConfig = useMemo(() => {
37
+ const raw = getAssignedStepConfig();
38
+ return {
39
+ raw,
40
+ paymentFlowId: getAssignedPaymentFlowId(),
41
+ staticResources: getAssignedStaticResources(),
42
+ pixels: getAssignedPixels(),
43
+ getScripts: (position) => getAssignedScripts(position),
44
+ };
45
+ }, []);
46
+ return {
47
+ ...funnel,
48
+ stepConfig,
49
+ };
30
50
  }
@@ -43,6 +43,7 @@ export interface GooglePlaceDetails {
43
43
  };
44
44
  }
45
45
  export interface ExtractedAddress {
46
+ subpremise: string;
46
47
  streetNumber: string;
47
48
  route: string;
48
49
  locality: string;
@@ -52,6 +53,14 @@ export interface ExtractedAddress {
52
53
  administrativeAreaLevel2Long: string;
53
54
  country: string;
54
55
  postalCode: string;
56
+ fullStreetAddress: string;
57
+ }
58
+ export interface FormattedAddress {
59
+ address1: string;
60
+ city: string;
61
+ country: string;
62
+ state: string;
63
+ postal: string;
55
64
  }
56
65
  export interface UseGoogleAutocompleteOptions {
57
66
  apiKey: string;
@@ -67,6 +76,7 @@ export interface UseGoogleAutocompleteResult {
67
76
  searchPlaces: (input: string, countryRestriction?: string) => void;
68
77
  getPlaceDetails: (placeId: string) => Promise<GooglePlaceDetails | null>;
69
78
  extractAddressComponents: (place: GooglePlaceDetails) => ExtractedAddress;
79
+ extractFormattedAddress: (place: GooglePlaceDetails) => FormattedAddress;
70
80
  clearPredictions: () => void;
71
81
  }
72
82
  /**
@@ -153,6 +153,7 @@ export function useGoogleAutocomplete(options) {
153
153
  // Extract structured address components from Google place
154
154
  const extractAddressComponents = useCallback((place) => {
155
155
  const extracted = {
156
+ subpremise: '',
156
157
  streetNumber: '',
157
158
  route: '',
158
159
  locality: '',
@@ -162,9 +163,14 @@ export function useGoogleAutocomplete(options) {
162
163
  administrativeAreaLevel2Long: '',
163
164
  country: '',
164
165
  postalCode: '',
166
+ fullStreetAddress: '',
165
167
  };
166
168
  place.address_components?.forEach((component) => {
167
169
  const types = component.types;
170
+ if (types.includes('subpremise')) {
171
+ // Unit/Apartment number (e.g., "Unit 711", "711", "Apt 5B")
172
+ extracted.subpremise = component.long_name;
173
+ }
168
174
  if (types.includes('street_number')) {
169
175
  extracted.streetNumber = component.long_name;
170
176
  }
@@ -202,8 +208,49 @@ export function useGoogleAutocomplete(options) {
202
208
  extracted.administrativeAreaLevel1 = extracted.administrativeAreaLevel2Long || extracted.administrativeAreaLevel2;
203
209
  extracted.administrativeAreaLevel1Long = extracted.administrativeAreaLevel2Long;
204
210
  }
211
+ // Construct full street address
212
+ // Handle different formats:
213
+ // - "711/3 Network Place" (Australian unit format)
214
+ // - "3 Network Place" (house number only)
215
+ // - "Unit 711, 3 Network Place" (alternative format)
216
+ const streetParts = [];
217
+ if (extracted.subpremise) {
218
+ // Check if subpremise already contains formatting (e.g., "Unit 711")
219
+ const normalizedSubpremise = extracted.subpremise.trim();
220
+ streetParts.push(normalizedSubpremise);
221
+ }
222
+ if (extracted.streetNumber) {
223
+ streetParts.push(extracted.streetNumber);
224
+ }
225
+ if (extracted.route) {
226
+ streetParts.push(extracted.route);
227
+ }
228
+ // For Australian format, if we have both subpremise and streetNumber,
229
+ // format as "subpremise/streetNumber route" (e.g., "711/3 Network Place")
230
+ if (extracted.subpremise && extracted.streetNumber && extracted.route) {
231
+ // Remove "Unit", "Apt", etc. prefixes for cleaner format
232
+ const cleanSubpremise = extracted.subpremise
233
+ .replace(/^(Unit|Apt|Apartment|Suite|#)\s*/i, '')
234
+ .trim();
235
+ extracted.fullStreetAddress = `${cleanSubpremise}/${extracted.streetNumber} ${extracted.route}`;
236
+ }
237
+ else {
238
+ // Standard format: join all parts with spaces
239
+ extracted.fullStreetAddress = streetParts.join(' ');
240
+ }
205
241
  return extracted;
206
242
  }, []);
243
+ // Extract address in the format expected by shipping/billing address forms
244
+ const extractFormattedAddress = useCallback((place) => {
245
+ const extracted = extractAddressComponents(place);
246
+ return {
247
+ address1: extracted.fullStreetAddress || '',
248
+ city: extracted.locality || '',
249
+ country: extracted.country || '',
250
+ state: extracted.administrativeAreaLevel1 || '',
251
+ postal: extracted.postalCode || '',
252
+ };
253
+ }, [extractAddressComponents]);
207
254
  // Clear predictions
208
255
  const clearPredictions = useCallback(() => {
209
256
  setPredictions([]);
@@ -215,6 +262,7 @@ export function useGoogleAutocomplete(options) {
215
262
  searchPlaces,
216
263
  getPlaceDetails,
217
264
  extractAddressComponents,
265
+ extractFormattedAddress,
218
266
  clearPredictions,
219
267
  };
220
268
  }
@@ -0,0 +1,21 @@
1
+ import { CheckoutData } from '../../core/resources/checkout';
2
+ export interface UseGooglePayCheckoutOptions {
3
+ checkout: CheckoutData | undefined;
4
+ onSuccess?: (result: {
5
+ payment: any;
6
+ order?: any;
7
+ }) => void;
8
+ onError?: (error: string) => void;
9
+ onCancel?: () => void;
10
+ googlePayConfig?: {
11
+ merchantId?: string;
12
+ merchantName?: string;
13
+ sandboxed?: boolean;
14
+ };
15
+ }
16
+ export declare function useGooglePayCheckout({ checkout, onSuccess, onError, onCancel, googlePayConfig, }: UseGooglePayCheckoutOptions): {
17
+ handleGooglePayClick: () => void;
18
+ processingPayment: boolean;
19
+ error: string | null;
20
+ clearError: () => void;
21
+ };
@@ -0,0 +1,198 @@
1
+ import { useState, useCallback, useMemo, useEffect } from 'react';
2
+ import { usePaymentQuery } from './usePaymentQuery';
3
+ import { getBasisTheoryKeys } from '../../../config/basisTheory';
4
+ import { useTagadaContext } from '../providers/TagadaProvider';
5
+ export function useGooglePayCheckout({ checkout, onSuccess, onError, onCancel, googlePayConfig, }) {
6
+ const [processingPayment, setProcessingPayment] = useState(false);
7
+ const [error, setError] = useState(null);
8
+ const [paymentMethodConfig, setPaymentMethodConfig] = useState(null);
9
+ const { processGooglePayPayment } = usePaymentQuery();
10
+ const { apiService } = useTagadaContext();
11
+ // Determine Basis Theory keys based on sandboxed flag from payment method config
12
+ // When sandboxed is true (even on production hostname), use test keys
13
+ const { basistheoryPublicKey, basistheoryTenantId } = useMemo(() => {
14
+ const config = googlePayConfig || paymentMethodConfig;
15
+ // Use test keys if sandboxed is true, production keys only when explicitly sandboxed: false
16
+ const useProductionKeys = config?.sandboxed === false;
17
+ const keys = getBasisTheoryKeys(useProductionKeys);
18
+ console.log('[useGooglePayCheckout] Using Basis Theory keys:', {
19
+ sandboxed: config?.sandboxed,
20
+ useProductionKeys,
21
+ tenantId: keys.tenantId,
22
+ });
23
+ return {
24
+ basistheoryPublicKey: keys.apiKey,
25
+ basistheoryTenantId: keys.tenantId,
26
+ };
27
+ }, [googlePayConfig, paymentMethodConfig]);
28
+ // Fetch Google Pay payment method configuration
29
+ useEffect(() => {
30
+ if (!checkout?.checkoutSession?.id || googlePayConfig)
31
+ return;
32
+ const fetchPaymentMethods = async () => {
33
+ try {
34
+ const data = await apiService.fetch('/api/v1/payment-methods', {
35
+ method: 'GET',
36
+ params: { checkoutSessionId: checkout.checkoutSession.id },
37
+ });
38
+ const googlePayMethod = data?.find((method) => method.type === 'google_pay');
39
+ if (googlePayMethod) {
40
+ setPaymentMethodConfig(googlePayMethod.metadata);
41
+ }
42
+ console.log('googlePayMethod', googlePayMethod);
43
+ }
44
+ catch (err) {
45
+ console.error('Failed to fetch payment methods:', err);
46
+ }
47
+ };
48
+ void fetchPaymentMethods();
49
+ }, [checkout?.checkoutSession?.id, googlePayConfig, apiService]);
50
+ // Tokenize Google Pay payment using Basis Theory
51
+ const tokenizeGooglePay = useCallback(async (paymentData) => {
52
+ try {
53
+ // Extract the Google Pay token from the payment data
54
+ const googlePayTokenString = paymentData.paymentMethodData.tokenizationData.token;
55
+ const googlePayToken = JSON.parse(googlePayTokenString);
56
+ const response = await fetch('https://api.basistheory.com/google-pay', {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ 'BT-API-KEY': basistheoryPublicKey,
61
+ },
62
+ body: JSON.stringify({
63
+ google_payment_data: googlePayToken,
64
+ }),
65
+ });
66
+ if (!response.ok) {
67
+ throw new Error(`HTTP error! Status: ${response.status}`);
68
+ }
69
+ const result = await response.json();
70
+ return result?.token_intent || result?.google_pay;
71
+ }
72
+ catch (err) {
73
+ console.error('Tokenization failed:', err);
74
+ throw err;
75
+ }
76
+ }, [basistheoryPublicKey]);
77
+ // Handle Google Pay payment click
78
+ const handleGooglePayClick = useCallback(() => {
79
+ // Don't proceed if checkout is not available
80
+ if (!checkout) {
81
+ console.error('Checkout data not available');
82
+ if (onError) {
83
+ onError('Checkout not ready');
84
+ }
85
+ return;
86
+ }
87
+ setProcessingPayment(true);
88
+ // Create Google Pay payment request (simpler than express - no shipping)
89
+ const paymentRequest = {
90
+ apiVersion: 2,
91
+ apiVersionMinor: 0,
92
+ allowedPaymentMethods: [
93
+ {
94
+ type: 'CARD',
95
+ parameters: {
96
+ allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
97
+ allowedCardNetworks: ['AMEX', 'DISCOVER', 'INTERAC', 'JCB', 'MASTERCARD', 'VISA'],
98
+ billingAddressRequired: false,
99
+ },
100
+ tokenizationSpecification: {
101
+ type: 'PAYMENT_GATEWAY',
102
+ parameters: {
103
+ gateway: 'basistheory',
104
+ gatewayMerchantId: basistheoryTenantId,
105
+ },
106
+ },
107
+ },
108
+ ],
109
+ transactionInfo: {
110
+ totalPriceStatus: 'FINAL',
111
+ totalPrice: (checkout.summary.totalAdjustedAmount / 100).toFixed(2),
112
+ currencyCode: checkout.summary.currency,
113
+ },
114
+ merchantInfo: {
115
+ merchantName: googlePayConfig?.merchantName || paymentMethodConfig?.merchantName || checkout.checkoutSession?.store?.name || 'Store',
116
+ merchantId: (() => {
117
+ const config = googlePayConfig || paymentMethodConfig;
118
+ if (!config)
119
+ return '12345678901234567890'; // Fallback to test
120
+ return config.sandboxed ? '12345678901234567890' : config.merchantId;
121
+ })(),
122
+ },
123
+ };
124
+ // Determine environment from configuration
125
+ const config = googlePayConfig || paymentMethodConfig;
126
+ const environment = config?.sandboxed !== false ? 'TEST' : 'PRODUCTION';
127
+ // Create Google Pay client
128
+ const paymentsClient = new google.payments.api.PaymentsClient({ environment });
129
+ // Load payment data
130
+ paymentsClient
131
+ .loadPaymentData(paymentRequest)
132
+ .then(async (paymentData) => {
133
+ try {
134
+ // Tokenize payment
135
+ const googlePayToken = await tokenizeGooglePay(paymentData);
136
+ // Process payment via SDK hook
137
+ const result = await processGooglePayPayment(checkout.checkoutSession.id, googlePayToken, {
138
+ onPaymentSuccess: (response) => {
139
+ // Keep processing state true during navigation
140
+ },
141
+ onPaymentFailed: (err) => {
142
+ setProcessingPayment(false);
143
+ setError(err.message);
144
+ if (onError) {
145
+ onError(err.message);
146
+ }
147
+ },
148
+ });
149
+ // Call success callback
150
+ if (onSuccess) {
151
+ onSuccess(result);
152
+ }
153
+ }
154
+ catch (error) {
155
+ console.error('Payment failed:', error);
156
+ setProcessingPayment(false);
157
+ const errorMsg = error instanceof Error ? error.message : 'Payment failed';
158
+ setError(errorMsg);
159
+ if (onError) {
160
+ onError(errorMsg);
161
+ }
162
+ }
163
+ })
164
+ .catch((error) => {
165
+ console.error('Google Pay error:', error);
166
+ setProcessingPayment(false);
167
+ // Check if user canceled
168
+ if (error.statusCode === 'CANCELED') {
169
+ if (onCancel) {
170
+ onCancel();
171
+ }
172
+ return;
173
+ }
174
+ const errorMsg = error.statusMessage || 'Google Pay failed';
175
+ setError(errorMsg);
176
+ if (onError) {
177
+ onError(errorMsg);
178
+ }
179
+ });
180
+ }, [
181
+ checkout,
182
+ basistheoryTenantId,
183
+ tokenizeGooglePay,
184
+ processGooglePayPayment,
185
+ onSuccess,
186
+ onError,
187
+ onCancel,
188
+ ]);
189
+ const clearError = useCallback(() => {
190
+ setError(null);
191
+ }, []);
192
+ return {
193
+ handleGooglePayClick,
194
+ processingPayment,
195
+ error,
196
+ clearError,
197
+ };
198
+ }
@@ -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' | '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,12 +20,24 @@ 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
+ };
29
+ provider?: string;
30
+ isTest?: boolean;
23
31
  };
24
32
  redirectUrl?: string;
25
33
  resumeToken?: string;
26
34
  message?: string;
27
35
  errorCode?: string;
28
36
  };
37
+ order?: {
38
+ id: string;
39
+ checkoutSessionId: string;
40
+ };
29
41
  }
30
42
  export interface PollingOptions {
31
43
  onRequireAction?: (payment: Payment, stop: () => void) => void;
@@ -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,18 +2,50 @@
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>;
40
+ processGooglePayPayment: (checkoutSessionId: string, googlePayToken: any, options?: PaymentOptions) => Promise<PaymentResponse>;
10
41
  processPaymentWithInstrument: (checkoutSessionId: string, paymentInstrumentId: string, options?: PaymentOptions) => Promise<PaymentResponse>;
11
42
  createCardPaymentInstrument: (cardData: CardPaymentMethod) => Promise<PaymentInstrumentResponse>;
12
43
  createApplePayPaymentInstrument: (applePayToken: ApplePayToken) => Promise<PaymentInstrumentResponse>;
44
+ createGooglePayPaymentInstrument: (googlePayToken: any) => Promise<PaymentInstrumentResponse>;
13
45
  getCardPaymentInstruments: () => Promise<PaymentInstrumentCustomerResponse>;
14
46
  isLoading: boolean;
15
47
  error: string | null;
16
48
  clearError: () => void;
17
49
  currentPaymentId: string | null;
18
50
  }
19
- export declare function usePaymentQuery(): PaymentHook;
51
+ export declare function usePaymentQuery(hookOptions?: UsePaymentOptions): PaymentHook;