@tagadapay/plugin-sdk 3.0.15 → 3.1.1

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.
@@ -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
@@ -3,13 +3,38 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  * Apple Pay Button Component for v2 Architecture
4
4
  * Uses v2 useExpressPaymentMethods hook and follows clean architecture principles
5
5
  */
6
- import { useCallback, useEffect, useState } from 'react';
6
+ import { useCallback, useEffect, useState, useMemo } from 'react';
7
+ import { PaymentsResource } from '../../core/resources/payments';
8
+ import { OrdersResource } from '../../core/resources/orders';
7
9
  import { useExpressPaymentMethods } from '../hooks/useExpressPaymentMethods';
10
+ import { getGlobalApiClient } from '../hooks/useApiQuery';
11
+ import { getBasisTheoryApiKey } from '../../../react/config/payment';
12
+ import { minorUnitsToMajorUnits, getCurrencyInfo } from '../../../react/utils/money';
8
13
  export const ApplePayButton = ({ className = '', disabled = false, onSuccess, onError, onCancel, storeName, currencyCode = 'USD', variant = 'default', size = 'lg', checkout, }) => {
9
14
  const { applePayPaymentMethod, shippingMethods, lineItems, handleAddExpressId, updateCheckoutSessionValues, updateCustomerEmail, reComputeOrderSummary, setError: setContextError, } = useExpressPaymentMethods();
10
15
  const [processingPayment, setProcessingPayment] = useState(false);
11
16
  const [isApplePayAvailable, setIsApplePayAvailable] = useState(false);
12
17
  const [applePayError, setApplePayError] = useState(null);
18
+ // Get Basis Theory API key from config (auto-detects environment)
19
+ const basistheoryPublicKey = useMemo(() => getBasisTheoryApiKey(), []);
20
+ // Create SDK resource clients
21
+ const paymentsResource = useMemo(() => {
22
+ try {
23
+ return new PaymentsResource(getGlobalApiClient());
24
+ }
25
+ catch (error) {
26
+ throw new Error('Failed to initialize payments resource: ' +
27
+ (error instanceof Error ? error.message : 'Unknown error'));
28
+ }
29
+ }, []);
30
+ const ordersResource = useMemo(() => {
31
+ try {
32
+ return new OrdersResource(getGlobalApiClient());
33
+ }
34
+ catch (error) {
35
+ throw new Error('Failed to initialize orders resource: ' + (error instanceof Error ? error.message : 'Unknown error'));
36
+ }
37
+ }, []);
13
38
  // Don't render if no Apple Pay payment method is enabled
14
39
  if (!applePayPaymentMethod) {
15
40
  return null;
@@ -42,6 +67,20 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
42
67
  };
43
68
  checkApplePayAvailability();
44
69
  }, [applePayPaymentMethod, handleAddExpressId]);
70
+ // Helper to convert minor units to currency string for Apple Pay
71
+ // Uses SDK's minorUnitsToMajorUnits which properly handles different currency decimal places
72
+ const minorUnitsToCurrencyString = useCallback((amountMinor, currency) => {
73
+ if (!amountMinor || !currency)
74
+ return '0.00';
75
+ const currencyInfo = getCurrencyInfo(currency);
76
+ if (!currencyInfo) {
77
+ // Fallback to simple division if currency info not found
78
+ console.warn(`Currency info not found for ${currency}, using fallback`);
79
+ return (amountMinor / 100).toFixed(2);
80
+ }
81
+ const majorUnits = minorUnitsToMajorUnits(amountMinor, currency);
82
+ return majorUnits.toFixed(currencyInfo.ISOdigits);
83
+ }, []);
45
84
  // Convert Apple Pay contact to internal Address format
46
85
  const applePayContactToAddress = useCallback((contact) => {
47
86
  return {
@@ -57,23 +96,55 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
57
96
  email: contact.emailAddress || '',
58
97
  };
59
98
  }, []);
60
- // Validate merchant with backend
61
- const validateMerchant = useCallback(async (validationURL) => {
62
- const response = await fetch('/api/v1/apple-pay/validate-merchant', {
63
- method: 'POST',
64
- headers: {
65
- 'Content-Type': 'application/json',
66
- },
67
- body: JSON.stringify({
68
- validationURL,
69
- checkoutSessionId: checkout.checkoutSession.id,
70
- }),
71
- });
72
- if (!response.ok) {
73
- throw new Error('Failed to validate Apple Pay merchant');
99
+ // Validate merchant with Basis Theory
100
+ const validateMerchant = useCallback(async () => {
101
+ try {
102
+ const response = await fetch('https://api.basistheory.com/apple-pay/session', {
103
+ method: 'POST',
104
+ headers: {
105
+ 'Content-Type': 'application/json',
106
+ 'BT-API-KEY': basistheoryPublicKey,
107
+ },
108
+ body: JSON.stringify({
109
+ display_name: checkout.checkoutSession.store?.name || 'Store',
110
+ domain: window.location.host,
111
+ }),
112
+ });
113
+ if (!response.ok) {
114
+ throw new Error(`Failed to validate merchant: ${response.status}`);
115
+ }
116
+ const merchantSession = await response.json();
117
+ return merchantSession;
118
+ }
119
+ catch (error) {
120
+ console.error('Merchant validation failed:', error);
121
+ throw error;
74
122
  }
75
- return response.json();
76
- }, [checkout.checkoutSession.id]);
123
+ }, [checkout.checkoutSession.store?.name]);
124
+ // Tokenize Apple Pay payment with Basis Theory
125
+ const tokenizeApplePay = useCallback(async (token) => {
126
+ try {
127
+ const response = await fetch('https://api.basistheory.com/apple-pay', {
128
+ method: 'POST',
129
+ headers: {
130
+ 'Content-Type': 'application/json',
131
+ 'BT-API-KEY': basistheoryPublicKey,
132
+ },
133
+ body: JSON.stringify({
134
+ apple_payment_data: token,
135
+ }),
136
+ });
137
+ if (!response.ok) {
138
+ throw new Error(`Failed to tokenize Apple Pay: ${response.status}`);
139
+ }
140
+ const result = await response.json();
141
+ return result.apple_pay; // Basis Theory returns the Apple Pay token in the apple_pay field
142
+ }
143
+ catch (error) {
144
+ console.error('Tokenization failed:', error);
145
+ throw error;
146
+ }
147
+ }, []);
77
148
  // Process Apple Pay payment
78
149
  const processApplePayPayment = useCallback(async (token, billingContact, shippingContact) => {
79
150
  try {
@@ -97,23 +168,35 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
97
168
  },
98
169
  });
99
170
  }
100
- // Process payment with backend API
101
- const response = await fetch('/api/v1/apple-pay/process-payment', {
102
- method: 'POST',
103
- headers: {
104
- 'Content-Type': 'application/json',
171
+ // 1. Tokenize with Basis Theory
172
+ const applePayToken = await tokenizeApplePay(token);
173
+ // 2. Create payment instrument from the tokenized Apple Pay payment
174
+ const paymentInstrumentData = {
175
+ type: 'apple_pay',
176
+ token: applePayToken.id,
177
+ dpanType: applePayToken.type,
178
+ card: {
179
+ bin: applePayToken.card.bin,
180
+ last4: applePayToken.card.last4,
181
+ expirationMonth: applePayToken.card.expiration_month,
182
+ expirationYear: applePayToken.card.expiration_year,
183
+ brand: applePayToken.card.brand,
105
184
  },
106
- body: JSON.stringify({
107
- checkoutSessionId: checkout.checkoutSession.id,
108
- paymentToken: token,
109
- billingContact,
110
- shippingContact,
111
- }),
112
- });
113
- if (!response.ok) {
114
- throw new Error('Failed to process Apple Pay payment');
185
+ };
186
+ const paymentInstrument = await paymentsResource.createPaymentInstrument(paymentInstrumentData);
187
+ if (!paymentInstrument?.id) {
188
+ throw new Error('Failed to create payment instrument');
189
+ }
190
+ // 3. Create order from checkout session
191
+ const orderResponse = await ordersResource.createOrder(checkout.checkoutSession.id);
192
+ if (!orderResponse?.success || !orderResponse?.order?.id) {
193
+ throw new Error('Failed to create order');
115
194
  }
116
- const paymentResult = await response.json();
195
+ // 4. Process payment
196
+ const paymentResult = await paymentsResource.processPaymentDirect(checkout.checkoutSession.id, paymentInstrument.id, undefined, {
197
+ initiatedBy: 'customer',
198
+ source: 'checkout',
199
+ });
117
200
  if (onSuccess) {
118
201
  onSuccess(paymentResult);
119
202
  }
@@ -137,6 +220,9 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
137
220
  applePayContactToAddress,
138
221
  updateCheckoutSessionValues,
139
222
  updateCustomerEmail,
223
+ tokenizeApplePay,
224
+ paymentsResource,
225
+ ordersResource,
140
226
  onSuccess,
141
227
  onError,
142
228
  setContextError,
@@ -147,13 +233,13 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
147
233
  return;
148
234
  }
149
235
  const paymentRequest = {
150
- countryCode: 'US', // TODO: Get from checkout session
151
- currencyCode: currencyCode,
236
+ countryCode: applePayPaymentMethod?.metadata?.country || 'US',
237
+ currencyCode: checkout.summary.currency,
152
238
  supportedNetworks: ['visa', 'masterCard', 'amex', 'discover'],
153
239
  merchantCapabilities: ['supports3DS'],
154
240
  total: {
155
241
  label: storeName || checkout.checkoutSession.store?.name || 'Total',
156
- amount: (checkout.summary.totalAdjustedAmount / 100).toFixed(2),
242
+ amount: minorUnitsToCurrencyString(checkout.summary.totalAdjustedAmount, checkout.summary.currency),
157
243
  type: 'final',
158
244
  },
159
245
  lineItems: lineItems.map((item) => ({
@@ -174,7 +260,8 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
174
260
  const session = new window.ApplePaySession(3, paymentRequest);
175
261
  session.onvalidatemerchant = async (event) => {
176
262
  try {
177
- const merchantSession = await validateMerchant(event.validationURL);
263
+ console.log('Merchant validation requested for:', event.validationURL);
264
+ const merchantSession = await validateMerchant();
178
265
  session.completeMerchantValidation(merchantSession);
179
266
  }
180
267
  catch (error) {
@@ -287,7 +374,6 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
287
374
  isApplePayAvailable,
288
375
  processingPayment,
289
376
  checkout,
290
- currencyCode,
291
377
  storeName,
292
378
  lineItems,
293
379
  shippingMethods,
@@ -299,6 +385,8 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
299
385
  onCancel,
300
386
  onError,
301
387
  setContextError,
388
+ minorUnitsToCurrencyString,
389
+ applePayPaymentMethod,
302
390
  ]);
303
391
  // Button size classes
304
392
  const sizeClasses = {
@@ -0,0 +1,14 @@
1
+ import { FunnelState } from '../../core';
2
+ interface FunnelScriptInjectorProps extends FunnelState {
3
+ }
4
+ /**
5
+ * FunnelScriptInjector - Handles injection of funnel scripts into the page.
6
+ *
7
+ * This component:
8
+ * - Sets up Tagada on the window object
9
+ * - Injects and manages funnel scripts from the context
10
+ * - Prevents duplicate script injection (handles React StrictMode)
11
+ * - Cleans up scripts when context changes or component unmounts
12
+ */
13
+ export declare function FunnelScriptInjector({ context, isInitialized }: FunnelScriptInjectorProps): null;
14
+ export {};
@@ -0,0 +1,242 @@
1
+ 'use client';
2
+ import { useEffect, useRef } from 'react';
3
+ /**
4
+ * FunnelScriptInjector - Handles injection of funnel scripts into the page.
5
+ *
6
+ * This component:
7
+ * - Sets up Tagada on the window object
8
+ * - Injects and manages funnel scripts from the context
9
+ * - Prevents duplicate script injection (handles React StrictMode)
10
+ * - Cleans up scripts when context changes or component unmounts
11
+ */
12
+ export function FunnelScriptInjector({ context, isInitialized }) {
13
+ console.log('FunnelScriptInjector here', context, isInitialized); // Track last injected script to prevent duplicate execution
14
+ const lastInjectedScriptRef = useRef(null);
15
+ useEffect(() => {
16
+ // Only run in browser environment
17
+ if (typeof document === 'undefined') {
18
+ return;
19
+ }
20
+ // Set up Tagada for funnel scripts (similar to HtmlScript.tsx)
21
+ const setupTagada = () => {
22
+ // @ts-expect-error - Adding utilities to window
23
+ if (window.Tagada) {
24
+ // Update pageType if context is available
25
+ if (context?.currentStepId) {
26
+ // @ts-expect-error - Updating window property
27
+ window.Tagada.pageType = context.currentStepId;
28
+ }
29
+ // Update isInitialized
30
+ // @ts-expect-error - Updating window property
31
+ window.Tagada.isInitialized = isInitialized;
32
+ // Update ressources
33
+ // @ts-expect-error - Updating window property
34
+ window.Tagada.ressources = context?.resources || null;
35
+ return; // Utils already exist, just update properties
36
+ }
37
+ // @ts-expect-error - Adding utilities to window
38
+ window.Tagada = {
39
+ // Wait for DOM to be ready
40
+ ready: (callback) => {
41
+ if (document.readyState === 'loading') {
42
+ document.addEventListener('DOMContentLoaded', callback);
43
+ }
44
+ else {
45
+ callback();
46
+ }
47
+ },
48
+ // Wait for window to be fully loaded AND funnel to be initialized
49
+ loaded: (callback) => {
50
+ const checkBothConditions = () => {
51
+ const pageLoaded = document.readyState === 'complete';
52
+ // @ts-expect-error - Accessing window property
53
+ const funnelInitialized = window.Tagada?.isInitialized === true;
54
+ if (pageLoaded && funnelInitialized) {
55
+ callback();
56
+ return true;
57
+ }
58
+ return false;
59
+ };
60
+ // Check immediately
61
+ if (checkBothConditions()) {
62
+ return;
63
+ }
64
+ // Set up listeners for both conditions
65
+ let loadListener = null;
66
+ let initCheckInterval = null;
67
+ let hasCalled = false;
68
+ const cleanup = () => {
69
+ if (loadListener) {
70
+ window.removeEventListener('load', loadListener);
71
+ loadListener = null;
72
+ }
73
+ if (initCheckInterval) {
74
+ clearInterval(initCheckInterval);
75
+ initCheckInterval = null;
76
+ }
77
+ };
78
+ // Listen for page load
79
+ loadListener = () => {
80
+ if (checkBothConditions() && !hasCalled) {
81
+ hasCalled = true;
82
+ cleanup();
83
+ }
84
+ };
85
+ window.addEventListener('load', loadListener);
86
+ // Poll for initialization status (in case page loads before initialization)
87
+ initCheckInterval = setInterval(() => {
88
+ if (checkBothConditions() && !hasCalled) {
89
+ hasCalled = true;
90
+ cleanup();
91
+ }
92
+ }, 100);
93
+ // Timeout fallback (10 seconds max wait)
94
+ setTimeout(() => {
95
+ if (!hasCalled) {
96
+ hasCalled = true;
97
+ cleanup();
98
+ // Call anyway if page is loaded (graceful degradation)
99
+ if (document.readyState === 'complete') {
100
+ callback();
101
+ }
102
+ }
103
+ }, 10000);
104
+ },
105
+ // Execute with delay
106
+ delay: (callback, ms = 1000) => {
107
+ setTimeout(callback, ms);
108
+ },
109
+ // Retry until condition is met
110
+ retry: (condition, callback, maxAttempts = 10, interval = 500) => {
111
+ let attempts = 0;
112
+ const check = () => {
113
+ attempts++;
114
+ if (condition() || attempts >= maxAttempts) {
115
+ callback();
116
+ }
117
+ else {
118
+ setTimeout(check, interval);
119
+ }
120
+ };
121
+ check();
122
+ },
123
+ // Wait for element to exist
124
+ waitForElement: (selector, callback, timeout = 10000) => {
125
+ const element = document.querySelector(selector);
126
+ if (element) {
127
+ callback(element);
128
+ return;
129
+ }
130
+ const observer = new MutationObserver(() => {
131
+ const element = document.querySelector(selector);
132
+ if (element) {
133
+ observer.disconnect();
134
+ callback(element);
135
+ }
136
+ });
137
+ observer.observe(document.body, {
138
+ childList: true,
139
+ subtree: true,
140
+ });
141
+ // Timeout fallback
142
+ setTimeout(() => {
143
+ observer.disconnect();
144
+ }, timeout);
145
+ },
146
+ // Page type helper (current step ID)
147
+ pageType: context?.currentStepId || null,
148
+ // Funnel initialization status
149
+ isInitialized: isInitialized,
150
+ // Expose resources directly (convenience access)
151
+ ressources: context?.resources || null,
152
+ // Expose funnel context data
153
+ funnel: context
154
+ ? {
155
+ sessionId: context.sessionId,
156
+ funnelId: context.funnelId,
157
+ currentStepId: context.currentStepId,
158
+ previousStepId: context.previousStepId,
159
+ ressources: context.resources,
160
+ }
161
+ : null,
162
+ };
163
+ };
164
+ // Set up utilities before injecting script
165
+ setupTagada();
166
+ const scriptContent = context?.script;
167
+ const scriptId = 'tagada-funnel-script';
168
+ if (!scriptContent || !scriptContent.trim()) {
169
+ // Clear ref if script is removed
170
+ lastInjectedScriptRef.current = null;
171
+ // Remove existing script if it exists
172
+ const existingScript = document.getElementById(scriptId);
173
+ if (existingScript) {
174
+ existingScript.remove();
175
+ }
176
+ return;
177
+ }
178
+ // Extract script content (remove <script> tags if present)
179
+ let scriptBody = scriptContent.trim();
180
+ // Check if script is wrapped in <script> tags
181
+ const scriptTagMatch = scriptBody.match(/^<script[^>]*>([\s\S]*)<\/script>$/i);
182
+ if (scriptTagMatch) {
183
+ scriptBody = scriptTagMatch[1].trim();
184
+ }
185
+ // Skip if script body is empty after extraction
186
+ if (!scriptBody) {
187
+ return;
188
+ }
189
+ // Prevent duplicate injection of the same script content
190
+ // This handles React StrictMode double-execution in development
191
+ if (lastInjectedScriptRef.current === scriptBody) {
192
+ return;
193
+ }
194
+ // Remove existing script if it exists (for script updates)
195
+ const existingScript = document.getElementById(scriptId);
196
+ if (existingScript) {
197
+ existingScript.remove();
198
+ }
199
+ // Wrap script content with error handling and context checks
200
+ const wrappedScript = `
201
+ (function() {
202
+ try {
203
+ // Check if we have basic DOM access
204
+ if (typeof document === 'undefined') {
205
+ console.error('[TagadaPay] Document not available');
206
+ return;
207
+ }
208
+
209
+ // Check if we have Tagada
210
+ if (!window.Tagada) {
211
+ console.error('[TagadaPay] Tagada not available');
212
+ return;
213
+ }
214
+
215
+ // Execute the original script
216
+ ${scriptBody}
217
+ } catch (error) {
218
+ console.error('[TagadaPay] Script execution error:', error);
219
+ }
220
+ })();
221
+ `;
222
+ // Create and inject new script element
223
+ const scriptElement = document.createElement('script');
224
+ scriptElement.id = scriptId;
225
+ scriptElement.textContent = wrappedScript;
226
+ document.body.appendChild(scriptElement);
227
+ // Track this script content to prevent re-injection (handles React StrictMode double-execution)
228
+ lastInjectedScriptRef.current = scriptBody;
229
+ // Cleanup: remove script element but keep ref to prevent re-injection on StrictMode second run
230
+ return () => {
231
+ const scriptToRemove = document.getElementById(scriptId);
232
+ if (scriptToRemove) {
233
+ scriptToRemove.remove();
234
+ }
235
+ // Note: We intentionally DON'T clear lastInjectedScriptRef here
236
+ // This prevents React StrictMode from re-injecting the same script on the second run
237
+ // The ref will be cleared when script content actually changes (next effect run)
238
+ };
239
+ }, [context?.script, context?.currentStepId, isInitialized]);
240
+ // This component doesn't render anything
241
+ return null;
242
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Apple Pay Hook for v2 Architecture
3
+ * Handles Apple Pay payment flow with Basis Theory integration
4
+ */
5
+ export interface UseApplePayOptions {
6
+ onSuccess?: (result: any) => void;
7
+ onError?: (error: string) => void;
8
+ onCancel?: () => void;
9
+ }
10
+ export interface UseApplePayResult {
11
+ handleApplePayClick: () => void;
12
+ processingPayment: boolean;
13
+ applePayError: string | null;
14
+ isAvailable: boolean;
15
+ }
16
+ export declare function useApplePay(options?: UseApplePayOptions): UseApplePayResult;