@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.
@@ -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,3 @@
1
+ import React from 'react';
2
+ export declare const ApplePayUniversalButtonExample: React.FC;
3
+ export default ApplePayUniversalButtonExample;
@@ -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
- // Check if Apple Pay is available
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
+ }
@@ -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';
@@ -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 (if any)
37
- // export { SomeComponent } from './components/SomeComponent';
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