@tagadapay/plugin-sdk 3.1.2 → 3.1.8
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 +1104 -491
- package/dist/external-tracker.min.js +2 -2
- package/dist/external-tracker.min.js.map +4 -4
- package/dist/react/hooks/useApplePay.js +25 -36
- package/dist/react/hooks/usePaymentPolling.d.ts +9 -3
- package/dist/react/providers/TagadaProvider.js +5 -5
- package/dist/react/utils/money.d.ts +4 -3
- package/dist/react/utils/money.js +39 -6
- package/dist/react/utils/trackingUtils.js +1 -0
- package/dist/v2/core/client.js +34 -2
- package/dist/v2/core/config/environment.js +9 -2
- package/dist/v2/core/funnelClient.d.ts +92 -1
- package/dist/v2/core/funnelClient.js +247 -3
- package/dist/v2/core/resources/apiClient.js +1 -1
- package/dist/v2/core/resources/checkout.d.ts +68 -0
- package/dist/v2/core/resources/funnel.d.ts +15 -0
- package/dist/v2/core/resources/payments.d.ts +50 -3
- package/dist/v2/core/resources/payments.js +38 -7
- package/dist/v2/core/utils/pluginConfig.js +40 -5
- package/dist/v2/core/utils/previewMode.d.ts +3 -0
- package/dist/v2/core/utils/previewMode.js +44 -14
- package/dist/v2/core/utils/previewModeIndicator.d.ts +19 -0
- package/dist/v2/core/utils/previewModeIndicator.js +414 -0
- package/dist/v2/core/utils/tokenStorage.d.ts +4 -0
- package/dist/v2/core/utils/tokenStorage.js +15 -1
- package/dist/v2/index.d.ts +6 -1
- package/dist/v2/index.js +6 -1
- package/dist/v2/react/components/ApplePayButton.d.ts +21 -121
- package/dist/v2/react/components/ApplePayButton.js +221 -290
- package/dist/v2/react/components/FunnelScriptInjector.d.ts +3 -1
- package/dist/v2/react/components/FunnelScriptInjector.js +128 -24
- package/dist/v2/react/components/PreviewModeIndicator.d.ts +46 -0
- package/dist/v2/react/components/PreviewModeIndicator.js +113 -0
- package/dist/v2/react/hooks/useApplePayCheckout.d.ts +16 -0
- package/dist/v2/react/hooks/useApplePayCheckout.js +193 -0
- package/dist/v2/react/hooks/useFunnel.d.ts +42 -6
- package/dist/v2/react/hooks/useFunnel.js +25 -5
- package/dist/v2/react/hooks/usePaymentPolling.d.ts +9 -3
- package/dist/v2/react/hooks/usePaymentPolling.js +31 -9
- package/dist/v2/react/hooks/usePaymentQuery.d.ts +32 -2
- package/dist/v2/react/hooks/usePaymentQuery.js +304 -7
- package/dist/v2/react/hooks/usePaymentRetrieve.d.ts +26 -0
- package/dist/v2/react/hooks/usePaymentRetrieve.js +175 -0
- package/dist/v2/react/hooks/useStepConfig.d.ts +62 -0
- package/dist/v2/react/hooks/useStepConfig.js +52 -0
- package/dist/v2/react/index.d.ts +9 -3
- package/dist/v2/react/index.js +5 -1
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +27 -19
- package/dist/v2/react/providers/TagadaProvider.js +7 -7
- package/dist/v2/standalone/external-tracker.d.ts +2 -0
- package/dist/v2/standalone/external-tracker.js +6 -3
- package/package.json +112 -112
- package/dist/v2/react/hooks/useApplePay.d.ts +0 -16
- package/dist/v2/react/hooks/useApplePay.js +0 -247
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { getAssignedStepConfig } from '../../core';
|
|
3
4
|
/**
|
|
4
5
|
* FunnelScriptInjector - Handles injection of funnel scripts into the page.
|
|
5
6
|
*
|
|
6
7
|
* This component:
|
|
7
8
|
* - Sets up Tagada on the window object
|
|
8
|
-
* - Injects
|
|
9
|
+
* - Injects scripts from stepConfig.scripts (NEW: from HTML injection, supports A/B variants)
|
|
10
|
+
* - Falls back to context.script (LEGACY: from funnel context)
|
|
11
|
+
* - Handles script positions: head-start, head-end, body-start, body-end
|
|
9
12
|
* - Prevents duplicate script injection (handles React StrictMode)
|
|
10
13
|
* - Cleans up scripts when context changes or component unmounts
|
|
11
14
|
*/
|
|
12
15
|
export function FunnelScriptInjector({ context, isInitialized }) {
|
|
13
|
-
|
|
16
|
+
// Track last injected scripts to prevent duplicate execution
|
|
14
17
|
const lastInjectedScriptRef = useRef(null);
|
|
18
|
+
const lastInjectedStepConfigScriptsRef = useRef(null);
|
|
19
|
+
// Get stepConfig scripts from HTML injection or local config
|
|
20
|
+
// Re-compute when initialized (local config loads async, so we need to re-check)
|
|
21
|
+
const stepConfigScripts = useMemo(() => {
|
|
22
|
+
const stepConfig = getAssignedStepConfig();
|
|
23
|
+
const scripts = stepConfig?.scripts?.filter(s => s.enabled) || [];
|
|
24
|
+
if (scripts.length > 0) {
|
|
25
|
+
console.log('📜 [SDK] Found stepConfig scripts:', scripts.length, scripts.map(s => s.name));
|
|
26
|
+
}
|
|
27
|
+
return scripts;
|
|
28
|
+
}, [isInitialized]); // Re-compute when funnel initializes (local config should be loaded by then)
|
|
15
29
|
useEffect(() => {
|
|
16
30
|
// Only run in browser environment
|
|
17
31
|
if (typeof document === 'undefined') {
|
|
@@ -159,6 +173,8 @@ export function FunnelScriptInjector({ context, isInitialized }) {
|
|
|
159
173
|
ressources: context.resources,
|
|
160
174
|
}
|
|
161
175
|
: null,
|
|
176
|
+
// Expose stepConfig from HTML injection (NEW: supports A/B variants)
|
|
177
|
+
stepConfig: getAssignedStepConfig() || null,
|
|
162
178
|
};
|
|
163
179
|
};
|
|
164
180
|
// Set up utilities before injecting script
|
|
@@ -197,27 +213,27 @@ export function FunnelScriptInjector({ context, isInitialized }) {
|
|
|
197
213
|
existingScript.remove();
|
|
198
214
|
}
|
|
199
215
|
// 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
|
-
})();
|
|
216
|
+
const wrappedScript = `
|
|
217
|
+
(function() {
|
|
218
|
+
try {
|
|
219
|
+
// Check if we have basic DOM access
|
|
220
|
+
if (typeof document === 'undefined') {
|
|
221
|
+
console.error('[TagadaPay] Document not available');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check if we have Tagada
|
|
226
|
+
if (!window.Tagada) {
|
|
227
|
+
console.error('[TagadaPay] Tagada not available');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Execute the original script
|
|
232
|
+
${scriptBody}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('[TagadaPay] Script execution error:', error);
|
|
235
|
+
}
|
|
236
|
+
})();
|
|
221
237
|
`;
|
|
222
238
|
// Create and inject new script element
|
|
223
239
|
const scriptElement = document.createElement('script');
|
|
@@ -237,6 +253,94 @@ export function FunnelScriptInjector({ context, isInitialized }) {
|
|
|
237
253
|
// The ref will be cleared when script content actually changes (next effect run)
|
|
238
254
|
};
|
|
239
255
|
}, [context?.script, context?.currentStepId, isInitialized]);
|
|
256
|
+
// Effect for NEW stepConfig.scripts (from HTML injection, supports A/B variants)
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (typeof document === 'undefined')
|
|
259
|
+
return;
|
|
260
|
+
if (stepConfigScripts.length === 0)
|
|
261
|
+
return;
|
|
262
|
+
// Create a hash of current scripts to detect changes
|
|
263
|
+
const scriptsHash = JSON.stringify(stepConfigScripts.map(s => ({ name: s.name, content: s.content, position: s.position })));
|
|
264
|
+
// Check if scripts are actually in the DOM (handles React StrictMode cleanup)
|
|
265
|
+
const existingScripts = document.querySelectorAll('[data-tagada-stepconfig-script]');
|
|
266
|
+
const scriptsExistInDom = existingScripts.length > 0;
|
|
267
|
+
// Skip ONLY if: same hash AND scripts actually exist in DOM
|
|
268
|
+
// This handles React StrictMode where cleanup removes scripts but ref persists
|
|
269
|
+
if (lastInjectedStepConfigScriptsRef.current === scriptsHash && scriptsExistInDom) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// Remove any existing stepConfig scripts before re-injecting
|
|
273
|
+
existingScripts.forEach(el => el.remove());
|
|
274
|
+
// Inject each enabled script at its correct position
|
|
275
|
+
stepConfigScripts.forEach((script, index) => {
|
|
276
|
+
const position = script.position || 'head-end';
|
|
277
|
+
const scriptId = `tagada-stepconfig-script-${index}`;
|
|
278
|
+
// Extract script content (remove <script> tags if present)
|
|
279
|
+
let scriptBody = script.content.trim();
|
|
280
|
+
const scriptTagMatch = scriptBody.match(/^<script[^>]*>([\s\S]*)<\/script>$/i);
|
|
281
|
+
if (scriptTagMatch) {
|
|
282
|
+
scriptBody = scriptTagMatch[1].trim();
|
|
283
|
+
}
|
|
284
|
+
if (!scriptBody)
|
|
285
|
+
return;
|
|
286
|
+
// Wrap script content with error handling
|
|
287
|
+
const wrappedScript = `
|
|
288
|
+
(function() {
|
|
289
|
+
try {
|
|
290
|
+
// Script: ${script.name}
|
|
291
|
+
${scriptBody}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error('[TagadaPay] StepConfig script "${script.name}" error:', error);
|
|
294
|
+
}
|
|
295
|
+
})();
|
|
296
|
+
`;
|
|
297
|
+
// Create script element
|
|
298
|
+
const scriptElement = document.createElement('script');
|
|
299
|
+
scriptElement.id = scriptId;
|
|
300
|
+
scriptElement.setAttribute('data-tagada-stepconfig-script', 'true');
|
|
301
|
+
scriptElement.setAttribute('data-script-name', script.name);
|
|
302
|
+
scriptElement.textContent = wrappedScript;
|
|
303
|
+
// Inject at the correct position
|
|
304
|
+
switch (position) {
|
|
305
|
+
case 'head-start':
|
|
306
|
+
// Insert at the beginning of <head>
|
|
307
|
+
if (document.head.firstChild) {
|
|
308
|
+
document.head.insertBefore(scriptElement, document.head.firstChild);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
document.head.appendChild(scriptElement);
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
case 'head-end':
|
|
315
|
+
// Insert at the end of <head>
|
|
316
|
+
document.head.appendChild(scriptElement);
|
|
317
|
+
break;
|
|
318
|
+
case 'body-start':
|
|
319
|
+
// Insert at the beginning of <body>
|
|
320
|
+
if (document.body.firstChild) {
|
|
321
|
+
document.body.insertBefore(scriptElement, document.body.firstChild);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
document.body.appendChild(scriptElement);
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
case 'body-end':
|
|
328
|
+
default:
|
|
329
|
+
// Insert at the end of <body>
|
|
330
|
+
document.body.appendChild(scriptElement);
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// Track injected scripts to prevent re-injection
|
|
335
|
+
lastInjectedStepConfigScriptsRef.current = scriptsHash;
|
|
336
|
+
// Cleanup: remove scripts on unmount
|
|
337
|
+
// Note: We keep the ref intact but check DOM existence on re-mount
|
|
338
|
+
// This handles both StrictMode (re-inject if cleanup removed them) and
|
|
339
|
+
// real unmounts (scripts properly cleaned up)
|
|
340
|
+
return () => {
|
|
341
|
+
document.querySelectorAll('[data-tagada-stepconfig-script]').forEach(el => el.remove());
|
|
342
|
+
};
|
|
343
|
+
}, [stepConfigScripts]);
|
|
240
344
|
// This component doesn't render anything
|
|
241
345
|
return null;
|
|
242
346
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview Mode Indicator
|
|
3
|
+
*
|
|
4
|
+
* Visual indicator shown when the SDK is in draft/preview mode
|
|
5
|
+
* Helps users distinguish between preview and production environments
|
|
6
|
+
*/
|
|
7
|
+
export interface PreviewModeIndicatorProps {
|
|
8
|
+
/**
|
|
9
|
+
* Position of the indicator
|
|
10
|
+
* @default 'bottom-right'
|
|
11
|
+
*/
|
|
12
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
13
|
+
/**
|
|
14
|
+
* Custom className for styling
|
|
15
|
+
*/
|
|
16
|
+
className?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Show detailed info on hover
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
showDetails?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Preview Mode Indicator Component
|
|
25
|
+
*
|
|
26
|
+
* Automatically shows when:
|
|
27
|
+
* - Draft mode is enabled (draft=true)
|
|
28
|
+
* - Funnel tracking is disabled (funnelTracking=false)
|
|
29
|
+
* - Custom API environment is set (tagadaClientEnv)
|
|
30
|
+
* - Custom API base URL is set (tagadaClientBaseUrl)
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* import { PreviewModeIndicator } from '@tagadapay/plugin-sdk';
|
|
35
|
+
*
|
|
36
|
+
* function App() {
|
|
37
|
+
* return (
|
|
38
|
+
* <>
|
|
39
|
+
* <PreviewModeIndicator />
|
|
40
|
+
* {/* Your app content *\/}
|
|
41
|
+
* </>
|
|
42
|
+
* );
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function PreviewModeIndicator({ position, className, showDetails, }?: PreviewModeIndicatorProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Preview Mode Indicator
|
|
4
|
+
*
|
|
5
|
+
* Visual indicator shown when the SDK is in draft/preview mode
|
|
6
|
+
* Helps users distinguish between preview and production environments
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import { isDraftMode, isFunnelTrackingEnabled, getSDKParams } from '../../core/utils/previewMode';
|
|
10
|
+
/**
|
|
11
|
+
* Preview Mode Indicator Component
|
|
12
|
+
*
|
|
13
|
+
* Automatically shows when:
|
|
14
|
+
* - Draft mode is enabled (draft=true)
|
|
15
|
+
* - Funnel tracking is disabled (funnelTracking=false)
|
|
16
|
+
* - Custom API environment is set (tagadaClientEnv)
|
|
17
|
+
* - Custom API base URL is set (tagadaClientBaseUrl)
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* import { PreviewModeIndicator } from '@tagadapay/plugin-sdk';
|
|
22
|
+
*
|
|
23
|
+
* function App() {
|
|
24
|
+
* return (
|
|
25
|
+
* <>
|
|
26
|
+
* <PreviewModeIndicator />
|
|
27
|
+
* {/* Your app content *\/}
|
|
28
|
+
* </>
|
|
29
|
+
* );
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function PreviewModeIndicator({ position = 'bottom-right', className, showDetails = true, } = {}) {
|
|
34
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
35
|
+
const [params, setParams] = useState({});
|
|
36
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
// Check if we're in preview mode
|
|
39
|
+
const sdkParams = getSDKParams();
|
|
40
|
+
const draftMode = isDraftMode();
|
|
41
|
+
const trackingDisabled = !isFunnelTrackingEnabled();
|
|
42
|
+
const hasCustomEnv = !!(sdkParams.tagadaClientEnv || sdkParams.tagadaClientBaseUrl);
|
|
43
|
+
setParams(sdkParams);
|
|
44
|
+
setIsVisible(draftMode || trackingDisabled || hasCustomEnv);
|
|
45
|
+
}, []);
|
|
46
|
+
if (!isVisible)
|
|
47
|
+
return null;
|
|
48
|
+
const positionStyles = {
|
|
49
|
+
'top-left': { top: '16px', left: '16px' },
|
|
50
|
+
'top-right': { top: '16px', right: '16px' },
|
|
51
|
+
'bottom-left': { bottom: '16px', left: '16px' },
|
|
52
|
+
'bottom-right': { bottom: '16px', right: '16px' },
|
|
53
|
+
};
|
|
54
|
+
const isDraft = isDraftMode();
|
|
55
|
+
const trackingDisabled = !isFunnelTrackingEnabled();
|
|
56
|
+
return (_jsxs("div", { className: className, style: {
|
|
57
|
+
position: 'fixed',
|
|
58
|
+
zIndex: 999999,
|
|
59
|
+
...positionStyles[position],
|
|
60
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
61
|
+
}, onMouseEnter: () => showDetails && setIsExpanded(true), onMouseLeave: () => setIsExpanded(false), children: [_jsxs("div", { style: {
|
|
62
|
+
background: isDraft ? '#ff9500' : '#007aff',
|
|
63
|
+
color: 'white',
|
|
64
|
+
padding: '8px 12px',
|
|
65
|
+
borderRadius: '8px',
|
|
66
|
+
fontSize: '13px',
|
|
67
|
+
fontWeight: '600',
|
|
68
|
+
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
|
69
|
+
cursor: showDetails ? 'pointer' : 'default',
|
|
70
|
+
transition: 'all 0.2s ease',
|
|
71
|
+
display: 'flex',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
gap: '6px',
|
|
74
|
+
}, children: [_jsx("span", { style: { fontSize: '16px' }, children: "\uD83D\uDD0D" }), _jsx("span", { children: isDraft ? 'Preview Mode' : 'Dev Mode' })] }), showDetails && isExpanded && (_jsxs("div", { style: {
|
|
75
|
+
position: 'absolute',
|
|
76
|
+
bottom: position.includes('bottom') ? 'calc(100% + 8px)' : undefined,
|
|
77
|
+
top: position.includes('top') ? 'calc(100% + 8px)' : undefined,
|
|
78
|
+
right: position.includes('right') ? 0 : undefined,
|
|
79
|
+
left: position.includes('left') ? 0 : undefined,
|
|
80
|
+
background: 'white',
|
|
81
|
+
border: '1px solid #e5e5e5',
|
|
82
|
+
borderRadius: '8px',
|
|
83
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
84
|
+
padding: '12px',
|
|
85
|
+
minWidth: '250px',
|
|
86
|
+
fontSize: '12px',
|
|
87
|
+
lineHeight: '1.5',
|
|
88
|
+
}, children: [_jsx("div", { style: { marginBottom: '8px', fontWeight: '600', color: '#1d1d1f' }, children: "Current Environment" }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: '6px' }, children: [isDraft && (_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', color: '#86868b' }, children: [_jsx("span", { children: "Draft Mode:" }), _jsx("span", { style: { color: '#ff9500', fontWeight: '600' }, children: "ON" })] })), trackingDisabled && (_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', color: '#86868b' }, children: [_jsx("span", { children: "Tracking:" }), _jsx("span", { style: { color: '#ff3b30', fontWeight: '600' }, children: "DISABLED" })] })), params.funnelEnv && (_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', color: '#86868b' }, children: [_jsx("span", { children: "Funnel Env:" }), _jsx("span", { style: { color: '#1d1d1f', fontWeight: '600', fontFamily: 'monospace', fontSize: '11px' }, children: params.funnelEnv })] })), params.tagadaClientEnv && (_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', color: '#86868b' }, children: [_jsx("span", { children: "API Env:" }), _jsx("span", { style: { color: '#1d1d1f', fontWeight: '600', fontFamily: 'monospace', fontSize: '11px' }, children: params.tagadaClientEnv })] })), params.tagadaClientBaseUrl && (_jsxs("div", { style: { color: '#86868b' }, children: [_jsx("div", { style: { marginBottom: '4px' }, children: "API URL:" }), _jsx("div", { style: {
|
|
89
|
+
color: '#1d1d1f',
|
|
90
|
+
fontWeight: '600',
|
|
91
|
+
fontFamily: 'monospace',
|
|
92
|
+
fontSize: '10px',
|
|
93
|
+
wordBreak: 'break-all',
|
|
94
|
+
background: '#f5f5f7',
|
|
95
|
+
padding: '4px 6px',
|
|
96
|
+
borderRadius: '4px',
|
|
97
|
+
}, children: params.tagadaClientBaseUrl })] })), params.funnelId && (_jsxs("div", { style: { color: '#86868b', marginTop: '4px', paddingTop: '8px', borderTop: '1px solid #e5e5e5' }, children: [_jsx("div", { style: { marginBottom: '4px' }, children: "Funnel ID:" }), _jsx("div", { style: {
|
|
98
|
+
color: '#1d1d1f',
|
|
99
|
+
fontFamily: 'monospace',
|
|
100
|
+
fontSize: '10px',
|
|
101
|
+
wordBreak: 'break-all',
|
|
102
|
+
background: '#f5f5f7',
|
|
103
|
+
padding: '4px 6px',
|
|
104
|
+
borderRadius: '4px',
|
|
105
|
+
}, children: params.funnelId })] }))] }), _jsxs("div", { style: {
|
|
106
|
+
marginTop: '12px',
|
|
107
|
+
paddingTop: '8px',
|
|
108
|
+
borderTop: '1px solid #e5e5e5',
|
|
109
|
+
fontSize: '11px',
|
|
110
|
+
color: '#86868b',
|
|
111
|
+
textAlign: 'center',
|
|
112
|
+
}, children: ["Add ", _jsx("code", { style: { background: '#f5f5f7', padding: '2px 4px', borderRadius: '3px' }, children: "?forceReset=true" }), " to reset"] })] }))] }));
|
|
113
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CheckoutData } from '../../core/resources/checkout';
|
|
2
|
+
export interface UseApplePayCheckoutOptions {
|
|
3
|
+
checkout: CheckoutData | undefined;
|
|
4
|
+
onSuccess?: (result: {
|
|
5
|
+
payment: any;
|
|
6
|
+
order?: any;
|
|
7
|
+
}) => void;
|
|
8
|
+
onError?: (error: string) => void;
|
|
9
|
+
onCancel?: () => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function useApplePayCheckout({ checkout, onSuccess, onError, onCancel, }: UseApplePayCheckoutOptions): {
|
|
12
|
+
handleApplePayClick: () => void;
|
|
13
|
+
processingPayment: boolean;
|
|
14
|
+
error: string | null;
|
|
15
|
+
clearError: () => void;
|
|
16
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { usePaymentQuery } from './usePaymentQuery';
|
|
3
|
+
import { getBasisTheoryApiKey } from '../../../react/config/payment';
|
|
4
|
+
export function useApplePayCheckout({ checkout, onSuccess, onError, onCancel, }) {
|
|
5
|
+
const [processingPayment, setProcessingPayment] = useState(false);
|
|
6
|
+
const [error, setError] = useState(null);
|
|
7
|
+
const { processApplePayPayment } = usePaymentQuery();
|
|
8
|
+
const basistheoryPublicKey = useMemo(() => getBasisTheoryApiKey(), []);
|
|
9
|
+
// Validate merchant with Basis Theory
|
|
10
|
+
const validateMerchant = useCallback(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch('https://api.basistheory.com/apple-pay/session', {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'BT-API-KEY': basistheoryPublicKey,
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
display_name: checkout?.checkoutSession?.store?.name || 'Store',
|
|
20
|
+
domain: window.location.host,
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
25
|
+
}
|
|
26
|
+
return await response.json();
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.error('Merchant validation failed:', err);
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}, [basistheoryPublicKey, checkout?.checkoutSession?.store?.name]);
|
|
33
|
+
// Tokenize Apple Pay payment
|
|
34
|
+
const tokenizeApplePay = useCallback(async (event) => {
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch('https://api.basistheory.com/apple-pay', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'BT-API-KEY': basistheoryPublicKey,
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
apple_payment_data: event.payment.token,
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
48
|
+
}
|
|
49
|
+
const result = await response.json();
|
|
50
|
+
return result.apple_pay;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
console.error('Tokenization failed:', err);
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}, [basistheoryPublicKey]);
|
|
57
|
+
// Handle Apple Pay payment click
|
|
58
|
+
const handleApplePayClick = useCallback(() => {
|
|
59
|
+
// Don't proceed if checkout is not available
|
|
60
|
+
if (!checkout) {
|
|
61
|
+
console.error('Checkout data not available');
|
|
62
|
+
if (onError) {
|
|
63
|
+
onError('Checkout not ready');
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Create line items from checkout summary
|
|
68
|
+
const lineItems = [
|
|
69
|
+
{
|
|
70
|
+
label: 'Subtotal',
|
|
71
|
+
amount: (checkout.summary.subtotalAdjustedAmount / 100).toFixed(2),
|
|
72
|
+
type: 'final',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: 'Shipping',
|
|
76
|
+
amount: ((checkout.summary.shippingCost ?? 0) / 100).toFixed(2),
|
|
77
|
+
type: 'final',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: 'Tax',
|
|
81
|
+
amount: (checkout.summary.totalTaxAmount / 100).toFixed(2),
|
|
82
|
+
type: 'final',
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
const total = {
|
|
86
|
+
label: checkout.checkoutSession.store?.name || 'Store',
|
|
87
|
+
amount: (checkout.summary.totalAdjustedAmount / 100).toFixed(2),
|
|
88
|
+
type: 'final',
|
|
89
|
+
};
|
|
90
|
+
const request = {
|
|
91
|
+
countryCode: 'US', // Could be from payment method metadata
|
|
92
|
+
currencyCode: checkout.summary.currency,
|
|
93
|
+
supportedNetworks: ['visa', 'masterCard', 'amex', 'discover'],
|
|
94
|
+
merchantCapabilities: ['supports3DS'],
|
|
95
|
+
total,
|
|
96
|
+
lineItems,
|
|
97
|
+
};
|
|
98
|
+
try {
|
|
99
|
+
const session = new window.ApplePaySession(3, request);
|
|
100
|
+
// Merchant validation
|
|
101
|
+
session.onvalidatemerchant = (event) => {
|
|
102
|
+
void (async () => {
|
|
103
|
+
try {
|
|
104
|
+
const merchantSession = await validateMerchant();
|
|
105
|
+
session.completeMerchantValidation(merchantSession);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
console.error('Merchant validation failed:', error);
|
|
109
|
+
session.abort();
|
|
110
|
+
if (onError) {
|
|
111
|
+
onError('Merchant validation failed');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
115
|
+
};
|
|
116
|
+
// Payment authorized
|
|
117
|
+
session.onpaymentauthorized = (event) => {
|
|
118
|
+
void (async () => {
|
|
119
|
+
try {
|
|
120
|
+
setProcessingPayment(true);
|
|
121
|
+
// Tokenize payment
|
|
122
|
+
const applePayToken = await tokenizeApplePay(event);
|
|
123
|
+
// Complete Apple Pay sheet
|
|
124
|
+
session.completePayment(window.ApplePaySession.STATUS_SUCCESS);
|
|
125
|
+
// Process payment via SDK hook
|
|
126
|
+
const result = await processApplePayPayment(checkout.checkoutSession.id, applePayToken, {
|
|
127
|
+
onPaymentSuccess: (response) => {
|
|
128
|
+
// Keep processing state true during navigation
|
|
129
|
+
},
|
|
130
|
+
onPaymentFailed: (err) => {
|
|
131
|
+
setProcessingPayment(false);
|
|
132
|
+
setError(err.message);
|
|
133
|
+
if (onError) {
|
|
134
|
+
onError(err.message);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
// Call success callback
|
|
139
|
+
if (onSuccess) {
|
|
140
|
+
onSuccess(result);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error('Payment failed:', error);
|
|
145
|
+
session.completePayment(window.ApplePaySession.STATUS_FAILURE);
|
|
146
|
+
setProcessingPayment(false);
|
|
147
|
+
const errorMsg = error instanceof Error ? error.message : 'Payment failed';
|
|
148
|
+
setError(errorMsg);
|
|
149
|
+
if (onError) {
|
|
150
|
+
onError(errorMsg);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
};
|
|
155
|
+
// Handle cancellation
|
|
156
|
+
session.oncancel = () => {
|
|
157
|
+
console.log('Apple Pay cancelled by user');
|
|
158
|
+
setProcessingPayment(false);
|
|
159
|
+
if (onCancel) {
|
|
160
|
+
onCancel();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
// Handle errors
|
|
164
|
+
session.onerror = (event) => {
|
|
165
|
+
console.error('Apple Pay session error:', event);
|
|
166
|
+
setProcessingPayment(false);
|
|
167
|
+
};
|
|
168
|
+
session.begin();
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
console.error('Failed to start Apple Pay session:', error);
|
|
172
|
+
const errorMsg = 'Failed to start Apple Pay';
|
|
173
|
+
setError(errorMsg);
|
|
174
|
+
if (onError) {
|
|
175
|
+
onError(errorMsg);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}, [
|
|
179
|
+
checkout,
|
|
180
|
+
validateMerchant,
|
|
181
|
+
tokenizeApplePay,
|
|
182
|
+
processApplePayPayment,
|
|
183
|
+
onSuccess,
|
|
184
|
+
onError,
|
|
185
|
+
onCancel,
|
|
186
|
+
]);
|
|
187
|
+
return {
|
|
188
|
+
handleApplePayClick,
|
|
189
|
+
processingPayment,
|
|
190
|
+
error,
|
|
191
|
+
clearError: () => setError(null),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -12,16 +12,52 @@
|
|
|
12
12
|
* </TagadaProvider>
|
|
13
13
|
*
|
|
14
14
|
* // In any child component:
|
|
15
|
-
* const { context, next, isLoading } = useFunnel();
|
|
15
|
+
* const { context, next, isLoading, stepConfig } = useFunnel();
|
|
16
|
+
*
|
|
17
|
+
* // Access step-specific config (payment flows, static resources, etc.)
|
|
18
|
+
* const offerId = stepConfig.staticResources?.offer;
|
|
19
|
+
* const paymentFlowId = stepConfig.paymentFlowId;
|
|
16
20
|
* ```
|
|
17
21
|
*/
|
|
18
22
|
import { FunnelAction, FunnelNavigationResult, SimpleFunnelContext } from '../../core/resources/funnel';
|
|
19
|
-
import { FunnelState } from '../../core/funnelClient';
|
|
23
|
+
import { FunnelState, RuntimeStepConfig } from '../../core/funnelClient';
|
|
24
|
+
/**
|
|
25
|
+
* Step configuration from HTML injection (for current step/variant)
|
|
26
|
+
*/
|
|
27
|
+
export interface StepConfigValue {
|
|
28
|
+
/**
|
|
29
|
+
* Full step configuration object
|
|
30
|
+
*/
|
|
31
|
+
raw: RuntimeStepConfig | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Payment flow ID override for this step
|
|
34
|
+
* If set, this payment flow should be used instead of the store default
|
|
35
|
+
*/
|
|
36
|
+
paymentFlowId: string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Static resources assigned to this step/variant
|
|
39
|
+
* For A/B tests, this contains the resources for the specific variant
|
|
40
|
+
* e.g., { offer: 'offer_xxx', product: 'product_xxx' }
|
|
41
|
+
*/
|
|
42
|
+
staticResources: Record<string, string> | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Get scripts for a specific injection position
|
|
45
|
+
* Only returns enabled scripts
|
|
46
|
+
*/
|
|
47
|
+
getScripts: (position?: 'head-start' | 'head-end' | 'body-start' | 'body-end') => RuntimeStepConfig['scripts'];
|
|
48
|
+
/**
|
|
49
|
+
* Pixel tracking configuration
|
|
50
|
+
*/
|
|
51
|
+
pixels: Record<string, string> | undefined;
|
|
52
|
+
}
|
|
20
53
|
export interface FunnelContextValue extends FunnelState {
|
|
21
54
|
currentStep: {
|
|
22
55
|
id: string;
|
|
23
56
|
} | null;
|
|
24
|
-
|
|
57
|
+
stepConfig: StepConfigValue;
|
|
58
|
+
next: (event: FunnelAction, options?: {
|
|
59
|
+
waitForSession?: boolean;
|
|
60
|
+
}) => Promise<FunnelNavigationResult>;
|
|
25
61
|
goToStep: (stepId: string) => Promise<FunnelNavigationResult>;
|
|
26
62
|
updateContext: (updates: Partial<SimpleFunnelContext>) => Promise<void>;
|
|
27
63
|
initializeSession: (entryStepId?: string) => Promise<void>;
|
|
@@ -32,9 +68,9 @@ export interface FunnelContextValue extends FunnelState {
|
|
|
32
68
|
/**
|
|
33
69
|
* Hook to access funnel state and methods
|
|
34
70
|
*
|
|
35
|
-
* This hook
|
|
36
|
-
* All complex logic is handled at the provider level.
|
|
71
|
+
* This hook returns the funnel state from TagadaProvider plus step configuration
|
|
72
|
+
* from HTML injection. All complex logic is handled at the provider level.
|
|
37
73
|
*
|
|
38
|
-
* @returns FunnelContextValue with state and
|
|
74
|
+
* @returns FunnelContextValue with state, methods, and step config
|
|
39
75
|
*/
|
|
40
76
|
export declare function useFunnel(): FunnelContextValue;
|
|
@@ -12,19 +12,39 @@
|
|
|
12
12
|
* </TagadaProvider>
|
|
13
13
|
*
|
|
14
14
|
* // In any child component:
|
|
15
|
-
* const { context, next, isLoading } = useFunnel();
|
|
15
|
+
* const { context, next, isLoading, stepConfig } = useFunnel();
|
|
16
|
+
*
|
|
17
|
+
* // Access step-specific config (payment flows, static resources, etc.)
|
|
18
|
+
* const offerId = stepConfig.staticResources?.offer;
|
|
19
|
+
* const paymentFlowId = stepConfig.paymentFlowId;
|
|
16
20
|
* ```
|
|
17
21
|
*/
|
|
22
|
+
import { useMemo } from 'react';
|
|
18
23
|
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
24
|
+
import { getAssignedStepConfig, getAssignedPaymentFlowId, getAssignedStaticResources, getAssignedScripts, } from '../../core/funnelClient';
|
|
19
25
|
/**
|
|
20
26
|
* Hook to access funnel state and methods
|
|
21
27
|
*
|
|
22
|
-
* This hook
|
|
23
|
-
* All complex logic is handled at the provider level.
|
|
28
|
+
* This hook returns the funnel state from TagadaProvider plus step configuration
|
|
29
|
+
* from HTML injection. All complex logic is handled at the provider level.
|
|
24
30
|
*
|
|
25
|
-
* @returns FunnelContextValue with state and
|
|
31
|
+
* @returns FunnelContextValue with state, methods, and step config
|
|
26
32
|
*/
|
|
27
33
|
export function useFunnel() {
|
|
28
34
|
const { funnel } = useTagadaContext();
|
|
29
|
-
|
|
35
|
+
// Compute step config from HTML injection (memoized, computed once on mount)
|
|
36
|
+
const stepConfig = useMemo(() => {
|
|
37
|
+
const raw = getAssignedStepConfig();
|
|
38
|
+
return {
|
|
39
|
+
raw,
|
|
40
|
+
paymentFlowId: getAssignedPaymentFlowId(),
|
|
41
|
+
staticResources: getAssignedStaticResources(),
|
|
42
|
+
getScripts: (position) => getAssignedScripts(position),
|
|
43
|
+
pixels: raw?.pixels,
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
46
|
+
return {
|
|
47
|
+
...funnel,
|
|
48
|
+
stepConfig,
|
|
49
|
+
};
|
|
30
50
|
}
|