@appfunnel-dev/sdk 0.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 +91 -0
- package/dist/index.cjs +660 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +207 -0
- package/dist/index.d.ts +207 -0
- package/dist/index.js +642 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.cjs +1178 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +204 -0
- package/dist/internal.d.ts +204 -0
- package/dist/internal.js +1176 -0
- package/dist/internal.js.map +1 -0
- package/dist/types-BPNKwDaL.d.cts +222 -0
- package/dist/types-BPNKwDaL.d.ts +222 -0
- package/package.json +68 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var stripeJs = require('@stripe/stripe-js');
|
|
6
|
+
var reactStripeJs = require('@stripe/react-stripe-js');
|
|
7
|
+
|
|
8
|
+
// src/config.ts
|
|
9
|
+
function defineConfig(config) {
|
|
10
|
+
return config;
|
|
11
|
+
}
|
|
12
|
+
function definePage(definition) {
|
|
13
|
+
return definition;
|
|
14
|
+
}
|
|
15
|
+
function registerIntegration(id, loader) {
|
|
16
|
+
}
|
|
17
|
+
var FunnelContext = react.createContext(null);
|
|
18
|
+
function useFunnelContext() {
|
|
19
|
+
const ctx = react.useContext(FunnelContext);
|
|
20
|
+
if (!ctx) {
|
|
21
|
+
throw new Error("useFunnelContext must be used within a <FunnelProvider>");
|
|
22
|
+
}
|
|
23
|
+
return ctx;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/hooks/useVariable.ts
|
|
27
|
+
function useVariable(id) {
|
|
28
|
+
const { variableStore } = useFunnelContext();
|
|
29
|
+
const subscribe = react.useCallback(
|
|
30
|
+
(callback) => variableStore.subscribe(callback),
|
|
31
|
+
[variableStore]
|
|
32
|
+
);
|
|
33
|
+
const getSnapshot = react.useCallback(
|
|
34
|
+
() => variableStore.get(id),
|
|
35
|
+
[variableStore, id]
|
|
36
|
+
);
|
|
37
|
+
const value = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
38
|
+
const setValue = react.useCallback(
|
|
39
|
+
(v) => variableStore.set(id, v),
|
|
40
|
+
[variableStore, id]
|
|
41
|
+
);
|
|
42
|
+
return [value, setValue];
|
|
43
|
+
}
|
|
44
|
+
function useVariables() {
|
|
45
|
+
const { variableStore } = useFunnelContext();
|
|
46
|
+
const subscribe = react.useCallback(
|
|
47
|
+
(callback) => variableStore.subscribe(callback),
|
|
48
|
+
[variableStore]
|
|
49
|
+
);
|
|
50
|
+
const getSnapshot = react.useCallback(
|
|
51
|
+
() => variableStore.getState(),
|
|
52
|
+
[variableStore]
|
|
53
|
+
);
|
|
54
|
+
return react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
55
|
+
}
|
|
56
|
+
function useUser() {
|
|
57
|
+
const { variableStore, tracker } = useFunnelContext();
|
|
58
|
+
const subscribe = react.useCallback(
|
|
59
|
+
(cb) => variableStore.subscribe(cb),
|
|
60
|
+
[variableStore]
|
|
61
|
+
);
|
|
62
|
+
const getSnapshot = react.useCallback(
|
|
63
|
+
() => variableStore.getState(),
|
|
64
|
+
[variableStore]
|
|
65
|
+
);
|
|
66
|
+
const variables = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
67
|
+
return react.useMemo(() => ({
|
|
68
|
+
email: variables["user.email"] || "",
|
|
69
|
+
name: variables["user.name"] || "",
|
|
70
|
+
stripeCustomerId: variables["user.stripeCustomerId"] || "",
|
|
71
|
+
paddleCustomerId: variables["user.paddleCustomerId"] || "",
|
|
72
|
+
setEmail(email) {
|
|
73
|
+
variableStore.set("user.email", email);
|
|
74
|
+
tracker.identify(email);
|
|
75
|
+
},
|
|
76
|
+
setName(name) {
|
|
77
|
+
variableStore.set("user.name", name);
|
|
78
|
+
}
|
|
79
|
+
}), [variables, variableStore, tracker]);
|
|
80
|
+
}
|
|
81
|
+
function useUserVariable(field) {
|
|
82
|
+
const { variableStore } = useFunnelContext();
|
|
83
|
+
const key = `user.${field}`;
|
|
84
|
+
const subscribe = react.useCallback(
|
|
85
|
+
(cb) => variableStore.subscribe(cb),
|
|
86
|
+
[variableStore]
|
|
87
|
+
);
|
|
88
|
+
const getSnapshot = react.useCallback(
|
|
89
|
+
() => variableStore.get(key) || "",
|
|
90
|
+
[variableStore, key]
|
|
91
|
+
);
|
|
92
|
+
const value = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
93
|
+
const setValue = react.useCallback(
|
|
94
|
+
(v) => variableStore.set(key, v),
|
|
95
|
+
[variableStore, key]
|
|
96
|
+
);
|
|
97
|
+
return [value, setValue];
|
|
98
|
+
}
|
|
99
|
+
function useQueryParams() {
|
|
100
|
+
const { variableStore } = useFunnelContext();
|
|
101
|
+
const subscribe = react.useCallback(
|
|
102
|
+
(cb) => variableStore.subscribe(cb),
|
|
103
|
+
[variableStore]
|
|
104
|
+
);
|
|
105
|
+
const getSnapshot = react.useCallback(
|
|
106
|
+
() => variableStore.getState(),
|
|
107
|
+
[variableStore]
|
|
108
|
+
);
|
|
109
|
+
const variables = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
110
|
+
return react.useMemo(() => {
|
|
111
|
+
const params = {};
|
|
112
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
113
|
+
if (key.startsWith("query.") && typeof value === "string") {
|
|
114
|
+
params[key.slice(6)] = value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return params;
|
|
118
|
+
}, [variables]);
|
|
119
|
+
}
|
|
120
|
+
function useLocale() {
|
|
121
|
+
return react.useMemo(() => {
|
|
122
|
+
if (typeof navigator === "undefined") {
|
|
123
|
+
return {
|
|
124
|
+
locale: "en-US",
|
|
125
|
+
language: "en",
|
|
126
|
+
region: "US",
|
|
127
|
+
languages: ["en-US"],
|
|
128
|
+
timeZone: "UTC",
|
|
129
|
+
is24Hour: false
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const locale = navigator.language || "en-US";
|
|
133
|
+
const [language, region] = locale.split("-");
|
|
134
|
+
const languages = [...navigator.languages || [locale]];
|
|
135
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
136
|
+
const is24Hour = detect24Hour(locale);
|
|
137
|
+
return {
|
|
138
|
+
locale,
|
|
139
|
+
language: language || "en",
|
|
140
|
+
region: region?.toUpperCase() || "US",
|
|
141
|
+
languages,
|
|
142
|
+
timeZone,
|
|
143
|
+
is24Hour
|
|
144
|
+
};
|
|
145
|
+
}, []);
|
|
146
|
+
}
|
|
147
|
+
function detect24Hour(locale) {
|
|
148
|
+
try {
|
|
149
|
+
const formatted = new Intl.DateTimeFormat(locale, { hour: "numeric" }).format(/* @__PURE__ */ new Date());
|
|
150
|
+
return !formatted.match(/am|pm/i);
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function useTranslation() {
|
|
156
|
+
const { i18n } = useFunnelContext();
|
|
157
|
+
const subscribe = react.useCallback(
|
|
158
|
+
(cb) => i18n.subscribe(cb),
|
|
159
|
+
[i18n]
|
|
160
|
+
);
|
|
161
|
+
const getSnapshot = react.useCallback(
|
|
162
|
+
() => i18n.getLocale(),
|
|
163
|
+
[i18n]
|
|
164
|
+
);
|
|
165
|
+
const locale = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
166
|
+
const t = react.useCallback(
|
|
167
|
+
(key, params) => i18n.t(key, params),
|
|
168
|
+
[i18n, locale]
|
|
169
|
+
// eslint-disable-line react-hooks/exhaustive-deps
|
|
170
|
+
);
|
|
171
|
+
const setLocale = react.useCallback(
|
|
172
|
+
(l) => i18n.setLocale(l),
|
|
173
|
+
[i18n]
|
|
174
|
+
);
|
|
175
|
+
const availableLocales = i18n.getAvailableLocales();
|
|
176
|
+
return { t, locale, setLocale, availableLocales };
|
|
177
|
+
}
|
|
178
|
+
function useNavigation() {
|
|
179
|
+
const { router, variableStore, tracker } = useFunnelContext();
|
|
180
|
+
const subscribe = react.useCallback(
|
|
181
|
+
(cb) => variableStore.subscribe(cb),
|
|
182
|
+
[variableStore]
|
|
183
|
+
);
|
|
184
|
+
const getSnapshot = react.useCallback(
|
|
185
|
+
() => variableStore.getState(),
|
|
186
|
+
[variableStore]
|
|
187
|
+
);
|
|
188
|
+
const variables = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
189
|
+
const goToNextPage = react.useCallback(() => {
|
|
190
|
+
const previousPage = router.getCurrentPage();
|
|
191
|
+
if (previousPage) {
|
|
192
|
+
tracker.stopPageTracking();
|
|
193
|
+
}
|
|
194
|
+
const nextKey = router.goToNextPage(variables);
|
|
195
|
+
if (nextKey) {
|
|
196
|
+
const nextPage = router.getCurrentPage();
|
|
197
|
+
if (nextPage) {
|
|
198
|
+
tracker.track("page.view", {
|
|
199
|
+
pageId: nextPage.key,
|
|
200
|
+
pageKey: nextPage.key,
|
|
201
|
+
pageName: nextPage.name
|
|
202
|
+
});
|
|
203
|
+
tracker.startPageTracking(nextPage.key);
|
|
204
|
+
}
|
|
205
|
+
variableStore.setMany({
|
|
206
|
+
"page.currentId": nextKey,
|
|
207
|
+
"page.currentIndex": router.getPageHistory().length,
|
|
208
|
+
"page.current": router.getPageHistory().length + 1,
|
|
209
|
+
"page.startedAt": Date.now(),
|
|
210
|
+
"page.timeOnCurrent": 0
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}, [router, variables, tracker, variableStore]);
|
|
214
|
+
const goBack = react.useCallback(() => {
|
|
215
|
+
tracker.stopPageTracking();
|
|
216
|
+
const prevKey = router.goBack();
|
|
217
|
+
if (prevKey) {
|
|
218
|
+
const page = router.getCurrentPage();
|
|
219
|
+
if (page) {
|
|
220
|
+
tracker.track("page.view", {
|
|
221
|
+
pageId: page.key,
|
|
222
|
+
pageKey: page.key,
|
|
223
|
+
pageName: page.name
|
|
224
|
+
});
|
|
225
|
+
tracker.startPageTracking(page.key);
|
|
226
|
+
}
|
|
227
|
+
variableStore.setMany({
|
|
228
|
+
"page.currentId": prevKey,
|
|
229
|
+
"page.currentIndex": router.getPageHistory().length,
|
|
230
|
+
"page.current": router.getPageHistory().length + 1,
|
|
231
|
+
"page.startedAt": Date.now(),
|
|
232
|
+
"page.timeOnCurrent": 0
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}, [router, tracker, variableStore]);
|
|
236
|
+
const goToPage = react.useCallback((pageKey) => {
|
|
237
|
+
tracker.stopPageTracking();
|
|
238
|
+
const key = router.goToPage(pageKey);
|
|
239
|
+
if (key) {
|
|
240
|
+
const page = router.getCurrentPage();
|
|
241
|
+
if (page) {
|
|
242
|
+
tracker.track("page.view", {
|
|
243
|
+
pageId: page.key,
|
|
244
|
+
pageKey: page.key,
|
|
245
|
+
pageName: page.name
|
|
246
|
+
});
|
|
247
|
+
tracker.startPageTracking(page.key);
|
|
248
|
+
}
|
|
249
|
+
variableStore.setMany({
|
|
250
|
+
"page.currentId": key,
|
|
251
|
+
"page.currentIndex": router.getPageHistory().length,
|
|
252
|
+
"page.current": router.getPageHistory().length + 1,
|
|
253
|
+
"page.startedAt": Date.now(),
|
|
254
|
+
"page.timeOnCurrent": 0
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}, [router, tracker, variableStore]);
|
|
258
|
+
return {
|
|
259
|
+
goToNextPage,
|
|
260
|
+
goBack,
|
|
261
|
+
goToPage,
|
|
262
|
+
currentPage: router.getCurrentPage(),
|
|
263
|
+
pageHistory: router.getPageHistory(),
|
|
264
|
+
progress: router.getProgress()
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function useProducts() {
|
|
268
|
+
const { products, variableStore, selectProduct: ctxSelect } = useFunnelContext();
|
|
269
|
+
const subscribe = react.useCallback(
|
|
270
|
+
(cb) => variableStore.subscribe(cb),
|
|
271
|
+
[variableStore]
|
|
272
|
+
);
|
|
273
|
+
const getSnapshot = react.useCallback(
|
|
274
|
+
() => variableStore.get("products.selectedProductId"),
|
|
275
|
+
[variableStore]
|
|
276
|
+
);
|
|
277
|
+
const selectedId = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
278
|
+
const selected = products.find((p) => p.id === selectedId) || null;
|
|
279
|
+
const select = react.useCallback((productId) => {
|
|
280
|
+
ctxSelect(productId);
|
|
281
|
+
}, [ctxSelect]);
|
|
282
|
+
return { products, selected, select };
|
|
283
|
+
}
|
|
284
|
+
function useTracking() {
|
|
285
|
+
const { tracker } = useFunnelContext();
|
|
286
|
+
const track = react.useCallback(
|
|
287
|
+
(eventName, data) => {
|
|
288
|
+
tracker.track(eventName, data);
|
|
289
|
+
},
|
|
290
|
+
[tracker]
|
|
291
|
+
);
|
|
292
|
+
const identify = react.useCallback(
|
|
293
|
+
(email) => {
|
|
294
|
+
tracker.identify(email);
|
|
295
|
+
},
|
|
296
|
+
[tracker]
|
|
297
|
+
);
|
|
298
|
+
return { track, identify };
|
|
299
|
+
}
|
|
300
|
+
function usePayment() {
|
|
301
|
+
const { variableStore, tracker } = useFunnelContext();
|
|
302
|
+
const subscribe = react.useCallback(
|
|
303
|
+
(cb) => variableStore.subscribe(cb),
|
|
304
|
+
[variableStore]
|
|
305
|
+
);
|
|
306
|
+
const getSnapshot = react.useCallback(
|
|
307
|
+
() => variableStore.getState(),
|
|
308
|
+
[variableStore]
|
|
309
|
+
);
|
|
310
|
+
const variables = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
311
|
+
return react.useMemo(() => {
|
|
312
|
+
const last4 = variables["card.last4"] || "";
|
|
313
|
+
const brand = variables["card.brand"] || "";
|
|
314
|
+
const expMonth = variables["card.expMonth"] || 0;
|
|
315
|
+
const expYear = variables["card.expYear"] || 0;
|
|
316
|
+
return {
|
|
317
|
+
customerId: tracker.getCustomerId(),
|
|
318
|
+
isAuthorized: !!last4,
|
|
319
|
+
loading: !!variables["payment.loading"],
|
|
320
|
+
error: variables["payment.error"] || null,
|
|
321
|
+
cardDetails: last4 ? { last4, brand, expMonth, expYear } : null
|
|
322
|
+
};
|
|
323
|
+
}, [variables, tracker]);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/hooks/useFunnel.ts
|
|
327
|
+
function useFunnel() {
|
|
328
|
+
const { funnelId, campaignId, tracker } = useFunnelContext();
|
|
329
|
+
return {
|
|
330
|
+
funnelId,
|
|
331
|
+
campaignId,
|
|
332
|
+
sessionId: tracker.getSessionId(),
|
|
333
|
+
variables: useVariables(),
|
|
334
|
+
user: useUser(),
|
|
335
|
+
queryParams: useQueryParams(),
|
|
336
|
+
navigation: useNavigation(),
|
|
337
|
+
products: useProducts(),
|
|
338
|
+
tracking: useTracking(),
|
|
339
|
+
payment: usePayment()
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function InnerPaymentForm({
|
|
343
|
+
paymentMode,
|
|
344
|
+
validateOnly,
|
|
345
|
+
onSuccess,
|
|
346
|
+
onError
|
|
347
|
+
}) {
|
|
348
|
+
const stripe = reactStripeJs.useStripe();
|
|
349
|
+
const elements = reactStripeJs.useElements();
|
|
350
|
+
const [error, setError] = react.useState(null);
|
|
351
|
+
const { variableStore, campaignId, tracker, apiBaseUrl, products } = useFunnelContext();
|
|
352
|
+
const handleSubmit = react.useCallback(async () => {
|
|
353
|
+
if (!stripe || !elements) {
|
|
354
|
+
const msg = "Stripe not loaded";
|
|
355
|
+
setError(msg);
|
|
356
|
+
onError?.(msg);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
setError(null);
|
|
360
|
+
variableStore.set("payment.loading", true);
|
|
361
|
+
try {
|
|
362
|
+
const confirmFn = paymentMode === "setup" ? stripe.confirmSetup : stripe.confirmPayment;
|
|
363
|
+
const confirmResult = await confirmFn({
|
|
364
|
+
elements,
|
|
365
|
+
redirect: "if_required",
|
|
366
|
+
confirmParams: { return_url: window.location.href }
|
|
367
|
+
});
|
|
368
|
+
if (confirmResult.error) {
|
|
369
|
+
const msg = confirmResult.error.message || "Payment failed";
|
|
370
|
+
setError(msg);
|
|
371
|
+
variableStore.set("payment.error", msg);
|
|
372
|
+
onError?.(msg);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
tracker.track("checkout.payment_added");
|
|
376
|
+
if (validateOnly) {
|
|
377
|
+
const piId = "paymentIntent" in confirmResult ? confirmResult.paymentIntent : void 0;
|
|
378
|
+
if (!piId?.id) {
|
|
379
|
+
const msg = "PaymentIntent not found after confirmation";
|
|
380
|
+
setError(msg);
|
|
381
|
+
variableStore.set("payment.error", msg);
|
|
382
|
+
onError?.(msg);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const response2 = await fetch(
|
|
386
|
+
`${apiBaseUrl}/campaign/${campaignId}/stripe/validate-card`,
|
|
387
|
+
{
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers: { "Content-Type": "application/json" },
|
|
390
|
+
body: JSON.stringify({
|
|
391
|
+
campaignId,
|
|
392
|
+
sessionId: tracker.getSessionId(),
|
|
393
|
+
paymentIntentId: piId.id
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
const result2 = await response2.json();
|
|
398
|
+
if (!result2.success) {
|
|
399
|
+
const msg = result2.error || "Card validation failed";
|
|
400
|
+
setError(msg);
|
|
401
|
+
variableStore.set("payment.error", msg);
|
|
402
|
+
onError?.(msg);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
variableStore.setMany({
|
|
406
|
+
"card.last4": result2.card.last4,
|
|
407
|
+
"card.brand": result2.card.brand,
|
|
408
|
+
"card.expMonth": result2.card.expMonth,
|
|
409
|
+
"card.expYear": result2.card.expYear,
|
|
410
|
+
"card.funding": result2.card.funding,
|
|
411
|
+
"payment.error": ""
|
|
412
|
+
});
|
|
413
|
+
onSuccess?.();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const paymentIntentId = "paymentIntent" in confirmResult ? confirmResult.paymentIntent?.id : void 0;
|
|
417
|
+
const selectedId = variableStore.get("products.selectedProductId");
|
|
418
|
+
const product = products.find((p) => p.id === selectedId);
|
|
419
|
+
if (!product?.stripePriceId) {
|
|
420
|
+
const msg = "No product selected or missing Stripe price";
|
|
421
|
+
setError(msg);
|
|
422
|
+
variableStore.set("payment.error", msg);
|
|
423
|
+
onError?.(msg);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const variables = variableStore.getState();
|
|
427
|
+
await tracker.updateUserData(variables);
|
|
428
|
+
const response = await fetch(
|
|
429
|
+
`${apiBaseUrl}/campaign/${campaignId}/stripe/purchase`,
|
|
430
|
+
{
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: { "Content-Type": "application/json" },
|
|
433
|
+
body: JSON.stringify({
|
|
434
|
+
campaignId,
|
|
435
|
+
sessionId: tracker.getSessionId(),
|
|
436
|
+
stripePriceId: product.stripePriceId,
|
|
437
|
+
trialPeriodDays: product.hasTrial ? product.trialDays : void 0,
|
|
438
|
+
onSessionPiId: paymentMode === "payment" ? paymentIntentId : void 0
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
const result = await response.json();
|
|
443
|
+
if (result.success) {
|
|
444
|
+
variableStore.set("payment.error", "");
|
|
445
|
+
if (result.eventId || result.eventIds?.firstPeriod) {
|
|
446
|
+
tracker.track("purchase.complete", {
|
|
447
|
+
amount: product.rawPrice ? product.rawPrice / 100 : 0,
|
|
448
|
+
currency: product.currencyCode || "USD"
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
onSuccess?.();
|
|
452
|
+
} else {
|
|
453
|
+
const msg = result.error || "Failed to process payment";
|
|
454
|
+
setError(msg);
|
|
455
|
+
variableStore.set("payment.error", msg);
|
|
456
|
+
onError?.(msg);
|
|
457
|
+
}
|
|
458
|
+
} catch (err) {
|
|
459
|
+
const msg = err instanceof Error ? err.message : "An error occurred";
|
|
460
|
+
setError(msg);
|
|
461
|
+
variableStore.set("payment.error", msg);
|
|
462
|
+
onError?.(msg);
|
|
463
|
+
} finally {
|
|
464
|
+
variableStore.set("payment.loading", false);
|
|
465
|
+
}
|
|
466
|
+
}, [stripe, elements, paymentMode, validateOnly, variableStore, campaignId, tracker, apiBaseUrl, products, onSuccess, onError]);
|
|
467
|
+
react.useEffect(() => {
|
|
468
|
+
if (typeof window !== "undefined") {
|
|
469
|
+
window.__paymentElementSubmit = handleSubmit;
|
|
470
|
+
}
|
|
471
|
+
return () => {
|
|
472
|
+
if (typeof window !== "undefined") {
|
|
473
|
+
delete window.__paymentElementSubmit;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}, [handleSubmit]);
|
|
477
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
478
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.PaymentElement, { options: { layout: "tabs" } }),
|
|
479
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#ef4444", fontSize: "14px", marginTop: "12px" }, children: error })
|
|
480
|
+
] });
|
|
481
|
+
}
|
|
482
|
+
function PaymentForm({
|
|
483
|
+
productId,
|
|
484
|
+
mode = "checkout",
|
|
485
|
+
onSuccess,
|
|
486
|
+
onError,
|
|
487
|
+
className,
|
|
488
|
+
appearance
|
|
489
|
+
}) {
|
|
490
|
+
const { campaignId, tracker, variableStore, apiBaseUrl, products } = useFunnelContext();
|
|
491
|
+
const [email] = useVariable("user.email");
|
|
492
|
+
const validateOnly = mode === "validate-only";
|
|
493
|
+
const product = react.useMemo(() => {
|
|
494
|
+
if (productId) return products.find((p) => p.id === productId) || null;
|
|
495
|
+
const selectedId = variableStore.get("products.selectedProductId");
|
|
496
|
+
return products.find((p) => p.id === selectedId) || null;
|
|
497
|
+
}, [productId, products, variableStore]);
|
|
498
|
+
const paymentMode = react.useMemo(() => {
|
|
499
|
+
if (validateOnly) return "payment";
|
|
500
|
+
if (product?.hasTrial && !product.paidTrial) return "setup";
|
|
501
|
+
return "payment";
|
|
502
|
+
}, [product, validateOnly]);
|
|
503
|
+
const [stripePromise, setStripePromise] = react.useState(null);
|
|
504
|
+
const [clientSecret, setClientSecret] = react.useState(null);
|
|
505
|
+
const [error, setError] = react.useState(null);
|
|
506
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
507
|
+
const hasInitialized = react.useRef(false);
|
|
508
|
+
react.useEffect(() => {
|
|
509
|
+
if (!email || !campaignId || hasInitialized.current) return;
|
|
510
|
+
if (!product?.storePriceId) return;
|
|
511
|
+
hasInitialized.current = true;
|
|
512
|
+
setIsLoading(true);
|
|
513
|
+
variableStore.set("payment.loading", true);
|
|
514
|
+
const createIntent = async () => {
|
|
515
|
+
try {
|
|
516
|
+
const endpoint = paymentMode === "setup" ? `/campaign/${campaignId}/stripe/setup-intent` : `/campaign/${campaignId}/stripe/payment-intent`;
|
|
517
|
+
const body = {
|
|
518
|
+
campaignId,
|
|
519
|
+
sessionId: tracker.getSessionId(),
|
|
520
|
+
customerEmail: email,
|
|
521
|
+
priceId: product.storePriceId
|
|
522
|
+
};
|
|
523
|
+
if (validateOnly) body.validateOnly = true;
|
|
524
|
+
const response = await fetch(`${apiBaseUrl}${endpoint}`, {
|
|
525
|
+
method: "POST",
|
|
526
|
+
headers: { "Content-Type": "application/json" },
|
|
527
|
+
body: JSON.stringify(body)
|
|
528
|
+
});
|
|
529
|
+
const result = await response.json();
|
|
530
|
+
if (!result.success) {
|
|
531
|
+
throw new Error(result.error || "Failed to create intent");
|
|
532
|
+
}
|
|
533
|
+
setStripePromise(stripeJs.loadStripe(result.publishableKey));
|
|
534
|
+
setClientSecret(result.clientSecret);
|
|
535
|
+
variableStore.set("user.stripeCustomerId", result.customerId);
|
|
536
|
+
tracker.track("checkout.start", {
|
|
537
|
+
productId: product.id,
|
|
538
|
+
priceId: product.stripePriceId || product.storePriceId,
|
|
539
|
+
productName: product.displayName
|
|
540
|
+
});
|
|
541
|
+
} catch (err) {
|
|
542
|
+
const msg = err instanceof Error ? err.message : "Failed to initialize payment";
|
|
543
|
+
setError(msg);
|
|
544
|
+
variableStore.set("payment.error", msg);
|
|
545
|
+
} finally {
|
|
546
|
+
setIsLoading(false);
|
|
547
|
+
variableStore.set("payment.loading", false);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
createIntent();
|
|
551
|
+
}, [email, campaignId, product, paymentMode, validateOnly, tracker, variableStore, apiBaseUrl]);
|
|
552
|
+
if (isLoading) {
|
|
553
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#6b7280" }, children: "Loading payment form..." });
|
|
554
|
+
}
|
|
555
|
+
if (!email) {
|
|
556
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#ef4444", fontSize: "14px" }, children: "Email is required to initialize payment" });
|
|
557
|
+
}
|
|
558
|
+
if (error) {
|
|
559
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#ef4444", fontSize: "14px" }, children: error });
|
|
560
|
+
}
|
|
561
|
+
if (!stripePromise || !clientSecret) {
|
|
562
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#6b7280" }, children: "Initializing payment..." });
|
|
563
|
+
}
|
|
564
|
+
const defaultAppearance = {
|
|
565
|
+
theme: "stripe",
|
|
566
|
+
variables: { colorPrimary: "#3b82f6", borderRadius: "8px" }
|
|
567
|
+
};
|
|
568
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.Elements, { stripe: stripePromise, options: { clientSecret, appearance: appearance || defaultAppearance }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
569
|
+
InnerPaymentForm,
|
|
570
|
+
{
|
|
571
|
+
paymentMode,
|
|
572
|
+
validateOnly,
|
|
573
|
+
onSuccess,
|
|
574
|
+
onError
|
|
575
|
+
}
|
|
576
|
+
) }) });
|
|
577
|
+
}
|
|
578
|
+
function PaddleCheckout({
|
|
579
|
+
productId,
|
|
580
|
+
mode = "overlay",
|
|
581
|
+
onSuccess,
|
|
582
|
+
onError,
|
|
583
|
+
className
|
|
584
|
+
}) {
|
|
585
|
+
const { variableStore, tracker, products } = useFunnelContext();
|
|
586
|
+
const containerRef = react.useRef(null);
|
|
587
|
+
const initializedRef = react.useRef(false);
|
|
588
|
+
const product = productId ? products.find((p) => p.id === productId) : products.find((p) => p.id === variableStore.get("products.selectedProductId"));
|
|
589
|
+
const handleCheckoutComplete = react.useCallback(() => {
|
|
590
|
+
tracker.track("purchase.complete", {
|
|
591
|
+
amount: product?.rawPrice ? product.rawPrice / 100 : 0,
|
|
592
|
+
currency: product?.currencyCode || "USD"
|
|
593
|
+
});
|
|
594
|
+
onSuccess?.();
|
|
595
|
+
}, [tracker, product, onSuccess]);
|
|
596
|
+
react.useEffect(() => {
|
|
597
|
+
if (initializedRef.current || !product?.paddlePriceId) return;
|
|
598
|
+
initializedRef.current = true;
|
|
599
|
+
if (!window.Paddle) {
|
|
600
|
+
onError?.("Paddle.js not loaded. Include the Paddle script in your HTML.");
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const email = variableStore.get("user.email") || "";
|
|
604
|
+
if (mode === "overlay") {
|
|
605
|
+
window.Paddle.Checkout.open({
|
|
606
|
+
items: [{ priceId: product.paddlePriceId, quantity: 1 }],
|
|
607
|
+
customer: email ? { email } : void 0,
|
|
608
|
+
successCallback: handleCheckoutComplete,
|
|
609
|
+
closeCallback: () => {
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
} else {
|
|
613
|
+
if (containerRef.current) {
|
|
614
|
+
window.Paddle.Checkout.open({
|
|
615
|
+
items: [{ priceId: product.paddlePriceId, quantity: 1 }],
|
|
616
|
+
customer: email ? { email } : void 0,
|
|
617
|
+
settings: {
|
|
618
|
+
displayMode: "inline",
|
|
619
|
+
frameTarget: containerRef.current.id || "paddle-checkout",
|
|
620
|
+
frameInitialHeight: 450,
|
|
621
|
+
frameStyle: "width: 100%; min-width: 312px; background-color: transparent; border: none;"
|
|
622
|
+
},
|
|
623
|
+
successCallback: handleCheckoutComplete
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
tracker.track("checkout.start", {
|
|
628
|
+
productId: product.id,
|
|
629
|
+
priceId: product.paddlePriceId,
|
|
630
|
+
productName: product.displayName
|
|
631
|
+
});
|
|
632
|
+
}, [product, mode, variableStore, tracker, handleCheckoutComplete, onError]);
|
|
633
|
+
if (!product) {
|
|
634
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#ef4444", fontSize: "14px" }, children: "No product selected" });
|
|
635
|
+
}
|
|
636
|
+
if (mode === "inline") {
|
|
637
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, id: "paddle-checkout", className });
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
exports.PaddleCheckout = PaddleCheckout;
|
|
643
|
+
exports.PaymentForm = PaymentForm;
|
|
644
|
+
exports.defineConfig = defineConfig;
|
|
645
|
+
exports.definePage = definePage;
|
|
646
|
+
exports.registerIntegration = registerIntegration;
|
|
647
|
+
exports.useFunnel = useFunnel;
|
|
648
|
+
exports.useLocale = useLocale;
|
|
649
|
+
exports.useNavigation = useNavigation;
|
|
650
|
+
exports.usePayment = usePayment;
|
|
651
|
+
exports.useProducts = useProducts;
|
|
652
|
+
exports.useQueryParams = useQueryParams;
|
|
653
|
+
exports.useTracking = useTracking;
|
|
654
|
+
exports.useTranslation = useTranslation;
|
|
655
|
+
exports.useUser = useUser;
|
|
656
|
+
exports.useUserVariable = useUserVariable;
|
|
657
|
+
exports.useVariable = useVariable;
|
|
658
|
+
exports.useVariables = useVariables;
|
|
659
|
+
//# sourceMappingURL=index.cjs.map
|
|
660
|
+
//# sourceMappingURL=index.cjs.map
|