@tagadapay/plugin-sdk 2.3.8 → 2.3.10
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.js +194 -38
- package/dist/react/hooks/useCheckout.js +0 -9
- package/dist/react/hooks/usePluginConfig.js +2 -0
- package/dist/react/hooks/useVipOffers.d.ts +69 -0
- package/dist/react/hooks/useVipOffers.js +144 -0
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.js +3 -2
- package/dist/react/types/apple-pay.d.ts +26 -1
- package/package.json +1 -1
|
@@ -1,51 +1,139 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { getBasisTheoryApiKey } from '../config/payment';
|
|
3
3
|
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
4
4
|
import { usePayment } from './usePayment';
|
|
5
5
|
export function useApplePay(options = {}) {
|
|
6
6
|
const [processingPayment, setProcessingPayment] = useState(false);
|
|
7
7
|
const [error, setError] = useState(null);
|
|
8
|
+
const [isApplePayAvailable, setIsApplePayAvailable] = useState(false);
|
|
8
9
|
const { createApplePayPaymentInstrument, processApplePayPayment } = usePayment();
|
|
9
10
|
const { environment, apiService } = useTagadaContext();
|
|
10
11
|
// Get API key from environment
|
|
11
12
|
const apiKey = getBasisTheoryApiKey(environment?.environment || 'local');
|
|
12
|
-
// Check
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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;
|
|
13
|
+
// Check Apple Pay availability on mount
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const checkApplePayAvailability = () => {
|
|
16
|
+
if (typeof window === 'undefined') {
|
|
17
|
+
setIsApplePayAvailable(false);
|
|
18
|
+
return;
|
|
26
19
|
}
|
|
27
|
-
|
|
20
|
+
// Check if ApplePaySession is available
|
|
21
|
+
const hasApplePaySession = !!window.ApplePaySession;
|
|
22
|
+
if (!hasApplePaySession) {
|
|
23
|
+
// In development, simulate Apple Pay availability for UI testing
|
|
24
|
+
const isDevelopment = process.env.NODE_ENV === 'development' ||
|
|
25
|
+
window.location.hostname === 'localhost' ||
|
|
26
|
+
window.location.hostname.includes('127.0.0.1');
|
|
27
|
+
if (isDevelopment) {
|
|
28
|
+
setIsApplePayAvailable(true);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
setIsApplePayAvailable(false);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
// Check basic Apple Pay support
|
|
36
|
+
const canMakePayments = window.ApplePaySession.canMakePayments();
|
|
37
|
+
setIsApplePayAvailable(canMakePayments);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.warn('Apple Pay availability check failed:', error);
|
|
41
|
+
setIsApplePayAvailable(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
checkApplePayAvailability();
|
|
45
|
+
// Debug logging
|
|
46
|
+
}, []);
|
|
47
|
+
// Utility function to convert Apple Pay contact to address
|
|
48
|
+
const appleContactToAddress = useCallback((contact) => {
|
|
49
|
+
return {
|
|
50
|
+
address1: contact?.addressLines?.[0] || '',
|
|
51
|
+
address2: contact?.addressLines?.[1] || '',
|
|
52
|
+
lastName: contact?.familyName || '',
|
|
53
|
+
firstName: contact?.givenName || '',
|
|
54
|
+
city: contact?.locality || '',
|
|
55
|
+
state: contact?.administrativeArea || '',
|
|
56
|
+
country: contact?.countryCode || '',
|
|
57
|
+
postal: contact?.postalCode || '',
|
|
58
|
+
phone: contact?.phoneNumber || '',
|
|
59
|
+
email: contact?.emailAddress || '',
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
// Update checkout session with addresses and customer info
|
|
63
|
+
const updateCheckoutSessionValues = useCallback(async (data) => {
|
|
64
|
+
try {
|
|
65
|
+
await apiService.fetch(`/api/v1/checkout-sessions/${options.checkoutSessionId}/address`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
body: {
|
|
68
|
+
data: {
|
|
69
|
+
shippingAddress: data.shippingAddress,
|
|
70
|
+
billingAddress: data.billingAddress,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error('Failed to update checkout session addresses:', error);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}, [apiService, options.checkoutSessionId]);
|
|
80
|
+
// Update customer email
|
|
81
|
+
const updateCustomerEmail = useCallback(async (email) => {
|
|
82
|
+
try {
|
|
83
|
+
await apiService.fetch(`/api/v1/customers/${options.customerId}`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: {
|
|
86
|
+
data: {
|
|
87
|
+
email,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error('Failed to update customer email:', error);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}, [apiService, options.customerId]);
|
|
97
|
+
// Recompute order summary after address/shipping changes
|
|
98
|
+
const reComputeOrderSummary = useCallback(async () => {
|
|
99
|
+
try {
|
|
100
|
+
const response = await apiService.fetch(`/api/v1/checkout-sessions/${options.checkoutSessionId}/order-summary`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
});
|
|
103
|
+
return response;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error('Failed to recompute order summary:', error);
|
|
107
|
+
throw error;
|
|
28
108
|
}
|
|
109
|
+
}, [apiService, options.checkoutSessionId]);
|
|
110
|
+
// Get shipping rates for the checkout session
|
|
111
|
+
const getShippingRates = useCallback(async () => {
|
|
29
112
|
try {
|
|
30
|
-
|
|
31
|
-
return
|
|
113
|
+
const response = await apiService.fetch(`/api/v1/checkout-sessions/${options.checkoutSessionId}/shipping-rates`);
|
|
114
|
+
return response;
|
|
32
115
|
}
|
|
33
116
|
catch (error) {
|
|
34
|
-
console.
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
117
|
+
console.error('Failed to get shipping rates:', error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}, [apiService, options.checkoutSessionId]);
|
|
121
|
+
// Set shipping rate
|
|
122
|
+
const setShippingRate = useCallback(async (shippingRateId) => {
|
|
123
|
+
try {
|
|
124
|
+
await apiService.fetch(`/api/v1/checkout-sessions/${options.checkoutSessionId}/shipping-rate`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
body: {
|
|
127
|
+
shippingRateId,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.error('Failed to set shipping rate:', error);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}, [apiService, options.checkoutSessionId]);
|
|
136
|
+
const validateMerchant = useCallback(async (storeName) => {
|
|
49
137
|
try {
|
|
50
138
|
const response = await fetch('https://api.basistheory.com/apple-pay/session', {
|
|
51
139
|
method: 'POST',
|
|
@@ -54,7 +142,7 @@ export function useApplePay(options = {}) {
|
|
|
54
142
|
'BT-API-KEY': apiKey,
|
|
55
143
|
},
|
|
56
144
|
body: JSON.stringify({
|
|
57
|
-
display_name: 'Tagada Pay Store',
|
|
145
|
+
display_name: storeName || 'Tagada Pay Store',
|
|
58
146
|
domain: typeof window !== 'undefined' ? window.location.host : 'localhost',
|
|
59
147
|
}),
|
|
60
148
|
});
|
|
@@ -92,7 +180,7 @@ export function useApplePay(options = {}) {
|
|
|
92
180
|
throw error;
|
|
93
181
|
}
|
|
94
182
|
}, [apiKey]);
|
|
95
|
-
const handleApplePayClick = useCallback((checkoutSessionId, lineItems, total, config = {}) => {
|
|
183
|
+
const handleApplePayClick = useCallback((checkoutSessionId, lineItems, total, config = {}, storeName, currencyCode, shippingMethods) => {
|
|
96
184
|
if (!isApplePayAvailable) {
|
|
97
185
|
const errorMsg = 'Apple Pay is not available on this device';
|
|
98
186
|
setError(errorMsg);
|
|
@@ -101,19 +189,21 @@ export function useApplePay(options = {}) {
|
|
|
101
189
|
}
|
|
102
190
|
const request = {
|
|
103
191
|
countryCode: config.countryCode || 'US',
|
|
104
|
-
currencyCode: 'USD',
|
|
192
|
+
currencyCode: currencyCode || 'USD',
|
|
105
193
|
supportedNetworks: config.supportedNetworks || ['visa', 'masterCard', 'amex', 'discover'],
|
|
106
194
|
merchantCapabilities: config.merchantCapabilities || ['supports3DS'],
|
|
107
195
|
total,
|
|
108
196
|
lineItems,
|
|
197
|
+
shippingMethods: shippingMethods || [],
|
|
198
|
+
requiredShippingContactFields: ['name', 'phone', 'email', 'postalAddress'],
|
|
199
|
+
requiredBillingContactFields: ['postalAddress'],
|
|
109
200
|
};
|
|
110
201
|
try {
|
|
111
202
|
const session = new window.ApplePaySession(3, request);
|
|
112
203
|
session.onvalidatemerchant = (event) => {
|
|
113
204
|
void (async () => {
|
|
114
205
|
try {
|
|
115
|
-
|
|
116
|
-
const merchantSession = await validateMerchant();
|
|
206
|
+
const merchantSession = await validateMerchant(storeName);
|
|
117
207
|
session.completeMerchantValidation(merchantSession);
|
|
118
208
|
}
|
|
119
209
|
catch (error) {
|
|
@@ -130,6 +220,22 @@ export function useApplePay(options = {}) {
|
|
|
130
220
|
try {
|
|
131
221
|
setProcessingPayment(true);
|
|
132
222
|
setError(null);
|
|
223
|
+
// Extract address information
|
|
224
|
+
const shippingContact = event.payment.shippingContact;
|
|
225
|
+
const billingContact = event.payment.billingContact;
|
|
226
|
+
const shippingAddress = shippingContact ? appleContactToAddress(shippingContact) : null;
|
|
227
|
+
const billingAddress = billingContact ? appleContactToAddress(billingContact) : null;
|
|
228
|
+
// Update checkout session with addresses
|
|
229
|
+
if (shippingAddress || billingAddress) {
|
|
230
|
+
await updateCheckoutSessionValues({
|
|
231
|
+
shippingAddress: shippingAddress || undefined,
|
|
232
|
+
billingAddress: billingAddress || undefined,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// Update customer email if available
|
|
236
|
+
if (shippingContact?.emailAddress) {
|
|
237
|
+
await updateCustomerEmail(shippingContact.emailAddress);
|
|
238
|
+
}
|
|
133
239
|
// Tokenize the Apple Pay payment
|
|
134
240
|
const applePayToken = await tokenizeApplePay(event);
|
|
135
241
|
// Complete the Apple Pay session
|
|
@@ -157,6 +263,48 @@ export function useApplePay(options = {}) {
|
|
|
157
263
|
}
|
|
158
264
|
})();
|
|
159
265
|
};
|
|
266
|
+
// Handle shipping method selection
|
|
267
|
+
session.onshippingmethodselected = (event) => {
|
|
268
|
+
void (async () => {
|
|
269
|
+
try {
|
|
270
|
+
await setShippingRate(event.shippingMethod.identifier);
|
|
271
|
+
const newOrderSummary = await reComputeOrderSummary();
|
|
272
|
+
if (!newOrderSummary) {
|
|
273
|
+
session.abort();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const { lineItems: newLineItems, total: newTotal } = newOrderSummary;
|
|
277
|
+
session.completeShippingMethodSelection(window.ApplePaySession.STATUS_SUCCESS, newTotal, newLineItems);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
console.error('Shipping method selection failed:', error);
|
|
281
|
+
session.abort();
|
|
282
|
+
}
|
|
283
|
+
})();
|
|
284
|
+
};
|
|
285
|
+
// Handle shipping contact selection
|
|
286
|
+
session.onshippingcontactselected = (event) => {
|
|
287
|
+
void (async () => {
|
|
288
|
+
try {
|
|
289
|
+
const shippingContact = event.shippingContact;
|
|
290
|
+
await updateCheckoutSessionValues({
|
|
291
|
+
shippingAddress: appleContactToAddress(shippingContact),
|
|
292
|
+
});
|
|
293
|
+
const newOrderSummary = await reComputeOrderSummary();
|
|
294
|
+
if (!newOrderSummary) {
|
|
295
|
+
session.abort();
|
|
296
|
+
setError('Payment Failed');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const { lineItems: newLineItems, total: newTotal, shippingMethods: newShippingMethods, } = newOrderSummary;
|
|
300
|
+
session.completeShippingContactSelection(window.ApplePaySession.STATUS_SUCCESS, newShippingMethods, newTotal, newLineItems);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
console.error('Shipping contact selection failed:', error);
|
|
304
|
+
session.abort();
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
};
|
|
160
308
|
session.onerror = (event) => {
|
|
161
309
|
console.error('Apple Pay Session Error:', event);
|
|
162
310
|
const errorMsg = 'Apple Pay session error';
|
|
@@ -168,6 +316,7 @@ export function useApplePay(options = {}) {
|
|
|
168
316
|
setProcessingPayment(false);
|
|
169
317
|
options.onCancel?.();
|
|
170
318
|
};
|
|
319
|
+
// Begin the Apple Pay session - this opens the modal
|
|
171
320
|
session.begin();
|
|
172
321
|
}
|
|
173
322
|
catch (error) {
|
|
@@ -181,6 +330,10 @@ export function useApplePay(options = {}) {
|
|
|
181
330
|
validateMerchant,
|
|
182
331
|
tokenizeApplePay,
|
|
183
332
|
processApplePayPayment,
|
|
333
|
+
updateCheckoutSessionValues,
|
|
334
|
+
updateCustomerEmail,
|
|
335
|
+
setShippingRate,
|
|
336
|
+
reComputeOrderSummary,
|
|
184
337
|
options,
|
|
185
338
|
]);
|
|
186
339
|
return {
|
|
@@ -188,5 +341,8 @@ export function useApplePay(options = {}) {
|
|
|
188
341
|
processingPayment,
|
|
189
342
|
applePayError: error,
|
|
190
343
|
isApplePayAvailable,
|
|
344
|
+
updateCheckoutSessionValues,
|
|
345
|
+
updateCustomerEmail,
|
|
346
|
+
setShippingRate,
|
|
191
347
|
};
|
|
192
348
|
}
|
|
@@ -179,19 +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
|
-
});
|
|
186
182
|
if (!currentCheckoutTokenRef.current) {
|
|
187
183
|
throw new Error('No checkout session to refresh');
|
|
188
184
|
}
|
|
189
|
-
console.log('🔄 [useCheckout] Refreshing checkout data...', {
|
|
190
|
-
checkoutToken: currentCheckoutTokenRef.current.substring(0, 8) + '...',
|
|
191
|
-
timestamp: new Date().toISOString(),
|
|
192
|
-
});
|
|
193
185
|
await getCheckout(currentCheckoutTokenRef.current);
|
|
194
|
-
console.log('✅ [useCheckout] Refresh completed, debug data will be updated automatically');
|
|
195
186
|
}, [getCheckout]);
|
|
196
187
|
// Register refresh function with coordinator and cleanup on unmount
|
|
197
188
|
useEffect(() => {
|
|
@@ -19,6 +19,7 @@ const loadLocalDevConfig = async (configVariant = 'default') => {
|
|
|
19
19
|
// Use hostname-based detection for better Vite compatibility
|
|
20
20
|
const isLocalDev = typeof window !== 'undefined' &&
|
|
21
21
|
(window.location.hostname === 'localhost' ||
|
|
22
|
+
window.location.hostname.includes('ngrok-free.app') ||
|
|
22
23
|
window.location.hostname.includes('.localhost') ||
|
|
23
24
|
window.location.hostname.includes('127.0.0.1'));
|
|
24
25
|
if (!isLocalDev) {
|
|
@@ -184,6 +185,7 @@ export const debugPluginConfig = async (configVariant = 'default') => {
|
|
|
184
185
|
// Use hostname-based detection for better Vite compatibility
|
|
185
186
|
const isLocalDev = typeof window !== 'undefined' &&
|
|
186
187
|
(window.location.hostname === 'localhost' ||
|
|
188
|
+
window.location.hostname.includes('ngrok-free.app') ||
|
|
187
189
|
window.location.hostname.includes('.localhost') ||
|
|
188
190
|
window.location.hostname.includes('127.0.0.1'));
|
|
189
191
|
if (!isLocalDev) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface VipOffer {
|
|
2
|
+
id: string;
|
|
3
|
+
productId: string;
|
|
4
|
+
variantId: string;
|
|
5
|
+
}
|
|
6
|
+
export interface VipPreviewResponse {
|
|
7
|
+
savings: number;
|
|
8
|
+
currency: string;
|
|
9
|
+
selectedOffers: {
|
|
10
|
+
productId: string;
|
|
11
|
+
variantId: string;
|
|
12
|
+
isSelected: boolean;
|
|
13
|
+
}[];
|
|
14
|
+
savingsPct: number;
|
|
15
|
+
}
|
|
16
|
+
export interface UseVipOffersOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Checkout session ID for VIP offers
|
|
19
|
+
*/
|
|
20
|
+
checkoutSessionId: string;
|
|
21
|
+
/**
|
|
22
|
+
* Array of VIP offer IDs to manage
|
|
23
|
+
*/
|
|
24
|
+
vipOfferIds: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Whether to automatically fetch VIP preview on mount
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
autoPreview?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface UseVipOffersResult {
|
|
32
|
+
/**
|
|
33
|
+
* Array of VIP offers
|
|
34
|
+
*/
|
|
35
|
+
vipOffers: VipOffer[];
|
|
36
|
+
/**
|
|
37
|
+
* VIP preview data including savings and selected offers
|
|
38
|
+
*/
|
|
39
|
+
vipPreview: VipPreviewResponse | null;
|
|
40
|
+
/**
|
|
41
|
+
* Loading state for VIP preview
|
|
42
|
+
*/
|
|
43
|
+
isLoadingVipPreview: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether any VIP offers are currently selected
|
|
46
|
+
*/
|
|
47
|
+
hasVip: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Check if a specific VIP offer is selected
|
|
50
|
+
*/
|
|
51
|
+
isOfferSelected: (offer: VipOffer) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Select all VIP offers
|
|
54
|
+
*/
|
|
55
|
+
selectVipOffers: () => Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Cancel/deselect all VIP offers
|
|
58
|
+
*/
|
|
59
|
+
cancelVipOffers: () => Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Refresh VIP preview data
|
|
62
|
+
*/
|
|
63
|
+
refreshVipPreview: () => Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Error state
|
|
66
|
+
*/
|
|
67
|
+
error: Error | null;
|
|
68
|
+
}
|
|
69
|
+
export declare function useVipOffers(options: UseVipOffersOptions): UseVipOffersResult;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
3
|
+
export function useVipOffers(options) {
|
|
4
|
+
const { apiService, refreshCoordinator } = useTagadaContext();
|
|
5
|
+
const { checkoutSessionId, vipOfferIds, autoPreview = true } = options;
|
|
6
|
+
const [vipOffers, setVipOffers] = useState([]);
|
|
7
|
+
const [vipPreview, setVipPreview] = useState(null);
|
|
8
|
+
const [isLoadingVipPreview, setIsLoadingVipPreview] = useState(false);
|
|
9
|
+
const [hasVip, setHasVip] = useState(false);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
// Convert offer IDs to VipOffer objects
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const offers = vipOfferIds.map((id) => ({
|
|
14
|
+
id,
|
|
15
|
+
productId: '', // These will be populated from the checkout session data
|
|
16
|
+
variantId: '', // These will be populated from the checkout session data
|
|
17
|
+
}));
|
|
18
|
+
setVipOffers(offers);
|
|
19
|
+
}, [vipOfferIds]);
|
|
20
|
+
// Fetch VIP preview
|
|
21
|
+
const refreshVipPreview = useCallback(async () => {
|
|
22
|
+
if (!checkoutSessionId || vipOfferIds.length === 0)
|
|
23
|
+
return;
|
|
24
|
+
setIsLoadingVipPreview(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
try {
|
|
27
|
+
const response = await apiService.fetch(`/api/v1/checkout-sessions/${checkoutSessionId}/vip-preview`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
body: {
|
|
30
|
+
orderBumpOfferIds: vipOfferIds,
|
|
31
|
+
orderBumpType: 'vip',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
setVipPreview(response);
|
|
35
|
+
// Update hasVip based on selected offers
|
|
36
|
+
const hasSelectedVipOffers = response.selectedOffers?.some((offer) => offer.isSelected) ?? false;
|
|
37
|
+
setHasVip(hasSelectedVipOffers);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const error = err instanceof Error ? err : new Error('Failed to fetch VIP preview');
|
|
41
|
+
setError(error);
|
|
42
|
+
console.error('VIP preview failed:', error);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
setIsLoadingVipPreview(false);
|
|
46
|
+
}
|
|
47
|
+
}, [checkoutSessionId, vipOfferIds]);
|
|
48
|
+
// Check if a specific VIP offer is selected
|
|
49
|
+
const isOfferSelected = useCallback((offer) => {
|
|
50
|
+
if (!vipPreview?.selectedOffers)
|
|
51
|
+
return false;
|
|
52
|
+
return vipPreview.selectedOffers.some((selected) => selected.productId === offer.productId &&
|
|
53
|
+
selected.variantId === offer.variantId &&
|
|
54
|
+
selected.isSelected);
|
|
55
|
+
}, [vipPreview?.selectedOffers]);
|
|
56
|
+
// Keep hasVip in sync with actual selected offers
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const hasSelectedVipOffers = vipOffers.some(isOfferSelected);
|
|
59
|
+
if (hasSelectedVipOffers !== hasVip) {
|
|
60
|
+
setHasVip(hasSelectedVipOffers);
|
|
61
|
+
}
|
|
62
|
+
}, [vipOffers, isOfferSelected, hasVip, vipPreview?.selectedOffers]);
|
|
63
|
+
// Toggle VIP offers mutation
|
|
64
|
+
const toggleOrderBump = useCallback(async (orderBumpOfferId, selected) => {
|
|
65
|
+
try {
|
|
66
|
+
const response = await apiService.fetch(`/api/v1/checkout-sessions/${checkoutSessionId}/toggle-order-bump`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
body: {
|
|
69
|
+
orderBumpOfferId,
|
|
70
|
+
selected,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (response.success) {
|
|
74
|
+
// Notify other hooks that checkout data changed
|
|
75
|
+
await refreshCoordinator.notifyCheckoutChanged();
|
|
76
|
+
}
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const error = err instanceof Error ? err : new Error('Failed to toggle order bump');
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}, [checkoutSessionId]);
|
|
84
|
+
// Handle selecting VIP offers
|
|
85
|
+
const selectVipOffers = useCallback(async () => {
|
|
86
|
+
try {
|
|
87
|
+
await Promise.all(vipOffers.map((offer) => {
|
|
88
|
+
if (!isOfferSelected(offer)) {
|
|
89
|
+
return toggleOrderBump(offer.id, true);
|
|
90
|
+
}
|
|
91
|
+
return Promise.resolve({ success: true });
|
|
92
|
+
}));
|
|
93
|
+
setHasVip(true);
|
|
94
|
+
await refreshVipPreview();
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const error = err instanceof Error ? err : new Error('Failed to select VIP offers');
|
|
98
|
+
setError(error);
|
|
99
|
+
console.error('Failed to select VIP offers:', error);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}, [vipOffers, isOfferSelected, toggleOrderBump, refreshVipPreview]);
|
|
103
|
+
// Handle canceling VIP offers
|
|
104
|
+
const cancelVipOffers = useCallback(async () => {
|
|
105
|
+
try {
|
|
106
|
+
setHasVip(false);
|
|
107
|
+
await Promise.all(vipOffers.map((offer) => toggleOrderBump(offer.id, false)));
|
|
108
|
+
await refreshVipPreview();
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const error = err instanceof Error ? err : new Error('Failed to cancel VIP offers');
|
|
112
|
+
setError(error);
|
|
113
|
+
console.error('Failed to cancel VIP offers:', error);
|
|
114
|
+
setHasVip(true); // Revert optimistic update
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}, [vipOffers, toggleOrderBump, refreshVipPreview]);
|
|
118
|
+
// Auto-fetch preview on mount and when dependencies change
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (autoPreview && checkoutSessionId && vipOfferIds.length > 0) {
|
|
121
|
+
refreshVipPreview().catch((error) => {
|
|
122
|
+
console.error('Auto-preview failed:', error);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}, [autoPreview, refreshVipPreview]);
|
|
126
|
+
// Register refresh function with coordinator and cleanup on unmount
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
refreshCoordinator.registerOrderBumpRefresh(refreshVipPreview);
|
|
129
|
+
return () => {
|
|
130
|
+
refreshCoordinator.unregisterOrderBumpRefresh();
|
|
131
|
+
};
|
|
132
|
+
}, [refreshVipPreview]);
|
|
133
|
+
return {
|
|
134
|
+
vipOffers,
|
|
135
|
+
vipPreview,
|
|
136
|
+
isLoadingVipPreview,
|
|
137
|
+
hasVip,
|
|
138
|
+
isOfferSelected,
|
|
139
|
+
selectVipOffers,
|
|
140
|
+
cancelVipOffers,
|
|
141
|
+
refreshVipPreview,
|
|
142
|
+
error,
|
|
143
|
+
};
|
|
144
|
+
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { usePostPurchases } from './hooks/usePostPurchases';
|
|
|
17
17
|
export { useProducts } from './hooks/useProducts';
|
|
18
18
|
export { useSession } from './hooks/useSession';
|
|
19
19
|
export { useTranslations } from './hooks/useTranslations';
|
|
20
|
+
export { useVipOffers } from './hooks/useVipOffers';
|
|
20
21
|
export { useTagadaContext } from './providers/TagadaProvider';
|
|
21
22
|
export { clearPluginConfigCache, debugPluginConfig, getPluginConfig, useBasePath, usePluginConfig } from './hooks/usePluginConfig';
|
|
22
23
|
export type { PluginConfig } from './hooks/usePluginConfig';
|
|
@@ -34,9 +35,10 @@ export type { AuthState, Currency, Customer, Environment, EnvironmentConfig, Loc
|
|
|
34
35
|
export type { CheckoutData, CheckoutInitParams, CheckoutLineItem, CheckoutSession, CheckoutSessionPreview, Promotion, UseCheckoutOptions, UseCheckoutResult } from './hooks/useCheckout';
|
|
35
36
|
export type { Discount, DiscountCodeValidation, UseDiscountsOptions, UseDiscountsResult } from './hooks/useDiscounts';
|
|
36
37
|
export type { OrderBumpPreview, UseOrderBumpOptions, UseOrderBumpResult } from './hooks/useOrderBump';
|
|
38
|
+
export type { UseVipOffersOptions, UseVipOffersResult, VipOffer, VipPreviewResponse } from './hooks/useVipOffers';
|
|
37
39
|
export type { PostPurchaseOffer, PostPurchaseOfferItem, PostPurchaseOfferLineItem, PostPurchaseOfferSummary, UsePostPurchasesOptions, UsePostPurchasesResult } from './hooks/usePostPurchases';
|
|
38
40
|
export type { Payment, PaymentPollingHook, PollingOptions } from './hooks/usePaymentPolling';
|
|
39
41
|
export type { PaymentInstrument, ThreedsChallenge, ThreedsHook, ThreedsOptions, ThreedsProvider, ThreedsSession } from './hooks/useThreeds';
|
|
40
42
|
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';
|
|
43
|
+
export type { ApplePayAddress, ApplePayConfig, ApplePayLineItem, ApplePayPaymentAuthorizedEvent, ApplePayPaymentRequest, ApplePayPaymentToken, ApplePayValidateMerchantEvent, BasisTheorySessionRequest, BasisTheoryTokenizeRequest, PayToken, UseApplePayOptions, UseApplePayResult } from './types/apple-pay';
|
|
42
44
|
export { convertCurrency, formatMoney, formatMoneyWithoutSymbol, formatSimpleMoney, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits } from './utils/money';
|
package/dist/react/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export { usePostPurchases } from './hooks/usePostPurchases';
|
|
|
20
20
|
export { useProducts } from './hooks/useProducts';
|
|
21
21
|
export { useSession } from './hooks/useSession';
|
|
22
22
|
export { useTranslations } from './hooks/useTranslations';
|
|
23
|
+
export { useVipOffers } from './hooks/useVipOffers';
|
|
23
24
|
export { useTagadaContext } from './providers/TagadaProvider';
|
|
24
25
|
// Plugin configuration hooks
|
|
25
26
|
export { clearPluginConfigCache, debugPluginConfig, getPluginConfig, useBasePath, usePluginConfig } from './hooks/usePluginConfig';
|
|
@@ -34,7 +35,7 @@ export { useThreeds } from './hooks/useThreeds';
|
|
|
34
35
|
export { useThreedsModal } from './hooks/useThreedsModal';
|
|
35
36
|
// Apple Pay hooks exports
|
|
36
37
|
export { useApplePay } from './hooks/useApplePay';
|
|
37
|
-
// Component exports
|
|
38
|
-
//
|
|
38
|
+
// Component exports
|
|
39
|
+
// Apple Pay components removed - use useApplePay hook directly
|
|
39
40
|
// Utility exports
|
|
40
41
|
export { convertCurrency, formatMoney, formatMoneyWithoutSymbol, formatSimpleMoney, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits } from './utils/money';
|
|
@@ -8,6 +8,9 @@ export interface ApplePayPaymentRequest {
|
|
|
8
8
|
merchantCapabilities: string[];
|
|
9
9
|
total: ApplePayLineItem;
|
|
10
10
|
lineItems: ApplePayLineItem[];
|
|
11
|
+
requiredShippingContactFields?: string[];
|
|
12
|
+
requiredBillingContactFields?: string[];
|
|
13
|
+
shippingMethods?: any[];
|
|
11
14
|
}
|
|
12
15
|
export interface ApplePayLineItem {
|
|
13
16
|
label: string;
|
|
@@ -17,6 +20,8 @@ export interface ApplePayLineItem {
|
|
|
17
20
|
export interface ApplePayPaymentAuthorizedEvent {
|
|
18
21
|
payment: {
|
|
19
22
|
token: ApplePayPaymentToken;
|
|
23
|
+
shippingContact?: any;
|
|
24
|
+
billingContact?: any;
|
|
20
25
|
};
|
|
21
26
|
}
|
|
22
27
|
export interface ApplePayPaymentToken {
|
|
@@ -59,12 +64,32 @@ export interface UseApplePayOptions {
|
|
|
59
64
|
onError?: (error: string) => void;
|
|
60
65
|
onCancel?: () => void;
|
|
61
66
|
config?: ApplePayConfig;
|
|
67
|
+
checkoutSessionId?: string;
|
|
68
|
+
customerId?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface ApplePayAddress {
|
|
71
|
+
address1: string;
|
|
72
|
+
address2?: string;
|
|
73
|
+
lastName: string;
|
|
74
|
+
firstName: string;
|
|
75
|
+
city: string;
|
|
76
|
+
state: string;
|
|
77
|
+
country: string;
|
|
78
|
+
postal: string;
|
|
79
|
+
phone?: string;
|
|
80
|
+
email?: string;
|
|
62
81
|
}
|
|
63
82
|
export interface UseApplePayResult {
|
|
64
|
-
handleApplePayClick: (checkoutSessionId: string, lineItems: ApplePayLineItem[], total: ApplePayLineItem, config?: ApplePayConfig) => void;
|
|
83
|
+
handleApplePayClick: (checkoutSessionId: string, lineItems: ApplePayLineItem[], total: ApplePayLineItem, config?: ApplePayConfig, storeName?: string, currencyCode?: string, shippingMethods?: any[]) => void;
|
|
65
84
|
processingPayment: boolean;
|
|
66
85
|
applePayError: string | null;
|
|
67
86
|
isApplePayAvailable: boolean;
|
|
87
|
+
updateCheckoutSessionValues: (data: {
|
|
88
|
+
shippingAddress?: ApplePayAddress;
|
|
89
|
+
billingAddress?: ApplePayAddress;
|
|
90
|
+
}) => Promise<void>;
|
|
91
|
+
updateCustomerEmail: (email: string) => Promise<void>;
|
|
92
|
+
setShippingRate: (shippingRateId: string) => Promise<void>;
|
|
68
93
|
}
|
|
69
94
|
declare global {
|
|
70
95
|
interface Window {
|