@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.
- package/README.md +1129 -1129
- package/build-cdn.js +220 -113
- package/dist/external-tracker.js +1225 -558
- 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/tagada-sdk.js +10142 -0
- package/dist/tagada-sdk.min.js +43 -0
- package/dist/tagada-sdk.min.js.map +7 -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 +180 -2
- package/dist/v2/core/funnelClient.js +289 -6
- 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 +25 -0
- package/dist/v2/core/resources/payments.d.ts +70 -3
- package/dist/v2/core/resources/payments.js +72 -7
- package/dist/v2/core/utils/index.d.ts +1 -0
- package/dist/v2/core/utils/index.js +2 -0
- package/dist/v2/core/utils/pluginConfig.d.ts +8 -0
- package/dist/v2/core/utils/pluginConfig.js +68 -5
- package/dist/v2/core/utils/previewMode.d.ts +7 -0
- package/dist/v2/core/utils/previewMode.js +72 -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 +9 -3
- package/dist/v2/index.js +8 -3
- package/dist/v2/react/components/ApplePayButton.d.ts +22 -123
- package/dist/v2/react/components/ApplePayButton.js +247 -317
- package/dist/v2/react/components/FunnelScriptInjector.d.ts +3 -1
- package/dist/v2/react/components/FunnelScriptInjector.js +255 -162
- package/dist/v2/react/components/GooglePayButton.d.ts +2 -0
- package/dist/v2/react/components/GooglePayButton.js +80 -64
- 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 +48 -6
- package/dist/v2/react/hooks/useFunnel.js +25 -5
- package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +10 -0
- package/dist/v2/react/hooks/useGoogleAutocomplete.js +48 -0
- package/dist/v2/react/hooks/useGooglePayCheckout.d.ts +21 -0
- package/dist/v2/react/hooks/useGooglePayCheckout.js +198 -0
- package/dist/v2/react/hooks/usePaymentPolling.d.ts +15 -3
- package/dist/v2/react/hooks/usePaymentPolling.js +31 -9
- package/dist/v2/react/hooks/usePaymentQuery.d.ts +34 -2
- package/dist/v2/react/hooks/usePaymentQuery.js +731 -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/usePixelTracking.d.ts +56 -0
- package/dist/v2/react/hooks/usePixelTracking.js +508 -0
- package/dist/v2/react/hooks/useStepConfig.d.ts +64 -0
- package/dist/v2/react/hooks/useStepConfig.js +53 -0
- package/dist/v2/react/index.d.ts +15 -5
- package/dist/v2/react/index.js +8 -2
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +1 -0
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +41 -13
- package/dist/v2/react/providers/TagadaProvider.js +24 -23
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
31
|
+
* @returns FunnelContextValue with state, methods, and step config
|
|
26
32
|
*/
|
|
27
33
|
export function useFunnel() {
|
|
28
34
|
const { funnel } = useTagadaContext();
|
|
29
|
-
|
|
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
|
|
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,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;
|