@tagadapay/plugin-sdk 2.3.5 → 2.3.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/dist/react/hooks/useApplePay.d.ts +2 -0
- package/dist/react/hooks/useApplePay.js +192 -0
- package/dist/react/hooks/useCheckout.js +4 -0
- package/dist/react/hooks/useOrderSummary.d.ts +72 -0
- package/dist/react/hooks/useOrderSummary.js +85 -0
- package/dist/react/hooks/useShippingRates.d.ts +48 -0
- package/dist/react/hooks/useShippingRates.js +204 -0
- package/dist/react/hooks/useTranslations.d.ts +18 -0
- package/dist/react/hooks/useTranslations.js +77 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +3 -0
- package/dist/react/types/apple-pay.d.ts +93 -0
- package/dist/react/types/apple-pay.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { getBasisTheoryApiKey } from '../config/payment';
|
|
3
|
+
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
4
|
+
import { usePayment } from './usePayment';
|
|
5
|
+
export function useApplePay(options = {}) {
|
|
6
|
+
const [processingPayment, setProcessingPayment] = useState(false);
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
const { createApplePayPaymentInstrument, processApplePayPayment } = usePayment();
|
|
9
|
+
const { environment, apiService } = useTagadaContext();
|
|
10
|
+
// Get API key from environment
|
|
11
|
+
const apiKey = getBasisTheoryApiKey(environment?.environment || 'local');
|
|
12
|
+
// Check if Apple Pay is available
|
|
13
|
+
const isApplePayAvailable = (() => {
|
|
14
|
+
if (typeof window === 'undefined')
|
|
15
|
+
return false;
|
|
16
|
+
// Check if ApplePaySession is available
|
|
17
|
+
const hasApplePaySession = !!window.ApplePaySession;
|
|
18
|
+
if (!hasApplePaySession) {
|
|
19
|
+
// In development, simulate Apple Pay availability for UI testing
|
|
20
|
+
const isDevelopment = process.env.NODE_ENV === 'development' ||
|
|
21
|
+
window.location.hostname === 'localhost' ||
|
|
22
|
+
window.location.hostname.includes('127.0.0.1');
|
|
23
|
+
if (isDevelopment) {
|
|
24
|
+
console.log('Development mode: Simulating Apple Pay availability for UI testing');
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
// Check basic Apple Pay support
|
|
31
|
+
return window.ApplePaySession.canMakePayments();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.warn('Apple Pay availability check failed:', error);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
// Debug logging
|
|
39
|
+
console.log('Apple Pay availability check:', {
|
|
40
|
+
hasWindow: typeof window !== 'undefined',
|
|
41
|
+
hasApplePaySession: typeof window !== 'undefined' && !!window.ApplePaySession,
|
|
42
|
+
canMakePayments: typeof window !== 'undefined' && window.ApplePaySession && window.ApplePaySession.canMakePayments(),
|
|
43
|
+
isDevelopment: process.env.NODE_ENV === 'development' ||
|
|
44
|
+
(typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname.includes('127.0.0.1'))),
|
|
45
|
+
isAvailable: isApplePayAvailable,
|
|
46
|
+
note: !window.ApplePaySession ? 'Apple Pay not available in this browser. Use Safari on iOS/macOS for real Apple Pay support.' : 'Apple Pay API detected'
|
|
47
|
+
});
|
|
48
|
+
const validateMerchant = useCallback(async () => {
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch('https://api.basistheory.com/apple-pay/session', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
'BT-API-KEY': apiKey,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
display_name: 'Tagada Pay Store',
|
|
58
|
+
domain: typeof window !== 'undefined' ? window.location.host : 'localhost',
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
63
|
+
}
|
|
64
|
+
const merchantSession = await response.json();
|
|
65
|
+
return merchantSession;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error('Merchant validation error:', err);
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}, [apiKey]);
|
|
72
|
+
const tokenizeApplePay = useCallback(async (event) => {
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch('https://api.basistheory.com/apple-pay', {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
'BT-API-KEY': apiKey,
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
apple_payment_data: event.payment.token,
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
86
|
+
}
|
|
87
|
+
const result = await response.json();
|
|
88
|
+
return result.apple_pay; // Basis Theory returns the Apple Pay token in the apple_pay field
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('Apple Pay tokenization error:', error);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}, [apiKey]);
|
|
95
|
+
const handleApplePayClick = useCallback((checkoutSessionId, lineItems, total, config = {}) => {
|
|
96
|
+
if (!isApplePayAvailable) {
|
|
97
|
+
const errorMsg = 'Apple Pay is not available on this device';
|
|
98
|
+
setError(errorMsg);
|
|
99
|
+
options.onError?.(errorMsg);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const request = {
|
|
103
|
+
countryCode: config.countryCode || 'US',
|
|
104
|
+
currencyCode: 'USD', // This should be passed as a parameter
|
|
105
|
+
supportedNetworks: config.supportedNetworks || ['visa', 'masterCard', 'amex', 'discover'],
|
|
106
|
+
merchantCapabilities: config.merchantCapabilities || ['supports3DS'],
|
|
107
|
+
total,
|
|
108
|
+
lineItems,
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
const session = new window.ApplePaySession(3, request);
|
|
112
|
+
session.onvalidatemerchant = (event) => {
|
|
113
|
+
void (async () => {
|
|
114
|
+
try {
|
|
115
|
+
console.log('Merchant validation requested for:', event.validationURL);
|
|
116
|
+
const merchantSession = await validateMerchant();
|
|
117
|
+
session.completeMerchantValidation(merchantSession);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error('Merchant validation failed:', error);
|
|
121
|
+
session.abort();
|
|
122
|
+
const errorMsg = 'Merchant validation failed';
|
|
123
|
+
setError(errorMsg);
|
|
124
|
+
options.onError?.(errorMsg);
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
127
|
+
};
|
|
128
|
+
session.onpaymentauthorized = (event) => {
|
|
129
|
+
void (async () => {
|
|
130
|
+
try {
|
|
131
|
+
setProcessingPayment(true);
|
|
132
|
+
setError(null);
|
|
133
|
+
// Tokenize the Apple Pay payment
|
|
134
|
+
const applePayToken = await tokenizeApplePay(event);
|
|
135
|
+
// Complete the Apple Pay session
|
|
136
|
+
session.completePayment(window.ApplePaySession.STATUS_SUCCESS);
|
|
137
|
+
// Process the payment using the SDK's payment methods
|
|
138
|
+
await processApplePayPayment(checkoutSessionId, applePayToken, {
|
|
139
|
+
onSuccess: (payment) => {
|
|
140
|
+
setProcessingPayment(false);
|
|
141
|
+
options.onSuccess?.(payment);
|
|
142
|
+
},
|
|
143
|
+
onFailure: (errorMsg) => {
|
|
144
|
+
setProcessingPayment(false);
|
|
145
|
+
setError(errorMsg);
|
|
146
|
+
options.onError?.(errorMsg);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error('Payment processing failed:', error);
|
|
152
|
+
session.completePayment(window.ApplePaySession.STATUS_FAILURE);
|
|
153
|
+
setProcessingPayment(false);
|
|
154
|
+
const errorMsg = error instanceof Error ? error.message : 'Payment processing failed';
|
|
155
|
+
setError(errorMsg);
|
|
156
|
+
options.onError?.(errorMsg);
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
159
|
+
};
|
|
160
|
+
session.onerror = (event) => {
|
|
161
|
+
console.error('Apple Pay Session Error:', event);
|
|
162
|
+
const errorMsg = 'Apple Pay session error';
|
|
163
|
+
setError(errorMsg);
|
|
164
|
+
options.onError?.(errorMsg);
|
|
165
|
+
};
|
|
166
|
+
session.oncancel = () => {
|
|
167
|
+
console.log('Payment cancelled by user');
|
|
168
|
+
setProcessingPayment(false);
|
|
169
|
+
options.onCancel?.();
|
|
170
|
+
};
|
|
171
|
+
session.begin();
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
console.error('Failed to start Apple Pay session:', error);
|
|
175
|
+
const errorMsg = 'Failed to start Apple Pay session';
|
|
176
|
+
setError(errorMsg);
|
|
177
|
+
options.onError?.(errorMsg);
|
|
178
|
+
}
|
|
179
|
+
}, [
|
|
180
|
+
isApplePayAvailable,
|
|
181
|
+
validateMerchant,
|
|
182
|
+
tokenizeApplePay,
|
|
183
|
+
processApplePayPayment,
|
|
184
|
+
options,
|
|
185
|
+
]);
|
|
186
|
+
return {
|
|
187
|
+
handleApplePayClick,
|
|
188
|
+
processingPayment,
|
|
189
|
+
applePayError: error,
|
|
190
|
+
isApplePayAvailable,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -179,6 +179,10 @@ export function useCheckout(options = {}) {
|
|
|
179
179
|
}
|
|
180
180
|
}, [apiService, currentCurrency, isSessionInitialized]);
|
|
181
181
|
const refresh = useCallback(async () => {
|
|
182
|
+
console.log('🔄 [useCheckout] Refreshing checkout data...', {
|
|
183
|
+
checkoutToken: currentCheckoutTokenRef.current?.substring(0, 8) + '...',
|
|
184
|
+
timestamp: new Date().toISOString(),
|
|
185
|
+
});
|
|
182
186
|
if (!currentCheckoutTokenRef.current) {
|
|
183
187
|
throw new Error('No checkout session to refresh');
|
|
184
188
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export interface OrderSummaryItem {
|
|
2
|
+
id: string;
|
|
3
|
+
productId: string;
|
|
4
|
+
variantId: string;
|
|
5
|
+
orderLineItemProduct?: {
|
|
6
|
+
name: string | null;
|
|
7
|
+
} | null;
|
|
8
|
+
orderLineItemVariant?: {
|
|
9
|
+
name: string | null;
|
|
10
|
+
imageUrl: string | null;
|
|
11
|
+
} | null;
|
|
12
|
+
product?: {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
quantity: number;
|
|
17
|
+
amount: number;
|
|
18
|
+
adjustedAmount: number;
|
|
19
|
+
currency: string;
|
|
20
|
+
recurring?: boolean | null;
|
|
21
|
+
interval?: 'day' | 'week' | 'month' | 'year' | null;
|
|
22
|
+
intervalCount?: number | null;
|
|
23
|
+
subscriptionSettings?: {
|
|
24
|
+
trial?: {
|
|
25
|
+
duration: number;
|
|
26
|
+
durationType: 'day' | 'week' | 'month' | 'year';
|
|
27
|
+
} | null;
|
|
28
|
+
} | null;
|
|
29
|
+
adjustments?: {
|
|
30
|
+
type: string;
|
|
31
|
+
amount: number;
|
|
32
|
+
description: string;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
35
|
+
export interface OrderSummaryData {
|
|
36
|
+
totalAdjustedAmount: number;
|
|
37
|
+
currency: string;
|
|
38
|
+
shippingCost: number;
|
|
39
|
+
shippingCostIsFree: boolean;
|
|
40
|
+
subtotalAdjustedAmount: number;
|
|
41
|
+
totalPromotionAmount: number;
|
|
42
|
+
totalTaxAmount: number;
|
|
43
|
+
items: OrderSummaryItem[];
|
|
44
|
+
adjustments?: {
|
|
45
|
+
type: string;
|
|
46
|
+
amount: number;
|
|
47
|
+
description: string;
|
|
48
|
+
}[];
|
|
49
|
+
}
|
|
50
|
+
export interface UseOrderSummaryOptions {
|
|
51
|
+
sessionId?: string;
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface UseOrderSummaryResult {
|
|
55
|
+
orderSummary: OrderSummaryData | undefined;
|
|
56
|
+
isLoading: boolean;
|
|
57
|
+
isRefetching: boolean;
|
|
58
|
+
error: Error | null;
|
|
59
|
+
total: number;
|
|
60
|
+
currency: string;
|
|
61
|
+
refetch: () => Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
export declare function useOrderSummary({ sessionId, disabled, }?: UseOrderSummaryOptions): UseOrderSummaryResult;
|
|
64
|
+
/**
|
|
65
|
+
* Simplified hook that just returns the total and currency
|
|
66
|
+
* Reuses the data from useOrderSummary
|
|
67
|
+
*/
|
|
68
|
+
export declare function useOrderTotal(options?: UseOrderSummaryOptions): {
|
|
69
|
+
total: number;
|
|
70
|
+
currency: string;
|
|
71
|
+
isLoading: boolean;
|
|
72
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
3
|
+
export function useOrderSummary({ sessionId, disabled = false, } = {}) {
|
|
4
|
+
const { apiService } = useTagadaContext();
|
|
5
|
+
const [orderSummary, setOrderSummary] = useState(undefined);
|
|
6
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
7
|
+
const [isRefetching, setIsRefetching] = useState(false);
|
|
8
|
+
const [error, setError] = useState(null);
|
|
9
|
+
const isMountedRef = useRef(true);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
isMountedRef.current = true;
|
|
12
|
+
return () => {
|
|
13
|
+
isMountedRef.current = false;
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
const fetchOrderSummary = useCallback(async () => {
|
|
17
|
+
if (!sessionId || disabled) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
setError(null);
|
|
22
|
+
const response = await apiService.fetch(`/api/v1/checkout-sessions/${sessionId}/order-summary`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
},
|
|
27
|
+
body: { checkoutSessionId: sessionId }
|
|
28
|
+
});
|
|
29
|
+
if (isMountedRef.current) {
|
|
30
|
+
setOrderSummary(response);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (isMountedRef.current) {
|
|
35
|
+
setError(err instanceof Error ? err : new Error('Failed to fetch order summary'));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}, [sessionId, disabled, apiService]);
|
|
39
|
+
const refetch = useCallback(async () => {
|
|
40
|
+
if (!sessionId || disabled) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setIsRefetching(true);
|
|
44
|
+
try {
|
|
45
|
+
await fetchOrderSummary();
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
if (isMountedRef.current) {
|
|
49
|
+
setIsRefetching(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}, [sessionId, disabled, fetchOrderSummary]);
|
|
53
|
+
// Initial fetch
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (sessionId && !disabled && isMountedRef.current) {
|
|
56
|
+
setIsLoading(true);
|
|
57
|
+
fetchOrderSummary().finally(() => {
|
|
58
|
+
if (isMountedRef.current) {
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}, [sessionId, disabled, fetchOrderSummary]);
|
|
64
|
+
return {
|
|
65
|
+
orderSummary,
|
|
66
|
+
isLoading,
|
|
67
|
+
isRefetching,
|
|
68
|
+
error,
|
|
69
|
+
total: orderSummary?.totalAdjustedAmount ?? 0,
|
|
70
|
+
currency: orderSummary?.currency ?? 'EUR',
|
|
71
|
+
refetch,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Simplified hook that just returns the total and currency
|
|
76
|
+
* Reuses the data from useOrderSummary
|
|
77
|
+
*/
|
|
78
|
+
export function useOrderTotal(options = {}) {
|
|
79
|
+
const { total, currency, isLoading } = useOrderSummary(options);
|
|
80
|
+
return {
|
|
81
|
+
total,
|
|
82
|
+
currency,
|
|
83
|
+
isLoading,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { PickupPoint } from '../types';
|
|
2
|
+
export interface ShippingRate {
|
|
3
|
+
id: string;
|
|
4
|
+
shippingRateName: string;
|
|
5
|
+
amount: number;
|
|
6
|
+
currency: string;
|
|
7
|
+
isFree: boolean;
|
|
8
|
+
description?: string;
|
|
9
|
+
highlighted?: boolean;
|
|
10
|
+
icon?: string;
|
|
11
|
+
isPickupPoint?: boolean;
|
|
12
|
+
pickupPointCarriers?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface ShippingRatesResponse {
|
|
15
|
+
rates: ShippingRate[];
|
|
16
|
+
pickupPoint?: PickupPoint;
|
|
17
|
+
forceCheckoutSessionRefetch?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface UseShippingRatesOptions {
|
|
20
|
+
onSuccess?: () => void;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
checkoutSessionId?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface UseShippingRatesResult {
|
|
25
|
+
shippingRates: ShippingRate[] | undefined;
|
|
26
|
+
forceCheckoutSessionRefetch: boolean | undefined;
|
|
27
|
+
handleSelectRate: (rateId: string) => void;
|
|
28
|
+
isPickupPointSelected: boolean;
|
|
29
|
+
selectedRateId: string | null;
|
|
30
|
+
setSelectedRateId: (rateId: string | null) => void;
|
|
31
|
+
setShippingRate: (data: {
|
|
32
|
+
shippingRateId: string;
|
|
33
|
+
}) => Promise<void>;
|
|
34
|
+
setShippingRateAsync: (data: {
|
|
35
|
+
shippingRateId: string;
|
|
36
|
+
}) => Promise<void>;
|
|
37
|
+
isPending: boolean;
|
|
38
|
+
isFetching: boolean;
|
|
39
|
+
isLoading: boolean;
|
|
40
|
+
error: Error | null;
|
|
41
|
+
refetch: () => Promise<void>;
|
|
42
|
+
setPickupPoint: (data: {
|
|
43
|
+
checkoutSessionId: string;
|
|
44
|
+
data: PickupPoint;
|
|
45
|
+
}) => Promise<void>;
|
|
46
|
+
selectedRate: ShippingRate | undefined;
|
|
47
|
+
}
|
|
48
|
+
export declare const useShippingRates: ({ onSuccess, disabled, checkoutSessionId }?: UseShippingRatesOptions) => UseShippingRatesResult;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
3
|
+
// Debounce utility to prevent rapid successive calls
|
|
4
|
+
function useDebounce(callback, delay) {
|
|
5
|
+
const timeoutRef = useRef(null);
|
|
6
|
+
const isMountedRef = useRef(true);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
isMountedRef.current = true;
|
|
9
|
+
return () => {
|
|
10
|
+
isMountedRef.current = false;
|
|
11
|
+
if (timeoutRef.current) {
|
|
12
|
+
clearTimeout(timeoutRef.current);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
return useCallback(((...args) => {
|
|
17
|
+
if (!isMountedRef.current)
|
|
18
|
+
return;
|
|
19
|
+
if (timeoutRef.current) {
|
|
20
|
+
clearTimeout(timeoutRef.current);
|
|
21
|
+
}
|
|
22
|
+
timeoutRef.current = setTimeout(() => {
|
|
23
|
+
if (isMountedRef.current) {
|
|
24
|
+
callback(...args);
|
|
25
|
+
}
|
|
26
|
+
}, delay);
|
|
27
|
+
}), [callback, delay]);
|
|
28
|
+
}
|
|
29
|
+
export const useShippingRates = ({ onSuccess, disabled, checkoutSessionId } = {}) => {
|
|
30
|
+
const { apiService } = useTagadaContext();
|
|
31
|
+
const [selectedRateId, setSelectedRateId] = useState(null);
|
|
32
|
+
const [shippingRates, setShippingRates] = useState();
|
|
33
|
+
const [forceCheckoutSessionRefetch, setForceCheckoutSessionRefetch] = useState();
|
|
34
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
35
|
+
const [isFetching, setIsFetching] = useState(false);
|
|
36
|
+
const [isPending, setIsPending] = useState(false);
|
|
37
|
+
const [error, setError] = useState(null);
|
|
38
|
+
const isMountedRef = useRef(true);
|
|
39
|
+
const isUpdatingRef = useRef(false);
|
|
40
|
+
// Use checkoutSessionId from props or fall back to session
|
|
41
|
+
const sessionId = checkoutSessionId;
|
|
42
|
+
// Track mounted state
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
isMountedRef.current = true;
|
|
45
|
+
return () => {
|
|
46
|
+
isMountedRef.current = false;
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
// Fetch shipping rates
|
|
50
|
+
const fetchShippingRates = useCallback(async () => {
|
|
51
|
+
if (!sessionId || !apiService)
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
setIsFetching(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
const response = await apiService.fetch(`/api/v1/checkout-sessions/${sessionId}/shipping-rates`, {
|
|
57
|
+
method: 'GET',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
if (isMountedRef.current) {
|
|
63
|
+
setShippingRates(response.rates);
|
|
64
|
+
setForceCheckoutSessionRefetch(response.forceCheckoutSessionRefetch);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
if (isMountedRef.current) {
|
|
69
|
+
setError(err instanceof Error ? err : new Error('Failed to fetch shipping rates'));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
if (isMountedRef.current) {
|
|
74
|
+
setIsFetching(false);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}, [sessionId, apiService]);
|
|
78
|
+
// Initial fetch of shipping rates when sessionId is available
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (sessionId && !disabled && isMountedRef.current) {
|
|
81
|
+
fetchShippingRates();
|
|
82
|
+
}
|
|
83
|
+
}, [sessionId, disabled, fetchShippingRates]);
|
|
84
|
+
// Debounced success handler to prevent query invalidation storms
|
|
85
|
+
const debouncedOnSuccess = useDebounce(useCallback(async () => {
|
|
86
|
+
if (!isMountedRef.current || isUpdatingRef.current)
|
|
87
|
+
return;
|
|
88
|
+
try {
|
|
89
|
+
isUpdatingRef.current = true;
|
|
90
|
+
// Refetch shipping rates after successful update
|
|
91
|
+
await fetchShippingRates();
|
|
92
|
+
if (isMountedRef.current && onSuccess) {
|
|
93
|
+
onSuccess();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.error('Error in shipping rate success handler:', error);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
if (isMountedRef.current) {
|
|
101
|
+
isUpdatingRef.current = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}, [fetchShippingRates, onSuccess]), 300);
|
|
105
|
+
// Set shipping rate mutation
|
|
106
|
+
const setShippingRate = useCallback(async (data) => {
|
|
107
|
+
if (!sessionId || !apiService)
|
|
108
|
+
return;
|
|
109
|
+
try {
|
|
110
|
+
setIsPending(true);
|
|
111
|
+
setError(null);
|
|
112
|
+
await apiService.fetch(`/api/v1/checkout-sessions/${sessionId}/shipping-rate`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: data,
|
|
115
|
+
});
|
|
116
|
+
if (isMountedRef.current) {
|
|
117
|
+
await debouncedOnSuccess();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (isMountedRef.current) {
|
|
122
|
+
setSelectedRateId(selectedRateId); // Revert to previous selection
|
|
123
|
+
setError(err instanceof Error ? err : new Error('Failed to update shipping rate'));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
if (isMountedRef.current) {
|
|
128
|
+
setIsPending(false);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, [sessionId, apiService, debouncedOnSuccess, selectedRateId]);
|
|
132
|
+
const setShippingRateAsync = setShippingRate; // Alias for consistency
|
|
133
|
+
// Debounced refetch for country/postal changes
|
|
134
|
+
const debouncedRefetchRates = useDebounce(fetchShippingRates, 500);
|
|
135
|
+
// Set pickup point mutation
|
|
136
|
+
const setPickupPoint = useCallback(async (data) => {
|
|
137
|
+
if (!apiService)
|
|
138
|
+
return;
|
|
139
|
+
try {
|
|
140
|
+
setIsPending(true);
|
|
141
|
+
setError(null);
|
|
142
|
+
await apiService.fetch(`/api/v1/checkout-sessions/${data.checkoutSessionId}/pickup-point`, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
body: { data: data.data },
|
|
145
|
+
});
|
|
146
|
+
if (isMountedRef.current) {
|
|
147
|
+
await debouncedOnSuccess();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (isMountedRef.current) {
|
|
152
|
+
setSelectedRateId(selectedRateId); // Revert to previous selection
|
|
153
|
+
setError(err instanceof Error ? err : new Error('Failed to update pickup point'));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
if (isMountedRef.current) {
|
|
158
|
+
setIsPending(false);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}, [apiService, debouncedOnSuccess, selectedRateId]);
|
|
162
|
+
const handleSelectRate = useCallback((rateId) => {
|
|
163
|
+
if (isPending || disabled || !isMountedRef.current)
|
|
164
|
+
return;
|
|
165
|
+
const selectedRate = shippingRates?.find((rate) => rate.id === rateId);
|
|
166
|
+
if (isMountedRef.current) {
|
|
167
|
+
setSelectedRateId(rateId);
|
|
168
|
+
setShippingRate({ shippingRateId: rateId });
|
|
169
|
+
}
|
|
170
|
+
}, [setShippingRate, isPending, disabled, shippingRates]);
|
|
171
|
+
// Auto-select shipping rate when only one is available
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!shippingRates?.length || selectedRateId || !isMountedRef.current)
|
|
174
|
+
return;
|
|
175
|
+
const nonPickupRates = shippingRates.filter((rate) => !rate.isPickupPoint);
|
|
176
|
+
if (nonPickupRates.length === 1) {
|
|
177
|
+
handleSelectRate(nonPickupRates[0].id);
|
|
178
|
+
}
|
|
179
|
+
else if (nonPickupRates.length > 1) {
|
|
180
|
+
const cheapestRate = nonPickupRates.reduce((prev, current) => current.amount < prev.amount ? current : prev);
|
|
181
|
+
handleSelectRate(cheapestRate.id);
|
|
182
|
+
}
|
|
183
|
+
}, [shippingRates, selectedRateId, handleSelectRate]);
|
|
184
|
+
// Get the currently selected shipping rate
|
|
185
|
+
const selectedRate = shippingRates?.find((rate) => rate.id === selectedRateId);
|
|
186
|
+
const isPickupPointSelected = Boolean(selectedRate?.isPickupPoint);
|
|
187
|
+
return {
|
|
188
|
+
shippingRates,
|
|
189
|
+
forceCheckoutSessionRefetch,
|
|
190
|
+
handleSelectRate,
|
|
191
|
+
isPickupPointSelected,
|
|
192
|
+
selectedRateId,
|
|
193
|
+
setSelectedRateId,
|
|
194
|
+
setShippingRate,
|
|
195
|
+
setShippingRateAsync,
|
|
196
|
+
isPending,
|
|
197
|
+
isFetching,
|
|
198
|
+
isLoading,
|
|
199
|
+
error,
|
|
200
|
+
refetch: fetchShippingRates,
|
|
201
|
+
setPickupPoint,
|
|
202
|
+
selectedRate,
|
|
203
|
+
};
|
|
204
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type Primitive = string | number | boolean | null | undefined;
|
|
2
|
+
export interface TranslateOptions {
|
|
3
|
+
defaultValue?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface UseTranslationsResult {
|
|
6
|
+
t: (key: string, vars?: Record<string, Primitive>, options?: TranslateOptions) => string;
|
|
7
|
+
messages: Record<string, string>;
|
|
8
|
+
locale: string;
|
|
9
|
+
language: string;
|
|
10
|
+
region: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* useTranslations - Lightweight translator for SDK plugins
|
|
14
|
+
* - Reads `locale.messages` populated by TagadaProvider session init
|
|
15
|
+
* - Provides `t(key, vars, options)` with simple `{var}` interpolation
|
|
16
|
+
*/
|
|
17
|
+
export declare function useTranslations(targetLanguageCode?: string): UseTranslationsResult;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
4
|
+
function interpolate(template, vars) {
|
|
5
|
+
if (!vars)
|
|
6
|
+
return template;
|
|
7
|
+
return template.replace(/\{(\w+)\}/g, (_, name) => {
|
|
8
|
+
const value = vars[name];
|
|
9
|
+
return value === null || value === undefined ? '' : String(value);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* useTranslations - Lightweight translator for SDK plugins
|
|
14
|
+
* - Reads `locale.messages` populated by TagadaProvider session init
|
|
15
|
+
* - Provides `t(key, vars, options)` with simple `{var}` interpolation
|
|
16
|
+
*/
|
|
17
|
+
export function useTranslations(targetLanguageCode) {
|
|
18
|
+
const { locale, apiService, pluginConfig } = useTagadaContext();
|
|
19
|
+
const selectedLanguage = targetLanguageCode || locale.language;
|
|
20
|
+
// Prefer backend messages for the selected language; fall back to provider
|
|
21
|
+
const [fetchedMessages, setFetchedMessages] = useState(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
let cancelled = false;
|
|
24
|
+
async function fetchMessages() {
|
|
25
|
+
const region = locale.region || 'US';
|
|
26
|
+
const desiredLanguage = selectedLanguage;
|
|
27
|
+
const targetLocale = `${desiredLanguage}-${region}`;
|
|
28
|
+
try {
|
|
29
|
+
const storeId = pluginConfig?.storeId;
|
|
30
|
+
if (!storeId) {
|
|
31
|
+
setFetchedMessages(null);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const data = await apiService.fetch('/api/v1/translation-messages', {
|
|
35
|
+
method: 'GET',
|
|
36
|
+
params: { locale: targetLocale, storeId },
|
|
37
|
+
});
|
|
38
|
+
if (!cancelled) {
|
|
39
|
+
setFetchedMessages(data || {});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (_err) {
|
|
43
|
+
if (!cancelled) {
|
|
44
|
+
setFetchedMessages(null);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
void fetchMessages();
|
|
49
|
+
return () => {
|
|
50
|
+
cancelled = true;
|
|
51
|
+
};
|
|
52
|
+
}, [selectedLanguage, locale.region, pluginConfig?.storeId, apiService]);
|
|
53
|
+
const messages = useMemo(() => {
|
|
54
|
+
if (fetchedMessages)
|
|
55
|
+
return fetchedMessages;
|
|
56
|
+
return locale.messages || {};
|
|
57
|
+
}, [fetchedMessages, locale.messages]);
|
|
58
|
+
const t = useMemo(() => {
|
|
59
|
+
return (key, vars, opts) => {
|
|
60
|
+
const raw = messages[key] ?? opts?.defaultValue ?? key;
|
|
61
|
+
return interpolate(raw, vars);
|
|
62
|
+
};
|
|
63
|
+
}, [messages]);
|
|
64
|
+
const computedLocale = useMemo(() => {
|
|
65
|
+
if (selectedLanguage === locale.language)
|
|
66
|
+
return locale.locale;
|
|
67
|
+
const region = locale.region || 'US';
|
|
68
|
+
return `${selectedLanguage}-${region}`;
|
|
69
|
+
}, [selectedLanguage, locale.locale, locale.language, locale.region]);
|
|
70
|
+
return {
|
|
71
|
+
t,
|
|
72
|
+
messages,
|
|
73
|
+
locale: computedLocale,
|
|
74
|
+
language: selectedLanguage,
|
|
75
|
+
region: computedLocale.split('-')[1] || 'US',
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { useOrderBump } from './hooks/useOrderBump';
|
|
|
16
16
|
export { usePostPurchases } from './hooks/usePostPurchases';
|
|
17
17
|
export { useProducts } from './hooks/useProducts';
|
|
18
18
|
export { useSession } from './hooks/useSession';
|
|
19
|
+
export { useTranslations } from './hooks/useTranslations';
|
|
19
20
|
export { useTagadaContext } from './providers/TagadaProvider';
|
|
20
21
|
export { clearPluginConfigCache, debugPluginConfig, getPluginConfig, useBasePath, usePluginConfig } from './hooks/usePluginConfig';
|
|
21
22
|
export type { PluginConfig } from './hooks/usePluginConfig';
|
|
@@ -28,6 +29,7 @@ export { usePayment } from './hooks/usePayment';
|
|
|
28
29
|
export { usePaymentPolling } from './hooks/usePaymentPolling';
|
|
29
30
|
export { useThreeds } from './hooks/useThreeds';
|
|
30
31
|
export { useThreedsModal } from './hooks/useThreedsModal';
|
|
32
|
+
export { useApplePay } from './hooks/useApplePay';
|
|
31
33
|
export type { AuthState, Currency, Customer, Environment, EnvironmentConfig, Locale, Order, OrderAddress, OrderItem, OrderSummary, PickupPoint, Session, Store } from './types';
|
|
32
34
|
export type { CheckoutData, CheckoutInitParams, CheckoutLineItem, CheckoutSession, CheckoutSessionPreview, Promotion, UseCheckoutOptions, UseCheckoutResult } from './hooks/useCheckout';
|
|
33
35
|
export type { Discount, DiscountCodeValidation, UseDiscountsOptions, UseDiscountsResult } from './hooks/useDiscounts';
|
|
@@ -36,4 +38,5 @@ export type { PostPurchaseOffer, PostPurchaseOfferItem, PostPurchaseOfferLineIte
|
|
|
36
38
|
export type { Payment, PaymentPollingHook, PollingOptions } from './hooks/usePaymentPolling';
|
|
37
39
|
export type { PaymentInstrument, ThreedsChallenge, ThreedsHook, ThreedsOptions, ThreedsProvider, ThreedsSession } from './hooks/useThreeds';
|
|
38
40
|
export type { ApplePayToken, CardPaymentMethod, PaymentHook, PaymentInstrumentResponse, PaymentOptions, PaymentResponse } from './hooks/usePayment';
|
|
41
|
+
export type { ApplePayConfig, ApplePayLineItem, ApplePayPaymentAuthorizedEvent, ApplePayPaymentRequest, ApplePayPaymentToken, ApplePayValidateMerchantEvent, BasisTheorySessionRequest, BasisTheoryTokenizeRequest, PayToken, UseApplePayOptions, UseApplePayResult } from './types/apple-pay';
|
|
39
42
|
export { convertCurrency, formatMoney, formatMoneyWithoutSymbol, formatSimpleMoney, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits } from './utils/money';
|
package/dist/react/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export { useOrderBump } from './hooks/useOrderBump';
|
|
|
19
19
|
export { usePostPurchases } from './hooks/usePostPurchases';
|
|
20
20
|
export { useProducts } from './hooks/useProducts';
|
|
21
21
|
export { useSession } from './hooks/useSession';
|
|
22
|
+
export { useTranslations } from './hooks/useTranslations';
|
|
22
23
|
export { useTagadaContext } from './providers/TagadaProvider';
|
|
23
24
|
// Plugin configuration hooks
|
|
24
25
|
export { clearPluginConfigCache, debugPluginConfig, getPluginConfig, useBasePath, usePluginConfig } from './hooks/usePluginConfig';
|
|
@@ -31,6 +32,8 @@ export { usePayment } from './hooks/usePayment';
|
|
|
31
32
|
export { usePaymentPolling } from './hooks/usePaymentPolling';
|
|
32
33
|
export { useThreeds } from './hooks/useThreeds';
|
|
33
34
|
export { useThreedsModal } from './hooks/useThreedsModal';
|
|
35
|
+
// Apple Pay hooks exports
|
|
36
|
+
export { useApplePay } from './hooks/useApplePay';
|
|
34
37
|
// Component exports (if any)
|
|
35
38
|
// export { SomeComponent } from './components/SomeComponent';
|
|
36
39
|
// Utility exports
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple Pay types for the Tagada Pay Plugin SDK
|
|
3
|
+
*/
|
|
4
|
+
export interface ApplePayPaymentRequest {
|
|
5
|
+
countryCode: string;
|
|
6
|
+
currencyCode: string;
|
|
7
|
+
supportedNetworks: string[];
|
|
8
|
+
merchantCapabilities: string[];
|
|
9
|
+
total: ApplePayLineItem;
|
|
10
|
+
lineItems: ApplePayLineItem[];
|
|
11
|
+
}
|
|
12
|
+
export interface ApplePayLineItem {
|
|
13
|
+
label: string;
|
|
14
|
+
amount: string;
|
|
15
|
+
type: 'final' | 'pending';
|
|
16
|
+
}
|
|
17
|
+
export interface ApplePayPaymentAuthorizedEvent {
|
|
18
|
+
payment: {
|
|
19
|
+
token: ApplePayPaymentToken;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface ApplePayPaymentToken {
|
|
23
|
+
paymentData: any;
|
|
24
|
+
paymentMethod: {
|
|
25
|
+
displayName: string;
|
|
26
|
+
network: string;
|
|
27
|
+
type: string;
|
|
28
|
+
};
|
|
29
|
+
transactionIdentifier: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ApplePayValidateMerchantEvent {
|
|
32
|
+
validationURL: string;
|
|
33
|
+
}
|
|
34
|
+
export interface BasisTheorySessionRequest {
|
|
35
|
+
display_name: string;
|
|
36
|
+
domain: string;
|
|
37
|
+
}
|
|
38
|
+
export interface BasisTheoryTokenizeRequest {
|
|
39
|
+
apple_payment_data: ApplePayPaymentToken;
|
|
40
|
+
}
|
|
41
|
+
export interface PayToken {
|
|
42
|
+
id: string;
|
|
43
|
+
type: string;
|
|
44
|
+
card: {
|
|
45
|
+
bin: string;
|
|
46
|
+
last4: string;
|
|
47
|
+
expiration_month: number;
|
|
48
|
+
expiration_year: number;
|
|
49
|
+
brand: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface ApplePayConfig {
|
|
53
|
+
countryCode?: string;
|
|
54
|
+
supportedNetworks?: string[];
|
|
55
|
+
merchantCapabilities?: string[];
|
|
56
|
+
}
|
|
57
|
+
export interface UseApplePayOptions {
|
|
58
|
+
onSuccess?: (payment: any) => void;
|
|
59
|
+
onError?: (error: string) => void;
|
|
60
|
+
onCancel?: () => void;
|
|
61
|
+
config?: ApplePayConfig;
|
|
62
|
+
}
|
|
63
|
+
export interface UseApplePayResult {
|
|
64
|
+
handleApplePayClick: (checkoutSessionId: string, lineItems: ApplePayLineItem[], total: ApplePayLineItem, config?: ApplePayConfig) => void;
|
|
65
|
+
processingPayment: boolean;
|
|
66
|
+
applePayError: string | null;
|
|
67
|
+
isApplePayAvailable: boolean;
|
|
68
|
+
}
|
|
69
|
+
declare global {
|
|
70
|
+
interface Window {
|
|
71
|
+
ApplePaySession: typeof ApplePaySession;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
declare class ApplePaySession {
|
|
75
|
+
static STATUS_SUCCESS: number;
|
|
76
|
+
static STATUS_FAILURE: number;
|
|
77
|
+
static canMakePayments(): boolean;
|
|
78
|
+
static canMakePaymentsWithActiveCard(merchantIdentifier: string): Promise<boolean>;
|
|
79
|
+
constructor(version: number, request: ApplePayPaymentRequest);
|
|
80
|
+
begin(): void;
|
|
81
|
+
abort(): void;
|
|
82
|
+
onerror: ((event: any) => void) | null;
|
|
83
|
+
completeMerchantValidation(merchantSession: any): void;
|
|
84
|
+
completeShippingContactSelection(status: number, shippingMethods: any[], total: any, lineItems: any[]): void;
|
|
85
|
+
completeShippingMethodSelection(status: number, total: any, lineItems: any[]): void;
|
|
86
|
+
completePayment(status: number): void;
|
|
87
|
+
onvalidatemerchant: ((event: ApplePayValidateMerchantEvent) => void) | null;
|
|
88
|
+
onpaymentauthorized: ((event: ApplePayPaymentAuthorizedEvent) => void) | null;
|
|
89
|
+
oncancel: (() => void) | null;
|
|
90
|
+
onshippingmethodselected: ((event: any) => void) | null;
|
|
91
|
+
onshippingcontactselected: ((event: any) => void) | null;
|
|
92
|
+
}
|
|
93
|
+
export {};
|