@tagadapay/plugin-sdk 2.3.6 → 2.3.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 +623 -623
- package/dist/react/components/ApplePayUniversalButton.d.ts +16 -0
- package/dist/react/components/ApplePayUniversalButton.example.d.ts +3 -0
- package/dist/react/components/ApplePayUniversalButton.example.js +39 -0
- package/dist/react/components/ApplePayUniversalButton.js +62 -0
- package/dist/react/hooks/useApplePay.js +69 -1
- package/dist/react/hooks/useCheckout.js +0 -5
- package/dist/react/hooks/useTranslations.d.ts +18 -0
- package/dist/react/hooks/useTranslations.js +77 -0
- package/dist/react/hooks/useVipOffers.d.ts +69 -0
- package/dist/react/hooks/useVipOffers.js +144 -0
- package/dist/react/index.d.ts +5 -1
- package/dist/react/index.js +4 -2
- package/dist/react/providers/TagadaProvider.js +5 -5
- package/dist/react/types/apple-pay.d.ts +27 -0
- package/package.json +83 -83
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ApplePayLineItem, ApplePayConfig } from '../types/apple-pay';
|
|
3
|
+
interface ApplePayUniversalButtonProps {
|
|
4
|
+
checkoutSessionId: string;
|
|
5
|
+
lineItems: ApplePayLineItem[];
|
|
6
|
+
total: ApplePayLineItem;
|
|
7
|
+
config?: ApplePayConfig;
|
|
8
|
+
onSuccess?: (payment: any) => void;
|
|
9
|
+
onError?: (error: string) => void;
|
|
10
|
+
onCancel?: () => void;
|
|
11
|
+
className?: string;
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare const ApplePayUniversalButton: React.FC<ApplePayUniversalButtonProps>;
|
|
16
|
+
export default ApplePayUniversalButton;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ApplePayUniversalButton } from './ApplePayUniversalButton';
|
|
3
|
+
// Example usage of ApplePayUniversalButton
|
|
4
|
+
export const ApplePayUniversalButtonExample = () => {
|
|
5
|
+
const checkoutSessionId = 'example-session-123';
|
|
6
|
+
const lineItems = [
|
|
7
|
+
{
|
|
8
|
+
label: 'Product 1',
|
|
9
|
+
amount: '29.99',
|
|
10
|
+
type: 'final',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
label: 'Shipping',
|
|
14
|
+
amount: '5.99',
|
|
15
|
+
type: 'final',
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
const total = {
|
|
19
|
+
label: 'Total',
|
|
20
|
+
amount: '35.98',
|
|
21
|
+
type: 'final',
|
|
22
|
+
};
|
|
23
|
+
const config = {
|
|
24
|
+
countryCode: 'US',
|
|
25
|
+
supportedNetworks: ['visa', 'masterCard', 'amex', 'discover'],
|
|
26
|
+
merchantCapabilities: ['supports3DS'],
|
|
27
|
+
};
|
|
28
|
+
const handleSuccess = (payment) => {
|
|
29
|
+
console.log('Payment successful:', payment);
|
|
30
|
+
};
|
|
31
|
+
const handleError = (error) => {
|
|
32
|
+
console.error('Payment error:', error);
|
|
33
|
+
};
|
|
34
|
+
const handleCancel = () => {
|
|
35
|
+
console.log('Payment cancelled');
|
|
36
|
+
};
|
|
37
|
+
return (_jsxs("div", { className: "max-w-md mx-auto p-6 bg-white rounded-lg shadow-lg", children: [_jsx("h2", { className: "text-2xl font-bold mb-4", children: "Apple Pay Universal Button Example" }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "font-semibold mb-2", children: "Order Summary:" }), lineItems.map((item, index) => (_jsxs("div", { className: "flex justify-between text-sm", children: [_jsx("span", { children: item.label }), _jsxs("span", { children: ["$", item.amount] })] }, index))), _jsxs("div", { className: "flex justify-between font-bold border-t pt-2 mt-2", children: [_jsx("span", { children: total.label }), _jsxs("span", { children: ["$", total.amount] })] })] }), _jsx(ApplePayUniversalButton, { checkoutSessionId: checkoutSessionId, lineItems: lineItems, total: total, config: config, onSuccess: handleSuccess, onError: handleError, onCancel: handleCancel, className: "w-full" }), _jsxs("div", { className: "text-xs text-gray-500 mt-4", children: [_jsx("p", { children: "This button will:" }), _jsxs("ul", { className: "list-disc list-inside space-y-1", children: [_jsx("li", { children: "Show Apple Pay on supported devices (Safari iOS/macOS)" }), _jsx("li", { children: "Show QR code on other mobile browsers" }), _jsx("li", { children: "Show \"Unavailable\" on unsupported browsers" })] })] })] })] }));
|
|
38
|
+
};
|
|
39
|
+
export default ApplePayUniversalButtonExample;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useApplePay } from '../hooks/useApplePay';
|
|
4
|
+
export const ApplePayUniversalButton = ({ checkoutSessionId, lineItems, total, config = {}, onSuccess, onError, onCancel, className = '', children, disabled = false, }) => {
|
|
5
|
+
const [showQRCode, setShowQRCode] = useState(false);
|
|
6
|
+
const { handleApplePayClick, processingPayment, applePayError, isApplePayAvailable, shouldShowQRCode, qrCodeData, generateQRCode, } = useApplePay({
|
|
7
|
+
onSuccess,
|
|
8
|
+
onError,
|
|
9
|
+
onCancel,
|
|
10
|
+
config,
|
|
11
|
+
});
|
|
12
|
+
const handleClick = () => {
|
|
13
|
+
if (isApplePayAvailable) {
|
|
14
|
+
handleApplePayClick(checkoutSessionId, lineItems, total, config);
|
|
15
|
+
}
|
|
16
|
+
else if (shouldShowQRCode) {
|
|
17
|
+
setShowQRCode(true);
|
|
18
|
+
// Generate QR code data
|
|
19
|
+
const qrData = {
|
|
20
|
+
checkoutSessionId,
|
|
21
|
+
lineItems,
|
|
22
|
+
total,
|
|
23
|
+
config,
|
|
24
|
+
qrCodeUrl: generateQRCode({ checkoutSessionId, lineItems, total, config }),
|
|
25
|
+
};
|
|
26
|
+
handleApplePayClick(checkoutSessionId, lineItems, total, config);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
onError?.('Apple Pay is not available on this device');
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const getButtonText = () => {
|
|
33
|
+
if (processingPayment)
|
|
34
|
+
return 'Processing...';
|
|
35
|
+
if (isApplePayAvailable)
|
|
36
|
+
return 'Pay with Apple Pay';
|
|
37
|
+
if (shouldShowQRCode)
|
|
38
|
+
return 'Show QR Code';
|
|
39
|
+
return 'Apple Pay Unavailable';
|
|
40
|
+
};
|
|
41
|
+
const getButtonIcon = () => {
|
|
42
|
+
if (isApplePayAvailable) {
|
|
43
|
+
return (_jsx("svg", { className: "h-6 w-6", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" }) }));
|
|
44
|
+
}
|
|
45
|
+
if (shouldShowQRCode) {
|
|
46
|
+
return (_jsx("svg", { className: "h-6 w-6", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M3 3h7v7H3V3zm2 2v3h3V5H5zm8-2h7v7h-7V3zm2 2v3h3V5h-3zM3 13h7v7H3v-7zm2 2v3h3v-3H5zm8-2h7v7h-7v-7zm2 2v3h3v-3h-3z" }) }));
|
|
47
|
+
}
|
|
48
|
+
return (_jsx("svg", { className: "h-6 w-6", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" }) }));
|
|
49
|
+
};
|
|
50
|
+
const getButtonStyle = () => {
|
|
51
|
+
const baseStyle = "h-10 w-full text-base shadow-sm transition-colors duration-200 flex items-center justify-center gap-2";
|
|
52
|
+
if (isApplePayAvailable) {
|
|
53
|
+
return `${baseStyle} bg-black text-white hover:bg-black/80 ${className}`;
|
|
54
|
+
}
|
|
55
|
+
if (shouldShowQRCode) {
|
|
56
|
+
return `${baseStyle} bg-blue-600 text-white hover:bg-blue-700 ${className}`;
|
|
57
|
+
}
|
|
58
|
+
return `${baseStyle} bg-gray-400 text-gray-600 cursor-not-allowed ${className}`;
|
|
59
|
+
};
|
|
60
|
+
return (_jsxs("div", { className: "w-full", children: [_jsxs("button", { onClick: handleClick, disabled: disabled || processingPayment || (!isApplePayAvailable && !shouldShowQRCode), className: getButtonStyle(), style: { margin: 0 }, children: [getButtonIcon(), children || getButtonText()] }), showQRCode && qrCodeData && (_jsx("div", { className: "mt-4 p-4 bg-white border border-gray-200 rounded-lg shadow-sm", children: _jsxs("div", { className: "text-center", children: [_jsx("h3", { className: "text-lg font-medium text-gray-900 mb-2", children: "Scan QR Code to Pay" }), _jsx("p", { className: "text-sm text-gray-600 mb-4", children: "Use your mobile device to scan this QR code and complete your Apple Pay payment" }), _jsx("div", { className: "flex justify-center", children: _jsx("img", { src: qrCodeData.qrCodeUrl, alt: "Apple Pay QR Code", className: "w-48 h-48 border border-gray-200 rounded" }) }), _jsxs("div", { className: "mt-4 text-xs text-gray-500", children: [_jsxs("p", { children: ["Total: ", total.amount, " ", total.label] }), _jsxs("p", { children: ["Session: ", checkoutSessionId.slice(0, 8), "..."] })] }), _jsx("button", { onClick: () => setShowQRCode(false), className: "mt-3 px-4 py-2 text-sm text-gray-600 hover:text-gray-800 underline", children: "Close QR Code" })] }) })), applePayError && (_jsx("div", { className: "mt-2 p-2 bg-red-50 border border-red-200 rounded text-sm text-red-600", children: applePayError }))] }));
|
|
61
|
+
};
|
|
62
|
+
export default ApplePayUniversalButton;
|
|
@@ -5,11 +5,41 @@ 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 [qrCodeData, setQrCodeData] = useState(null);
|
|
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
|
-
//
|
|
13
|
+
// Utility function to convert Apple Pay contact to address
|
|
14
|
+
const appleContactToAddress = useCallback((contact) => {
|
|
15
|
+
return {
|
|
16
|
+
address1: contact?.addressLines?.[0] || '',
|
|
17
|
+
address2: contact?.addressLines?.[1] || '',
|
|
18
|
+
lastName: contact?.familyName || '',
|
|
19
|
+
firstName: contact?.givenName || '',
|
|
20
|
+
city: contact?.locality || '',
|
|
21
|
+
state: contact?.administrativeArea || '',
|
|
22
|
+
country: contact?.countryCode || '',
|
|
23
|
+
postal: contact?.postalCode || '',
|
|
24
|
+
phone: contact?.phoneNumber || '',
|
|
25
|
+
email: contact?.emailAddress || '',
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
// Generate QR code URL for Apple Pay fallback
|
|
29
|
+
const generateQRCode = useCallback((data) => {
|
|
30
|
+
const qrData = {
|
|
31
|
+
type: 'apple_pay_fallback',
|
|
32
|
+
checkoutSessionId: data.checkoutSessionId,
|
|
33
|
+
total: data.total,
|
|
34
|
+
lineItems: data.lineItems,
|
|
35
|
+
config: data.config,
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
};
|
|
38
|
+
// Encode data as base64 for QR code
|
|
39
|
+
const encodedData = btoa(JSON.stringify(qrData));
|
|
40
|
+
return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(encodedData)}`;
|
|
41
|
+
}, []);
|
|
42
|
+
// Check if Apple Pay is available with enhanced cross-browser support
|
|
13
43
|
const isApplePayAvailable = (() => {
|
|
14
44
|
if (typeof window === 'undefined')
|
|
15
45
|
return false;
|
|
@@ -35,6 +65,17 @@ export function useApplePay(options = {}) {
|
|
|
35
65
|
return false;
|
|
36
66
|
}
|
|
37
67
|
})();
|
|
68
|
+
// Check if we should show QR code fallback
|
|
69
|
+
const shouldShowQRCode = (() => {
|
|
70
|
+
if (typeof window === 'undefined')
|
|
71
|
+
return false;
|
|
72
|
+
// Show QR code if Apple Pay is not available but we're in a supported environment
|
|
73
|
+
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
|
74
|
+
const isSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
|
|
75
|
+
const isChrome = /Chrome/.test(navigator.userAgent);
|
|
76
|
+
// Show QR code for mobile devices or when Apple Pay is not natively supported
|
|
77
|
+
return !isApplePayAvailable && (isMobile || isSafari || isChrome);
|
|
78
|
+
})();
|
|
38
79
|
// Debug logging
|
|
39
80
|
console.log('Apple Pay availability check:', {
|
|
40
81
|
hasWindow: typeof window !== 'undefined',
|
|
@@ -94,6 +135,19 @@ export function useApplePay(options = {}) {
|
|
|
94
135
|
}, [apiKey]);
|
|
95
136
|
const handleApplePayClick = useCallback((checkoutSessionId, lineItems, total, config = {}) => {
|
|
96
137
|
if (!isApplePayAvailable) {
|
|
138
|
+
if (shouldShowQRCode) {
|
|
139
|
+
// Generate QR code data for fallback
|
|
140
|
+
const qrData = {
|
|
141
|
+
checkoutSessionId,
|
|
142
|
+
lineItems,
|
|
143
|
+
total,
|
|
144
|
+
config,
|
|
145
|
+
qrCodeUrl: generateQRCode({ checkoutSessionId, lineItems, total, config }),
|
|
146
|
+
};
|
|
147
|
+
setQrCodeData(qrData);
|
|
148
|
+
options.onError?.('Apple Pay not available - QR code generated');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
97
151
|
const errorMsg = 'Apple Pay is not available on this device';
|
|
98
152
|
setError(errorMsg);
|
|
99
153
|
options.onError?.(errorMsg);
|
|
@@ -106,6 +160,8 @@ export function useApplePay(options = {}) {
|
|
|
106
160
|
merchantCapabilities: config.merchantCapabilities || ['supports3DS'],
|
|
107
161
|
total,
|
|
108
162
|
lineItems,
|
|
163
|
+
requiredShippingContactFields: ['name', 'phone', 'email', 'postalAddress'],
|
|
164
|
+
requiredBillingContactFields: ['postalAddress'],
|
|
109
165
|
};
|
|
110
166
|
try {
|
|
111
167
|
const session = new window.ApplePaySession(3, request);
|
|
@@ -130,6 +186,15 @@ export function useApplePay(options = {}) {
|
|
|
130
186
|
try {
|
|
131
187
|
setProcessingPayment(true);
|
|
132
188
|
setError(null);
|
|
189
|
+
// Extract address information
|
|
190
|
+
const shippingContact = event.payment.shippingContact;
|
|
191
|
+
const billingContact = event.payment.billingContact;
|
|
192
|
+
const shippingAddress = shippingContact ? appleContactToAddress(shippingContact) : null;
|
|
193
|
+
const billingAddress = billingContact ? appleContactToAddress(billingContact) : null;
|
|
194
|
+
console.log('Apple Pay payment authorized with addresses:', {
|
|
195
|
+
shipping: shippingAddress,
|
|
196
|
+
billing: billingAddress,
|
|
197
|
+
});
|
|
133
198
|
// Tokenize the Apple Pay payment
|
|
134
199
|
const applePayToken = await tokenizeApplePay(event);
|
|
135
200
|
// Complete the Apple Pay session
|
|
@@ -188,5 +253,8 @@ export function useApplePay(options = {}) {
|
|
|
188
253
|
processingPayment,
|
|
189
254
|
applePayError: error,
|
|
190
255
|
isApplePayAvailable,
|
|
256
|
+
shouldShowQRCode,
|
|
257
|
+
qrCodeData,
|
|
258
|
+
generateQRCode,
|
|
191
259
|
};
|
|
192
260
|
}
|
|
@@ -182,12 +182,7 @@ export function useCheckout(options = {}) {
|
|
|
182
182
|
if (!currentCheckoutTokenRef.current) {
|
|
183
183
|
throw new Error('No checkout session to refresh');
|
|
184
184
|
}
|
|
185
|
-
console.log('🔄 [useCheckout] Refreshing checkout data...', {
|
|
186
|
-
checkoutToken: currentCheckoutTokenRef.current.substring(0, 8) + '...',
|
|
187
|
-
timestamp: new Date().toISOString(),
|
|
188
|
-
});
|
|
189
185
|
await getCheckout(currentCheckoutTokenRef.current);
|
|
190
|
-
console.log('✅ [useCheckout] Refresh completed, debug data will be updated automatically');
|
|
191
186
|
}, [getCheckout]);
|
|
192
187
|
// Register refresh function with coordinator and cleanup on unmount
|
|
193
188
|
useEffect(() => {
|
|
@@ -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
|
+
}
|
|
@@ -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
|
@@ -16,6 +16,8 @@ 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';
|
|
20
|
+
export { useVipOffers } from './hooks/useVipOffers';
|
|
19
21
|
export { useTagadaContext } from './providers/TagadaProvider';
|
|
20
22
|
export { clearPluginConfigCache, debugPluginConfig, getPluginConfig, useBasePath, usePluginConfig } from './hooks/usePluginConfig';
|
|
21
23
|
export type { PluginConfig } from './hooks/usePluginConfig';
|
|
@@ -33,9 +35,11 @@ export type { AuthState, Currency, Customer, Environment, EnvironmentConfig, Loc
|
|
|
33
35
|
export type { CheckoutData, CheckoutInitParams, CheckoutLineItem, CheckoutSession, CheckoutSessionPreview, Promotion, UseCheckoutOptions, UseCheckoutResult } from './hooks/useCheckout';
|
|
34
36
|
export type { Discount, DiscountCodeValidation, UseDiscountsOptions, UseDiscountsResult } from './hooks/useDiscounts';
|
|
35
37
|
export type { OrderBumpPreview, UseOrderBumpOptions, UseOrderBumpResult } from './hooks/useOrderBump';
|
|
38
|
+
export type { UseVipOffersOptions, UseVipOffersResult, VipOffer, VipPreviewResponse } from './hooks/useVipOffers';
|
|
36
39
|
export type { PostPurchaseOffer, PostPurchaseOfferItem, PostPurchaseOfferLineItem, PostPurchaseOfferSummary, UsePostPurchasesOptions, UsePostPurchasesResult } from './hooks/usePostPurchases';
|
|
37
40
|
export type { Payment, PaymentPollingHook, PollingOptions } from './hooks/usePaymentPolling';
|
|
38
41
|
export type { PaymentInstrument, ThreedsChallenge, ThreedsHook, ThreedsOptions, ThreedsProvider, ThreedsSession } from './hooks/useThreeds';
|
|
39
42
|
export type { ApplePayToken, CardPaymentMethod, PaymentHook, PaymentInstrumentResponse, PaymentOptions, PaymentResponse } from './hooks/usePayment';
|
|
40
|
-
export type { ApplePayConfig, ApplePayLineItem, ApplePayPaymentAuthorizedEvent, ApplePayPaymentRequest, ApplePayPaymentToken, ApplePayValidateMerchantEvent, BasisTheorySessionRequest, BasisTheoryTokenizeRequest, PayToken, UseApplePayOptions, UseApplePayResult } from './types/apple-pay';
|
|
43
|
+
export type { ApplePayConfig, ApplePayLineItem, ApplePayPaymentAuthorizedEvent, ApplePayPaymentRequest, ApplePayPaymentToken, ApplePayValidateMerchantEvent, BasisTheorySessionRequest, BasisTheoryTokenizeRequest, PayToken, UseApplePayOptions, UseApplePayResult, ApplePayAddress, ApplePayQRCodeData } from './types/apple-pay';
|
|
44
|
+
export { ApplePayUniversalButton } from './components/ApplePayUniversalButton';
|
|
41
45
|
export { convertCurrency, formatMoney, formatMoneyWithoutSymbol, formatSimpleMoney, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits } from './utils/money';
|
package/dist/react/index.js
CHANGED
|
@@ -19,6 +19,8 @@ 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';
|
|
23
|
+
export { useVipOffers } from './hooks/useVipOffers';
|
|
22
24
|
export { useTagadaContext } from './providers/TagadaProvider';
|
|
23
25
|
// Plugin configuration hooks
|
|
24
26
|
export { clearPluginConfigCache, debugPluginConfig, getPluginConfig, useBasePath, usePluginConfig } from './hooks/usePluginConfig';
|
|
@@ -33,7 +35,7 @@ export { useThreeds } from './hooks/useThreeds';
|
|
|
33
35
|
export { useThreedsModal } from './hooks/useThreedsModal';
|
|
34
36
|
// Apple Pay hooks exports
|
|
35
37
|
export { useApplePay } from './hooks/useApplePay';
|
|
36
|
-
// Component exports
|
|
37
|
-
|
|
38
|
+
// Component exports
|
|
39
|
+
export { ApplePayUniversalButton } from './components/ApplePayUniversalButton';
|
|
38
40
|
// Utility exports
|
|
39
41
|
export { convertCurrency, formatMoney, formatMoneyWithoutSymbol, formatSimpleMoney, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits } from './utils/money';
|
|
@@ -38,11 +38,11 @@ const InitializationLoader = () => (_jsxs("div", { style: {
|
|
|
38
38
|
borderTop: '1.5px solid #9ca3af',
|
|
39
39
|
borderRadius: '50%',
|
|
40
40
|
animation: 'tagada-spin 1s linear infinite',
|
|
41
|
-
} }), _jsx("span", { children: "Loading..." }), _jsx("style", { children: `
|
|
42
|
-
@keyframes tagada-spin {
|
|
43
|
-
0% { transform: rotate(0deg); }
|
|
44
|
-
100% { transform: rotate(360deg); }
|
|
45
|
-
}
|
|
41
|
+
} }), _jsx("span", { children: "Loading..." }), _jsx("style", { children: `
|
|
42
|
+
@keyframes tagada-spin {
|
|
43
|
+
0% { transform: rotate(0deg); }
|
|
44
|
+
100% { transform: rotate(360deg); }
|
|
45
|
+
}
|
|
46
46
|
` })] }));
|
|
47
47
|
const TagadaContext = createContext(null);
|
|
48
48
|
export function TagadaProvider({ children, environment, customApiConfig, debugMode, // Remove default, will be set based on environment
|