@tagadapay/plugin-sdk 3.0.15 → 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.
- package/README.md +1129 -1129
- package/build-cdn.js +113 -113
- package/dist/external-tracker.js +1 -1
- package/dist/external-tracker.min.js +1 -1
- package/dist/external-tracker.min.js.map +1 -1
- package/dist/react/providers/TagadaProvider.js +5 -5
- package/dist/v2/react/components/ApplePayButton.js +105 -33
- package/dist/v2/react/components/FunnelScriptInjector.d.ts +14 -0
- package/dist/v2/react/components/FunnelScriptInjector.js +242 -0
- package/dist/v2/react/providers/TagadaProvider.js +8 -67
- package/package.json +112 -112
|
@@ -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,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
|
|
61
|
-
const validateMerchant = useCallback(async (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
//
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!
|
|
114
|
-
throw new Error('Failed to
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|
|
@@ -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
|
+
}
|
|
@@ -4,11 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
* TagadaProvider - Main provider component for the Tagada Pay React SDK
|
|
5
5
|
*/
|
|
6
6
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
7
|
-
import { createContext, useCallback, useContext, useEffect, useMemo,
|
|
7
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
8
8
|
import { ApiService } from '../../../react/services/apiService';
|
|
9
9
|
import { convertCurrency, formatMoney, formatMoneyWithoutSymbol, formatSimpleMoney, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits, } from '../../../react/utils/money';
|
|
10
10
|
import { TagadaClient } from '../../core/client';
|
|
11
11
|
import { default as DebugDrawer } from '../components/DebugDrawer';
|
|
12
|
+
import { FunnelScriptInjector } from '../components/FunnelScriptInjector';
|
|
12
13
|
import { setGlobalApiClient } from '../hooks/useApiQuery';
|
|
13
14
|
// Professional, subtle loading component for initialization
|
|
14
15
|
const InitializationLoader = () => (_jsxs("div", { style: {
|
|
@@ -36,11 +37,11 @@ const InitializationLoader = () => (_jsxs("div", { style: {
|
|
|
36
37
|
borderTop: '1.5px solid #9ca3af',
|
|
37
38
|
borderRadius: '50%',
|
|
38
39
|
animation: 'tagada-spin 1s linear infinite',
|
|
39
|
-
} }), _jsx("span", { children: "Loading..." }), _jsx("style", { children: `
|
|
40
|
-
@keyframes tagada-spin {
|
|
41
|
-
0% { transform: rotate(0deg); }
|
|
42
|
-
100% { transform: rotate(360deg); }
|
|
43
|
-
}
|
|
40
|
+
} }), _jsx("span", { children: "Loading..." }), _jsx("style", { children: `
|
|
41
|
+
@keyframes tagada-spin {
|
|
42
|
+
0% { transform: rotate(0deg); }
|
|
43
|
+
100% { transform: rotate(360deg); }
|
|
44
|
+
}
|
|
44
45
|
` })] }));
|
|
45
46
|
const TagadaContext = createContext(null);
|
|
46
47
|
export function TagadaProvider({ children, environment, customApiConfig, debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, features, funnelId, autoInitializeFunnel = true, onNavigate, onFunnelError, debugScripts = [], }) {
|
|
@@ -211,8 +212,6 @@ export function TagadaProvider({ children, environment, customApiConfig, debugMo
|
|
|
211
212
|
formatSimpleMoney,
|
|
212
213
|
}), []);
|
|
213
214
|
const [isDebugDrawerOpen, setIsDebugDrawerOpen] = useState(false);
|
|
214
|
-
// Track last injected script to prevent duplicate execution
|
|
215
|
-
const lastInjectedScriptRef = useRef(null);
|
|
216
215
|
// Funnel Methods
|
|
217
216
|
const funnelMethods = useMemo(() => {
|
|
218
217
|
if (!client.funnel) {
|
|
@@ -285,63 +284,6 @@ export function TagadaProvider({ children, environment, customApiConfig, debugMo
|
|
|
285
284
|
},
|
|
286
285
|
};
|
|
287
286
|
}, [client, state.auth.session, state.store, funnelId, onNavigate]);
|
|
288
|
-
// Inject funnel script into the page
|
|
289
|
-
useEffect(() => {
|
|
290
|
-
// Only run in browser environment
|
|
291
|
-
if (typeof document === 'undefined') {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
const scriptContent = funnelState.context?.script;
|
|
295
|
-
const scriptId = 'tagada-funnel-script';
|
|
296
|
-
if (!scriptContent || !scriptContent.trim()) {
|
|
297
|
-
// Clear ref if script is removed
|
|
298
|
-
lastInjectedScriptRef.current = null;
|
|
299
|
-
// Remove existing script if it exists
|
|
300
|
-
const existingScript = document.getElementById(scriptId);
|
|
301
|
-
if (existingScript) {
|
|
302
|
-
existingScript.remove();
|
|
303
|
-
}
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
// Extract script content (remove <script> tags if present)
|
|
307
|
-
let scriptBody = scriptContent.trim();
|
|
308
|
-
// Check if script is wrapped in <script> tags
|
|
309
|
-
const scriptTagMatch = scriptBody.match(/^<script[^>]*>([\s\S]*)<\/script>$/i);
|
|
310
|
-
if (scriptTagMatch) {
|
|
311
|
-
scriptBody = scriptTagMatch[1].trim();
|
|
312
|
-
}
|
|
313
|
-
// Skip if script body is empty after extraction
|
|
314
|
-
if (!scriptBody) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
// Prevent duplicate injection of the same script content
|
|
318
|
-
// This handles React StrictMode double-execution in development
|
|
319
|
-
if (lastInjectedScriptRef.current === scriptBody) {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
// Remove existing script if it exists (for script updates)
|
|
323
|
-
const existingScript = document.getElementById(scriptId);
|
|
324
|
-
if (existingScript) {
|
|
325
|
-
existingScript.remove();
|
|
326
|
-
}
|
|
327
|
-
// Create and inject new script element
|
|
328
|
-
const scriptElement = document.createElement('script');
|
|
329
|
-
scriptElement.id = scriptId;
|
|
330
|
-
scriptElement.textContent = scriptBody;
|
|
331
|
-
document.body.appendChild(scriptElement);
|
|
332
|
-
// Track this script content to prevent re-injection (handles React StrictMode double-execution)
|
|
333
|
-
lastInjectedScriptRef.current = scriptBody;
|
|
334
|
-
// Cleanup: remove script element but keep ref to prevent re-injection on StrictMode second run
|
|
335
|
-
return () => {
|
|
336
|
-
const scriptToRemove = document.getElementById(scriptId);
|
|
337
|
-
if (scriptToRemove) {
|
|
338
|
-
scriptToRemove.remove();
|
|
339
|
-
}
|
|
340
|
-
// Note: We intentionally DON'T clear lastInjectedScriptRef here
|
|
341
|
-
// This prevents React StrictMode from re-injecting the same script on the second run
|
|
342
|
-
// The ref will be cleared when script content actually changes (next effect run)
|
|
343
|
-
};
|
|
344
|
-
}, [funnelState.context?.script]);
|
|
345
287
|
const contextValue = {
|
|
346
288
|
client,
|
|
347
289
|
...state,
|
|
@@ -360,7 +302,6 @@ export function TagadaProvider({ children, environment, customApiConfig, debugMo
|
|
|
360
302
|
refreshCoordinator,
|
|
361
303
|
money: moneyUtils,
|
|
362
304
|
};
|
|
363
|
-
console.log('contextValue', contextValue, contextValue.funnel.currentStep);
|
|
364
305
|
// Query Client
|
|
365
306
|
const [queryClient] = useState(() => new QueryClient({
|
|
366
307
|
defaultOptions: {
|
|
@@ -378,7 +319,7 @@ export function TagadaProvider({ children, environment, customApiConfig, debugMo
|
|
|
378
319
|
// Loading State Logic
|
|
379
320
|
const shouldShowLoading = state.isLoading || state.pluginConfigLoading || (blockUntilSessionReady && !state.isSessionInitialized);
|
|
380
321
|
const canRenderChildren = !state.pluginConfigLoading && (!blockUntilSessionReady || state.isSessionInitialized);
|
|
381
|
-
return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(TagadaContext.Provider, { value: contextValue, children: [shouldShowLoading && _jsx(InitializationLoader, {}), state.debugMode && canRenderChildren && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => setIsDebugDrawerOpen(true), style: {
|
|
322
|
+
return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(TagadaContext.Provider, { value: contextValue, children: [_jsx(FunnelScriptInjector, { ...funnelState }), shouldShowLoading && _jsx(InitializationLoader, {}), state.debugMode && canRenderChildren && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => setIsDebugDrawerOpen(true), style: {
|
|
382
323
|
position: 'fixed',
|
|
383
324
|
bottom: '16px',
|
|
384
325
|
right: '16px',
|