@appfunnel-dev/sdk 0.13.0 → 0.14.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.
@@ -378,7 +378,8 @@ var Router = class {
378
378
  this.initialKey = config.pages?.[initialPage] ? initialPage : defaultInitial;
379
379
  } else {
380
380
  const hasSession = this.hasSessionCookie(campaignSlug);
381
- this.initialKey = hasSession && config.pages?.[initialPage] ? initialPage : defaultInitial;
381
+ const hasPendingSession = typeof window !== "undefined" && (window.location.search.includes("customer=") || window.location.search.includes("sid="));
382
+ this.initialKey = (hasSession || hasPendingSession) && config.pages?.[initialPage] ? initialPage : defaultInitial;
382
383
  }
383
384
  } else {
384
385
  this.initialKey = initialPage || defaultInitial;
@@ -1178,6 +1179,121 @@ var I18n = class {
1178
1179
  this.listeners.forEach((l) => l());
1179
1180
  }
1180
1181
  };
1182
+
1183
+ // src/runtime/session.ts
1184
+ var API_BASE_URL2 = "https://api.appfunnel.net";
1185
+ var API_HEADERS = { "ngrok-skip-browser-warning": "1" };
1186
+ async function apiFetch(path) {
1187
+ try {
1188
+ const res = await fetch(`${API_BASE_URL2}${path}`, { headers: API_HEADERS });
1189
+ return res.ok ? res.json() : null;
1190
+ } catch {
1191
+ return null;
1192
+ }
1193
+ }
1194
+ function mergeVariables(store, data) {
1195
+ if (Object.keys(data).length === 0) return;
1196
+ store.setState((prev) => {
1197
+ const merged = { ...prev };
1198
+ for (const [key, value] of Object.entries(data)) {
1199
+ if (value !== void 0) merged[key] = value;
1200
+ }
1201
+ return merged;
1202
+ });
1203
+ }
1204
+ function cleanUrlParams(...keys) {
1205
+ const url = new URL(window.location.href);
1206
+ for (const key of keys) url.searchParams.delete(key);
1207
+ window.history.replaceState({}, "", url.toString());
1208
+ }
1209
+ async function fetchAndRestoreSession(store, sessionId, funnelId) {
1210
+ const result = await apiFetch(`/session/${sessionId}/data`);
1211
+ if (!result?.success) return;
1212
+ if (result.data?.funnelId && result.data.funnelId !== funnelId) return;
1213
+ const savedData = result.data?.data || {};
1214
+ mergeVariables(store, savedData);
1215
+ }
1216
+ async function resolveCheckoutSuccess(ctx, params) {
1217
+ const stripeSessionId = params.get("session_id");
1218
+ cleanUrlParams("checkout", "session_id");
1219
+ const sessionId = ctx.tracker.getSessionId();
1220
+ if (!sessionId) return { ok: false };
1221
+ ctx.store.setMany({
1222
+ "purchase.status": "success",
1223
+ "purchase.success": true,
1224
+ "purchase.cancelled": false
1225
+ });
1226
+ await Promise.all([
1227
+ apiFetch(
1228
+ `/campaign/${ctx.campaignId}/stripe/checkout-status?sessionId=${encodeURIComponent(sessionId)}&checkoutSessionId=${encodeURIComponent(stripeSessionId)}`
1229
+ ).then((result) => {
1230
+ if (!result?.success) return;
1231
+ const updates = {
1232
+ "payment.status": result.paymentStatus || "paid"
1233
+ };
1234
+ if (result.stripeCustomerId) {
1235
+ updates["user.stripeCustomerId"] = result.stripeCustomerId;
1236
+ }
1237
+ if (result.subscriptionId) {
1238
+ updates["stripe.subscriptionId"] = result.subscriptionId;
1239
+ updates["subscription.status"] = result.subscriptionStatus || "active";
1240
+ }
1241
+ if (result.paymentIntentId) {
1242
+ updates["stripe.paymentIntentId"] = result.paymentIntentId;
1243
+ }
1244
+ ctx.store.setMany(updates);
1245
+ ctx.tracker.track("purchase.complete", {
1246
+ eventId: stripeSessionId,
1247
+ amount: result.amountTotal ? result.amountTotal / 100 : 0,
1248
+ currency: result.currency || "usd",
1249
+ email: result.customerEmail
1250
+ });
1251
+ }),
1252
+ fetchAndRestoreSession(ctx.store, sessionId, ctx.funnelId)
1253
+ ]);
1254
+ return { ok: true };
1255
+ }
1256
+ function resolveCheckoutCanceled(ctx) {
1257
+ cleanUrlParams("checkout", "session_id");
1258
+ ctx.store.setMany({
1259
+ "purchase.status": "cancelled",
1260
+ "purchase.success": false,
1261
+ "purchase.cancelled": true
1262
+ });
1263
+ return { ok: true };
1264
+ }
1265
+ async function resolveCustomer(ctx, customerId) {
1266
+ cleanUrlParams("customer");
1267
+ const result = await apiFetch(
1268
+ `/session/by-customer?customerId=${encodeURIComponent(customerId)}&campaignId=${encodeURIComponent(ctx.campaignId)}&funnelId=${encodeURIComponent(ctx.funnelId)}`
1269
+ );
1270
+ if (!result?.success || !result.sessionId) return { ok: false };
1271
+ ctx.tracker.setSessionId(result.sessionId);
1272
+ if (result.data) {
1273
+ mergeVariables(ctx.store, result.data);
1274
+ }
1275
+ return { ok: true };
1276
+ }
1277
+ async function resolveExistingSession(ctx) {
1278
+ const sessionId = ctx.tracker.getSessionId();
1279
+ if (!sessionId) return { ok: true };
1280
+ await fetchAndRestoreSession(ctx.store, sessionId, ctx.funnelId);
1281
+ return { ok: true };
1282
+ }
1283
+ async function resolveSession(ctx, params) {
1284
+ const checkoutStatus = params.get("checkout");
1285
+ const customerId = params.get("customer");
1286
+ if (checkoutStatus === "success" && params.get("session_id")) {
1287
+ return resolveCheckoutSuccess(ctx, params);
1288
+ }
1289
+ if (checkoutStatus === "canceled") {
1290
+ return resolveCheckoutCanceled(ctx);
1291
+ }
1292
+ if (customerId) {
1293
+ return resolveCustomer(ctx, customerId);
1294
+ }
1295
+ return resolveExistingSession(ctx);
1296
+ }
1181
1297
  var FunnelContext = react.createContext(null);
1182
1298
  function useFunnelContext() {
1183
1299
  const ctx = react.useContext(FunnelContext);
@@ -1199,12 +1315,16 @@ function FunnelProvider({
1199
1315
  const campaignId = sessionData?.campaignId || config.projectId || "";
1200
1316
  const funnelId = sessionData?.funnelId || config.projectId || "";
1201
1317
  const disableLoadingScreen = config.settings?.disableLoadingScreen === true;
1318
+ const initialUrlParams = react.useRef(null);
1319
+ if (!initialUrlParams.current && typeof window !== "undefined") {
1320
+ initialUrlParams.current = new URLSearchParams(window.location.search);
1321
+ }
1202
1322
  const [isReady, setIsReady] = react.useState(() => {
1203
1323
  if (disableLoadingScreen) return true;
1204
1324
  if (sessionData?.variables) return true;
1205
1325
  if (globalThis.__APPFUNNEL_DEV__) return true;
1206
1326
  if (typeof window === "undefined") return false;
1207
- const params = new URLSearchParams(window.location.search);
1327
+ const params = initialUrlParams.current;
1208
1328
  if (!params.has("checkout") && !params.has("customer") && !params.has("sid")) {
1209
1329
  const hasCookie = document.cookie.includes("fs_");
1210
1330
  if (!hasCookie) return true;
@@ -1223,12 +1343,7 @@ function FunnelProvider({
1223
1343
  );
1224
1344
  }
1225
1345
  if (!routerRef.current) {
1226
- routerRef.current = new Router({
1227
- config,
1228
- initialPage,
1229
- basePath,
1230
- campaignSlug
1231
- });
1346
+ routerRef.current = new Router({ config, initialPage, basePath, campaignSlug });
1232
1347
  }
1233
1348
  if (!trackerRef.current) {
1234
1349
  trackerRef.current = new FunnelTracker();
@@ -1251,163 +1366,18 @@ function FunnelProvider({
1251
1366
  react.useEffect(() => {
1252
1367
  if (sessionData?.variables) return;
1253
1368
  if (globalThis.__APPFUNNEL_DEV__) return;
1254
- const API_BASE_URL2 = "https://api.appfunnel.net";
1255
- const markReady = () => setIsReady(true);
1256
- tracker.init(campaignId, funnelId, campaignSlug, sessionData?.experimentId);
1257
1369
  if (typeof window === "undefined") return;
1258
- const urlParams = new URLSearchParams(window.location.search);
1259
- const checkoutStatus = urlParams.get("checkout");
1260
- const stripeSessionId = urlParams.get("session_id");
1261
- if (checkoutStatus === "success" && stripeSessionId) {
1262
- const url = new URL(window.location.href);
1263
- url.searchParams.delete("checkout");
1264
- url.searchParams.delete("session_id");
1265
- window.history.replaceState({}, "", url.toString());
1266
- const sessionId2 = tracker.getSessionId();
1267
- if (!sessionId2) {
1268
- markReady();
1269
- return;
1370
+ tracker.init(campaignId, funnelId, campaignSlug, sessionData?.experimentId);
1371
+ const params = initialUrlParams.current || new URLSearchParams(window.location.search);
1372
+ resolveSession({ store, tracker, campaignId, funnelId }, params).then((result) => {
1373
+ if (!result.ok) {
1374
+ const initialKey = config.initialPageKey || Object.keys(config.pages ?? {})[0];
1375
+ if (initialKey) router.goToPage(initialKey);
1270
1376
  }
1271
- store.setMany({
1272
- "purchase.status": "success",
1273
- "purchase.success": true,
1274
- "purchase.cancelled": false
1275
- });
1276
- Promise.all([
1277
- // Checkout details (stripeCustomerId, subscription info, etc.)
1278
- fetch(
1279
- `${API_BASE_URL2}/campaign/${campaignId}/stripe/checkout-status?sessionId=${encodeURIComponent(sessionId2)}&checkoutSessionId=${encodeURIComponent(stripeSessionId)}`,
1280
- { headers: { "ngrok-skip-browser-warning": "1" } }
1281
- ).then((res) => res.ok ? res.json() : null).then((result) => {
1282
- if (!result?.success) return;
1283
- const updates = {};
1284
- if (result.stripeCustomerId) {
1285
- updates["user.stripeCustomerId"] = result.stripeCustomerId;
1286
- }
1287
- if (result.subscriptionId) {
1288
- updates["stripe.subscriptionId"] = result.subscriptionId;
1289
- updates["subscription.status"] = result.subscriptionStatus || "active";
1290
- }
1291
- if (result.paymentIntentId) {
1292
- updates["stripe.paymentIntentId"] = result.paymentIntentId;
1293
- }
1294
- updates["payment.status"] = result.paymentStatus || "paid";
1295
- if (Object.keys(updates).length > 0) {
1296
- store.setMany(updates);
1297
- }
1298
- tracker.track("purchase.complete", {
1299
- eventId: stripeSessionId,
1300
- amount: result.amountTotal ? result.amountTotal / 100 : 0,
1301
- currency: result.currency || "usd",
1302
- email: result.customerEmail
1303
- });
1304
- }).catch(() => {
1305
- }),
1306
- // Session variable restoration
1307
- fetch(`${API_BASE_URL2}/session/${sessionId2}/data`, {
1308
- headers: { "ngrok-skip-browser-warning": "1" }
1309
- }).then((res) => res.ok ? res.json() : null).then((result) => {
1310
- if (!result?.success) return;
1311
- if (result.data?.funnelId && result.data.funnelId !== funnelId) return;
1312
- const savedData = result.data?.data || {};
1313
- if (Object.keys(savedData).length === 0) return;
1314
- store.setState((prev) => {
1315
- const merged = { ...prev };
1316
- for (const [key, value] of Object.entries(savedData)) {
1317
- if (value !== void 0) merged[key] = value;
1318
- }
1319
- return merged;
1320
- });
1321
- }).catch(() => {
1322
- })
1323
- ]).finally(markReady);
1324
- return;
1325
- }
1326
- if (checkoutStatus === "canceled") {
1327
- const url = new URL(window.location.href);
1328
- url.searchParams.delete("checkout");
1329
- url.searchParams.delete("session_id");
1330
- window.history.replaceState({}, "", url.toString());
1331
- store.setMany({
1332
- "purchase.status": "cancelled",
1333
- "purchase.success": false,
1334
- "purchase.cancelled": true
1335
- });
1336
- markReady();
1337
- return;
1338
- }
1339
- const customerParam = urlParams.get("customer");
1340
- if (customerParam) {
1341
- const url = new URL(window.location.href);
1342
- url.searchParams.delete("customer");
1343
- window.history.replaceState({}, "", url.toString());
1344
- fetch(
1345
- `${API_BASE_URL2}/session/by-customer?customerId=${encodeURIComponent(customerParam)}&campaignId=${encodeURIComponent(campaignId)}&funnelId=${encodeURIComponent(funnelId)}`,
1346
- { headers: { "ngrok-skip-browser-warning": "1" } }
1347
- ).then((res) => res.ok ? res.json() : null).then((result) => {
1348
- if (!result?.success || !result.sessionId) return;
1349
- tracker.setSessionId(result.sessionId);
1350
- if (!result.isNew && result.data) {
1351
- const savedData = result.data;
1352
- if (Object.keys(savedData).length > 0) {
1353
- store.setState((prev) => {
1354
- const merged = { ...prev };
1355
- for (const [key, value] of Object.entries(savedData)) {
1356
- if (value !== void 0) merged[key] = value;
1357
- }
1358
- return merged;
1359
- });
1360
- }
1361
- }
1362
- }).catch(() => {
1363
- }).finally(markReady);
1364
- return;
1365
- }
1366
- const sessionId = tracker.getSessionId();
1367
- if (!sessionId) {
1368
- markReady();
1369
- return;
1370
- }
1371
- fetch(`${API_BASE_URL2}/session/${sessionId}/data`, {
1372
- headers: { "ngrok-skip-browser-warning": "1" }
1373
- }).then((res) => res.ok ? res.json() : null).then((result) => {
1374
- if (!result?.success) return;
1375
- if (result.data?.funnelId && result.data.funnelId !== funnelId) return;
1376
- const savedData = result.data?.data || {};
1377
- if (Object.keys(savedData).length === 0) return;
1378
- store.setState((prev) => {
1379
- const merged = { ...prev };
1380
- for (const [key, value] of Object.entries(savedData)) {
1381
- if (value !== void 0) merged[key] = value;
1382
- }
1383
- return merged;
1384
- });
1385
- }).catch(() => {
1386
- }).finally(markReady);
1387
- }, []);
1388
- const products = react.useMemo(() => {
1389
- if (!config.products?.items || !priceData) return [];
1390
- return buildRuntimeProducts(config.products.items, priceData);
1391
- }, [config.products, priceData]);
1392
- const defaultProductId = config.products?.defaultId || products[0]?.id || null;
1393
- const selectedProductIdRef = react.useRef(defaultProductId);
1394
- const selectProduct = react.useCallback((productId) => {
1395
- selectedProductIdRef.current = productId;
1396
- store.set("products.selectedProductId", productId);
1397
- }, [store]);
1398
- react.useEffect(() => {
1399
- const eventBus = new AppFunnelEventBus(store, router, selectProduct);
1400
- eventBus.attach();
1401
- eventBusRef.current = eventBus;
1402
- if (config.integrations && Object.keys(config.integrations).length > 0) {
1403
- initializeIntegrations(config.integrations);
1404
- }
1405
- return () => {
1406
- eventBus.destroy();
1407
- eventBusRef.current = null;
1408
- };
1377
+ }).finally(() => setIsReady(true));
1409
1378
  }, []);
1410
1379
  react.useEffect(() => {
1380
+ if (!isReady) return;
1411
1381
  tracker.init(campaignId, funnelId, campaignSlug, sessionData?.experimentId);
1412
1382
  if (sessionData?.sessionId) {
1413
1383
  tracker.setSessionId(sessionData.sessionId);
@@ -1430,11 +1400,33 @@ function FunnelProvider({
1430
1400
  tracker.stopPageTracking();
1431
1401
  tracker.flushVariables();
1432
1402
  };
1403
+ }, [isReady]);
1404
+ const products = react.useMemo(() => {
1405
+ if (!config.products?.items || !priceData) return [];
1406
+ return buildRuntimeProducts(config.products.items, priceData);
1407
+ }, [config.products, priceData]);
1408
+ const defaultProductId = config.products?.defaultId || products[0]?.id || null;
1409
+ const selectedProductIdRef = react.useRef(defaultProductId);
1410
+ const selectProduct = react.useCallback((productId) => {
1411
+ selectedProductIdRef.current = productId;
1412
+ store.set("products.selectedProductId", productId);
1413
+ }, [store]);
1414
+ react.useEffect(() => {
1415
+ const eventBus = new AppFunnelEventBus(store, router, selectProduct);
1416
+ eventBus.attach();
1417
+ eventBusRef.current = eventBus;
1418
+ if (config.integrations && Object.keys(config.integrations).length > 0) {
1419
+ initializeIntegrations(config.integrations);
1420
+ }
1421
+ return () => {
1422
+ eventBus.destroy();
1423
+ eventBusRef.current = null;
1424
+ };
1433
1425
  }, []);
1434
1426
  const sessionStartTime = react.useRef(Date.now());
1435
1427
  const pageStartTime = react.useRef(Date.now());
1436
1428
  react.useEffect(() => {
1437
- const sysVars = computeSystemVariables({
1429
+ store.setMany(computeSystemVariables({
1438
1430
  currentPageKey: router.getCurrentPage()?.key || "",
1439
1431
  pageHistory: router.getPageHistory(),
1440
1432
  pageStartTime: pageStartTime.current,
@@ -1442,8 +1434,7 @@ function FunnelProvider({
1442
1434
  totalPages: Object.keys(config.pages ?? {}).length,
1443
1435
  funnelId,
1444
1436
  campaignId
1445
- });
1446
- store.setMany(sysVars);
1437
+ }));
1447
1438
  if (defaultProductId) {
1448
1439
  store.set("products.selectedProductId", defaultProductId);
1449
1440
  }
@@ -1485,5 +1476,5 @@ exports.FunnelProvider = FunnelProvider;
1485
1476
  exports.__require = __require;
1486
1477
  exports.registerIntegration = registerIntegration;
1487
1478
  exports.useFunnelContext = useFunnelContext;
1488
- //# sourceMappingURL=chunk-KEFKKBBP.cjs.map
1489
- //# sourceMappingURL=chunk-KEFKKBBP.cjs.map
1479
+ //# sourceMappingURL=chunk-OTBIW5DN.cjs.map
1480
+ //# sourceMappingURL=chunk-OTBIW5DN.cjs.map