@tagadapay/plugin-sdk 3.0.14 → 3.1.0

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
@@ -58,6 +58,7 @@ export declare class TagadaClient {
58
58
  private tokenPromise;
59
59
  private tokenResolver;
60
60
  private boundHandleStorageChange;
61
+ private boundHandlePageshow;
61
62
  private readonly config;
62
63
  private instanceId;
63
64
  private isInitializingSession;
@@ -26,6 +26,29 @@ export class TagadaClient {
26
26
  this.config = config;
27
27
  this.instanceId = Math.random().toString(36).substr(2, 9);
28
28
  this.boundHandleStorageChange = this.handleStorageChange.bind(this);
29
+ this.boundHandlePageshow = (event) => {
30
+ if (event.persisted) {
31
+ if (this.state.debugMode) {
32
+ console.log(`[TagadaClient ${this.instanceId}] Page restored from BFcache (back button), re-initializing funnel...`);
33
+ }
34
+ // If we have an active session and store, we only need to re-initialize the funnel
35
+ // This ensures tracking is correct and the session is fresh on the backend
36
+ if (this.funnel && this.state.session && this.state.store) {
37
+ this.funnel.resetInitialization();
38
+ const accountId = this.getAccountId();
39
+ const urlParams = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
40
+ const funnelId = urlParams.get('funnelId') || undefined;
41
+ this.funnel.autoInitialize({ customerId: this.state.session.customerId, sessionId: this.state.session.sessionId }, { id: this.state.store.id, accountId }, funnelId).catch((err) => {
42
+ console.error('[TagadaClient] Funnel re-initialization failed:', err);
43
+ });
44
+ }
45
+ else {
46
+ // If state is missing, perform a full initialization
47
+ this.sessionInitRetryCount = 0;
48
+ this.initialize();
49
+ }
50
+ }
51
+ };
29
52
  console.log(`[TagadaClient ${this.instanceId}] Initializing...`);
30
53
  console.log(`[TagadaClient ${this.instanceId}] Config:`, {
31
54
  debugMode: config.debugMode,
@@ -115,6 +138,7 @@ export class TagadaClient {
115
138
  // Listen for storage changes (cross-tab sync)
116
139
  if (typeof window !== 'undefined') {
117
140
  window.addEventListener('storage', this.boundHandleStorageChange);
141
+ window.addEventListener('pageshow', this.boundHandlePageshow);
118
142
  }
119
143
  // Setup config hot-reload listener (for live config editing)
120
144
  this.setupConfigHotReload();
@@ -127,6 +151,7 @@ export class TagadaClient {
127
151
  destroy() {
128
152
  if (typeof window !== 'undefined') {
129
153
  window.removeEventListener('storage', this.boundHandleStorageChange);
154
+ window.removeEventListener('pageshow', this.boundHandlePageshow);
130
155
  }
131
156
  if (this.state.debugMode) {
132
157
  console.log(`[TagadaClient ${this.instanceId}] Destroyed`);
@@ -50,6 +50,15 @@ export declare class FunnelClient {
50
50
  * Get current state
51
51
  */
52
52
  getState(): FunnelState;
53
+ /**
54
+ * Get the session ID that would be used for initialization (URL params or cookie)
55
+ * This allows getting the session ID even before the client is fully initialized.
56
+ */
57
+ getDetectedSessionId(): string | null;
58
+ /**
59
+ * Reset initialization state (used for back-button restores)
60
+ */
61
+ resetInitialization(): void;
53
62
  /**
54
63
  * Initialize session with automatic detection (cookies, URL, etc.)
55
64
  */
@@ -92,6 +92,37 @@ export class FunnelClient {
92
92
  getState() {
93
93
  return this.state;
94
94
  }
95
+ /**
96
+ * Get the session ID that would be used for initialization (URL params or cookie)
97
+ * This allows getting the session ID even before the client is fully initialized.
98
+ */
99
+ getDetectedSessionId() {
100
+ // Priority 1: Already initialized session
101
+ if (this.state.context?.sessionId) {
102
+ return this.state.context.sessionId;
103
+ }
104
+ if (typeof window === 'undefined')
105
+ return null;
106
+ // Priority 2: URL params
107
+ const params = new URLSearchParams(window.location.search);
108
+ const urlSessionId = params.get('funnelSessionId');
109
+ if (urlSessionId)
110
+ return urlSessionId;
111
+ // Priority 3: Cookie
112
+ return getFunnelSessionCookie() || null;
113
+ }
114
+ /**
115
+ * Reset initialization state (used for back-button restores)
116
+ */
117
+ resetInitialization() {
118
+ this.initializationAttempted = false;
119
+ this.isInitializing = false;
120
+ // Clear context to force a fresh autoInitialize call to hit the backend
121
+ this.updateState({
122
+ context: null,
123
+ isInitialized: false,
124
+ });
125
+ }
95
126
  /**
96
127
  * Initialize session with automatic detection (cookies, URL, etc.)
97
128
  */
@@ -106,15 +137,12 @@ export class FunnelClient {
106
137
  this.isInitializing = true;
107
138
  this.updateState({ isLoading: true, error: null });
108
139
  try {
109
- // URL params
140
+ // 🎯 Get detected session ID
141
+ const existingSessionId = this.getDetectedSessionId();
142
+ // URL params for funnelId
110
143
  const params = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
111
144
  const urlFunnelId = params.get('funnelId');
112
145
  const effectiveFunnelId = urlFunnelId || funnelId;
113
- let existingSessionId = params.get('funnelSessionId');
114
- // Cookie fallback
115
- if (!existingSessionId) {
116
- existingSessionId = getFunnelSessionCookie() || null;
117
- }
118
146
  // 🎯 Read funnel tracking data from injected HTML
119
147
  const injectedFunnelId = getAssignedFunnelId(); // Funnel ID from server
120
148
  const funnelVariantId = getAssignedFunnelVariant(); // A/B test variant ID
@@ -209,6 +209,38 @@ export declare class CheckoutResource {
209
209
  customerId: string;
210
210
  status: 'processing';
211
211
  }>;
212
+ /**
213
+ * Preload checkout session (ultra-fast background pre-computation) ⚡⚡⚡
214
+ *
215
+ * This is the recommended way to handle cart changes or "Buy Now" intent.
216
+ * It pre-computes everything (checkoutToken, navigation URL, CMS session)
217
+ * before the user even clicks the checkout button.
218
+ *
219
+ * The SDK automatically gets funnelSessionId from FunnelClient if provided.
220
+ * Only FunnelClient knows how to properly extract funnelSessionId (from state, URL, cookies, etc.)
221
+ *
222
+ * @param params - Checkout and funnel parameters
223
+ * @param getFunnelSessionId - Optional function to get funnelSessionId from FunnelClient
224
+ * This maintains separation of concerns - only FunnelClient knows how to get it
225
+ *
226
+ * @returns { checkoutToken, customerId, navigationUrl }
227
+ */
228
+ preloadCheckout(params: CheckoutInitParams & {
229
+ funnelSessionId?: string;
230
+ currentUrl?: string;
231
+ funnelStepId?: string;
232
+ funnelVariantId?: string;
233
+ navigationEvent?: string | {
234
+ type: string;
235
+ data?: any;
236
+ };
237
+ navigationOptions?: any;
238
+ }, getFunnelSessionId?: () => string | null | undefined): Promise<{
239
+ checkoutToken: string;
240
+ customerId: string;
241
+ navigationUrl: string | null;
242
+ funnelStepId?: string;
243
+ }>;
212
244
  /**
213
245
  * Check async checkout processing status (instant, no waiting)
214
246
  * Perfect for polling or checking if background job completed
@@ -33,6 +33,44 @@ export class CheckoutResource {
33
33
  async initCheckoutAsync(params) {
34
34
  return this.apiClient.post('/api/v1/checkout/session/init-async', params);
35
35
  }
36
+ /**
37
+ * Preload checkout session (ultra-fast background pre-computation) ⚡⚡⚡
38
+ *
39
+ * This is the recommended way to handle cart changes or "Buy Now" intent.
40
+ * It pre-computes everything (checkoutToken, navigation URL, CMS session)
41
+ * before the user even clicks the checkout button.
42
+ *
43
+ * The SDK automatically gets funnelSessionId from FunnelClient if provided.
44
+ * Only FunnelClient knows how to properly extract funnelSessionId (from state, URL, cookies, etc.)
45
+ *
46
+ * @param params - Checkout and funnel parameters
47
+ * @param getFunnelSessionId - Optional function to get funnelSessionId from FunnelClient
48
+ * This maintains separation of concerns - only FunnelClient knows how to get it
49
+ *
50
+ * @returns { checkoutToken, customerId, navigationUrl }
51
+ */
52
+ async preloadCheckout(params, getFunnelSessionId) {
53
+ // ⚡ GET FUNNEL SESSION ID: Only FunnelClient knows how to properly get it
54
+ // Priority: explicit param > FunnelClient > backend fallback (via currentUrl)
55
+ let funnelSessionId = params.funnelSessionId;
56
+ if (!funnelSessionId && getFunnelSessionId) {
57
+ // Let FunnelClient handle extraction (from state, URL, cookies, etc.)
58
+ funnelSessionId = getFunnelSessionId() || undefined;
59
+ }
60
+ // Format navigationEvent if it's a string
61
+ const navigationEvent = typeof params.navigationEvent === 'string'
62
+ ? { type: params.navigationEvent }
63
+ : params.navigationEvent;
64
+ // Build request - backend will also try to extract from currentUrl if not provided
65
+ const requestParams = {
66
+ ...params,
67
+ ...(funnelSessionId && { funnelSessionId }),
68
+ ...(navigationEvent && { navigationEvent }),
69
+ // Ensure currentUrl is always set for backend extraction fallback
70
+ currentUrl: params.currentUrl || (typeof window !== 'undefined' ? window.location.href : undefined),
71
+ };
72
+ return this.apiClient.post('/api/v1/checkout/session/preload', requestParams);
73
+ }
36
74
  /**
37
75
  * Check async checkout processing status (instant, no waiting)
38
76
  * Perfect for polling or checking if background job completed
@@ -430,6 +430,7 @@ export interface SimpleFunnelContext<TCustom = {}> {
430
430
  * For backward compatibility and flexible unstructured data
431
431
  */
432
432
  metadata?: Record<string, any>;
433
+ script?: string;
433
434
  }
434
435
  export interface FunnelInitializeRequest {
435
436
  cmsSession: {
@@ -3,13 +3,37 @@ 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';
8
12
  export const ApplePayButton = ({ className = '', disabled = false, onSuccess, onError, onCancel, storeName, currencyCode = 'USD', variant = 'default', size = 'lg', checkout, }) => {
9
13
  const { applePayPaymentMethod, shippingMethods, lineItems, handleAddExpressId, updateCheckoutSessionValues, updateCustomerEmail, reComputeOrderSummary, setError: setContextError, } = useExpressPaymentMethods();
10
14
  const [processingPayment, setProcessingPayment] = useState(false);
11
15
  const [isApplePayAvailable, setIsApplePayAvailable] = useState(false);
12
16
  const [applePayError, setApplePayError] = useState(null);
17
+ // Get Basis Theory API key from config (auto-detects environment)
18
+ const basistheoryPublicKey = useMemo(() => getBasisTheoryApiKey(), []);
19
+ // Create SDK resource clients
20
+ const paymentsResource = useMemo(() => {
21
+ try {
22
+ return new PaymentsResource(getGlobalApiClient());
23
+ }
24
+ catch (error) {
25
+ throw new Error('Failed to initialize payments resource: ' +
26
+ (error instanceof Error ? error.message : 'Unknown error'));
27
+ }
28
+ }, []);
29
+ const ordersResource = useMemo(() => {
30
+ try {
31
+ return new OrdersResource(getGlobalApiClient());
32
+ }
33
+ catch (error) {
34
+ throw new Error('Failed to initialize orders resource: ' + (error instanceof Error ? error.message : 'Unknown error'));
35
+ }
36
+ }, []);
13
37
  // Don't render if no Apple Pay payment method is enabled
14
38
  if (!applePayPaymentMethod) {
15
39
  return null;
@@ -57,23 +81,55 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
57
81
  email: contact.emailAddress || '',
58
82
  };
59
83
  }, []);
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');
84
+ // Validate merchant with Basis Theory
85
+ const validateMerchant = useCallback(async () => {
86
+ try {
87
+ const response = await fetch('https://api.basistheory.com/apple-pay/session', {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ 'BT-API-KEY': basistheoryPublicKey,
92
+ },
93
+ body: JSON.stringify({
94
+ display_name: checkout.checkoutSession.store?.name || 'Store',
95
+ domain: window.location.host,
96
+ }),
97
+ });
98
+ if (!response.ok) {
99
+ throw new Error(`Failed to validate merchant: ${response.status}`);
100
+ }
101
+ const merchantSession = await response.json();
102
+ return merchantSession;
103
+ }
104
+ catch (error) {
105
+ console.error('Merchant validation failed:', error);
106
+ throw error;
74
107
  }
75
- return response.json();
76
- }, [checkout.checkoutSession.id]);
108
+ }, [checkout.checkoutSession.store?.name]);
109
+ // Tokenize Apple Pay payment with Basis Theory
110
+ const tokenizeApplePay = useCallback(async (token) => {
111
+ try {
112
+ const response = await fetch('https://api.basistheory.com/apple-pay', {
113
+ method: 'POST',
114
+ headers: {
115
+ 'Content-Type': 'application/json',
116
+ 'BT-API-KEY': basistheoryPublicKey,
117
+ },
118
+ body: JSON.stringify({
119
+ apple_payment_data: token,
120
+ }),
121
+ });
122
+ if (!response.ok) {
123
+ throw new Error(`Failed to tokenize Apple Pay: ${response.status}`);
124
+ }
125
+ const result = await response.json();
126
+ return result.apple_pay; // Basis Theory returns the Apple Pay token in the apple_pay field
127
+ }
128
+ catch (error) {
129
+ console.error('Tokenization failed:', error);
130
+ throw error;
131
+ }
132
+ }, []);
77
133
  // Process Apple Pay payment
78
134
  const processApplePayPayment = useCallback(async (token, billingContact, shippingContact) => {
79
135
  try {
@@ -97,23 +153,35 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
97
153
  },
98
154
  });
99
155
  }
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',
156
+ // 1. Tokenize with Basis Theory
157
+ const applePayToken = await tokenizeApplePay(token);
158
+ // 2. Create payment instrument from the tokenized Apple Pay payment
159
+ const paymentInstrumentData = {
160
+ type: 'apple_pay',
161
+ token: applePayToken.id,
162
+ dpanType: applePayToken.type,
163
+ card: {
164
+ bin: applePayToken.card.bin,
165
+ last4: applePayToken.card.last4,
166
+ expirationMonth: applePayToken.card.expiration_month,
167
+ expirationYear: applePayToken.card.expiration_year,
168
+ brand: applePayToken.card.brand,
105
169
  },
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');
170
+ };
171
+ const paymentInstrument = await paymentsResource.createPaymentInstrument(paymentInstrumentData);
172
+ if (!paymentInstrument?.id) {
173
+ throw new Error('Failed to create payment instrument');
174
+ }
175
+ // 3. Create order from checkout session
176
+ const orderResponse = await ordersResource.createOrder(checkout.checkoutSession.id);
177
+ if (!orderResponse?.success || !orderResponse?.order?.id) {
178
+ throw new Error('Failed to create order');
115
179
  }
116
- const paymentResult = await response.json();
180
+ // 4. Process payment
181
+ const paymentResult = await paymentsResource.processPaymentDirect(checkout.checkoutSession.id, paymentInstrument.id, undefined, {
182
+ initiatedBy: 'customer',
183
+ source: 'checkout',
184
+ });
117
185
  if (onSuccess) {
118
186
  onSuccess(paymentResult);
119
187
  }
@@ -137,6 +205,9 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
137
205
  applePayContactToAddress,
138
206
  updateCheckoutSessionValues,
139
207
  updateCustomerEmail,
208
+ tokenizeApplePay,
209
+ paymentsResource,
210
+ ordersResource,
140
211
  onSuccess,
141
212
  onError,
142
213
  setContextError,
@@ -174,7 +245,8 @@ export const ApplePayButton = ({ className = '', disabled = false, onSuccess, on
174
245
  const session = new window.ApplePaySession(3, paymentRequest);
175
246
  session.onvalidatemerchant = async (event) => {
176
247
  try {
177
- const merchantSession = await validateMerchant(event.validationURL);
248
+ console.log('Merchant validation requested for:', event.validationURL);
249
+ const merchantSession = await validateMerchant();
178
250
  session.completeMerchantValidation(merchantSession);
179
251
  }
180
252
  catch (error) {
@@ -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 {};