@virex-tech/paywallo-sdk 2.2.6 → 2.2.7

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/dist/index.mjs CHANGED
@@ -2370,8 +2370,8 @@ var init_NotificationHandlerSetup = __esm({
2370
2370
  // src/core/version.ts
2371
2371
  function resolveVersion() {
2372
2372
  try {
2373
- if ("2.2.6") {
2374
- return "2.2.6";
2373
+ if ("2.2.7") {
2374
+ return "2.2.7";
2375
2375
  }
2376
2376
  } catch {
2377
2377
  }
@@ -3041,6 +3041,41 @@ var init_emitPaywallClosed = __esm({
3041
3041
  }
3042
3042
  });
3043
3043
 
3044
+ // src/domains/paywall/extractFirstPageBackground.ts
3045
+ function extractFirstPageBackground(craftData) {
3046
+ if (!craftData) return DEFAULT_BACKGROUND;
3047
+ try {
3048
+ const data = JSON.parse(craftData);
3049
+ const nodes = { ...data.nodes, ...data.sharedNodes };
3050
+ const rootId = data.rootId;
3051
+ if (!rootId || !nodes[rootId]) return DEFAULT_BACKGROUND;
3052
+ const firstPageId = nodes[rootId].children?.[0];
3053
+ if (!firstPageId) return DEFAULT_BACKGROUND;
3054
+ const bg = nodes[firstPageId]?.props?.backgroundColor;
3055
+ return typeof bg === "string" && bg.length > 0 && bg !== "transparent" ? bg : DEFAULT_BACKGROUND;
3056
+ } catch {
3057
+ return DEFAULT_BACKGROUND;
3058
+ }
3059
+ }
3060
+ function isColorDark(color) {
3061
+ const hex = color.replace("#", "").trim();
3062
+ const normalized = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
3063
+ if (normalized.length !== 6) return false;
3064
+ const r = parseInt(normalized.slice(0, 2), 16);
3065
+ const g = parseInt(normalized.slice(2, 4), 16);
3066
+ const b = parseInt(normalized.slice(4, 6), 16);
3067
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return false;
3068
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
3069
+ return luminance < 0.5;
3070
+ }
3071
+ var DEFAULT_BACKGROUND;
3072
+ var init_extractFirstPageBackground = __esm({
3073
+ "src/domains/paywall/extractFirstPageBackground.ts"() {
3074
+ "use strict";
3075
+ DEFAULT_BACKGROUND = "#FFFFFF";
3076
+ }
3077
+ });
3078
+
3044
3079
  // src/domains/paywall/parseWebViewMessage.ts
3045
3080
  function deriveMessageId(message) {
3046
3081
  if (message.id) return message.id;
@@ -3283,6 +3318,66 @@ var init_localization = __esm({
3283
3318
  }
3284
3319
  });
3285
3320
 
3321
+ // src/domains/paywall/PaywallSkeleton.tsx
3322
+ import { useEffect, useRef } from "react";
3323
+ import { Animated, StyleSheet, View } from "react-native";
3324
+ import { jsx, jsxs } from "react/jsx-runtime";
3325
+ function SkeletonBlock({ style, color, opacity }) {
3326
+ return /* @__PURE__ */ jsx(Animated.View, { style: [style, { backgroundColor: color, opacity }] });
3327
+ }
3328
+ function PaywallSkeleton({ backgroundColor = "#FFFFFF" }) {
3329
+ const pulse = useRef(new Animated.Value(0.4)).current;
3330
+ useEffect(() => {
3331
+ const loop = Animated.loop(
3332
+ Animated.sequence([
3333
+ Animated.timing(pulse, { toValue: 1, duration: 700, useNativeDriver: true }),
3334
+ Animated.timing(pulse, { toValue: 0.4, duration: 700, useNativeDriver: true })
3335
+ ])
3336
+ );
3337
+ loop.start();
3338
+ return () => loop.stop();
3339
+ }, [pulse]);
3340
+ const placeholder = isColorDark(backgroundColor) ? "rgba(255,255,255,0.13)" : "rgba(0,0,0,0.07)";
3341
+ return /* @__PURE__ */ jsxs(View, { style: [styles.container, { backgroundColor }], pointerEvents: "none", children: [
3342
+ /* @__PURE__ */ jsx(View, { style: styles.header, children: /* @__PURE__ */ jsx(SkeletonBlock, { style: styles.logo, color: placeholder, opacity: pulse }) }),
3343
+ /* @__PURE__ */ jsx(SkeletonBlock, { style: styles.hero, color: placeholder, opacity: pulse }),
3344
+ /* @__PURE__ */ jsxs(View, { style: styles.titleGroup, children: [
3345
+ /* @__PURE__ */ jsx(SkeletonBlock, { style: styles.titleLine, color: placeholder, opacity: pulse }),
3346
+ /* @__PURE__ */ jsx(SkeletonBlock, { style: styles.titleLineShort, color: placeholder, opacity: pulse })
3347
+ ] }),
3348
+ /* @__PURE__ */ jsxs(View, { style: styles.cardsRow, children: [
3349
+ /* @__PURE__ */ jsx(SkeletonBlock, { style: styles.card, color: placeholder, opacity: pulse }),
3350
+ /* @__PURE__ */ jsx(SkeletonBlock, { style: styles.card, color: placeholder, opacity: pulse })
3351
+ ] }),
3352
+ /* @__PURE__ */ jsxs(View, { style: styles.footerGroup, children: [
3353
+ /* @__PURE__ */ jsx(SkeletonBlock, { style: styles.cta, color: placeholder, opacity: pulse }),
3354
+ /* @__PURE__ */ jsx(SkeletonBlock, { style: styles.footer, color: placeholder, opacity: pulse })
3355
+ ] })
3356
+ ] });
3357
+ }
3358
+ var RADIUS, styles;
3359
+ var init_PaywallSkeleton = __esm({
3360
+ "src/domains/paywall/PaywallSkeleton.tsx"() {
3361
+ "use strict";
3362
+ init_extractFirstPageBackground();
3363
+ RADIUS = 14;
3364
+ styles = StyleSheet.create({
3365
+ card: { borderRadius: RADIUS, flex: 1, height: 120 },
3366
+ cardsRow: { flexDirection: "row", gap: 12, marginTop: "auto", paddingHorizontal: 24 },
3367
+ container: { flex: 1, paddingBottom: 40, paddingTop: 64 },
3368
+ cta: { borderRadius: 30, height: 58, marginHorizontal: 24 },
3369
+ footer: { alignSelf: "center", borderRadius: 6, height: 12, marginTop: 20, width: 180 },
3370
+ footerGroup: { marginTop: 28 },
3371
+ header: { alignItems: "center", paddingVertical: 12 },
3372
+ hero: { alignSelf: "center", borderRadius: 20, height: 220, marginTop: 16, width: "70%" },
3373
+ logo: { borderRadius: 6, height: 22, width: 110 },
3374
+ titleGroup: { alignItems: "center", gap: 12, marginTop: 28, paddingHorizontal: 24 },
3375
+ titleLine: { borderRadius: 8, height: 26, width: "85%" },
3376
+ titleLineShort: { borderRadius: 8, height: 26, width: "60%" }
3377
+ });
3378
+ }
3379
+ });
3380
+
3286
3381
  // src/domains/paywall/paywallScripts.ts
3287
3382
  function buildPaywallInjectionScript(data) {
3288
3383
  const paywallData = {
@@ -3327,7 +3422,7 @@ var init_paywallScripts = __esm({
3327
3422
  });
3328
3423
 
3329
3424
  // src/domains/paywall/PaywallWebView.tsx
3330
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3425
+ import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState } from "react";
3331
3426
  import {
3332
3427
  BackHandler,
3333
3428
  findNodeHandle,
@@ -3336,11 +3431,11 @@ import {
3336
3431
  NativeModules as NativeModules6,
3337
3432
  Platform as Platform9,
3338
3433
  requireNativeComponent,
3339
- StyleSheet,
3434
+ StyleSheet as StyleSheet2,
3340
3435
  UIManager,
3341
- View
3436
+ View as View2
3342
3437
  } from "react-native";
3343
- import { jsx } from "react/jsx-runtime";
3438
+ import { jsx as jsx2 } from "react/jsx-runtime";
3344
3439
  function getNativeWebView() {
3345
3440
  if (NativeWebViewComponent) return NativeWebViewComponent;
3346
3441
  if (nativeWebViewLookupFailed) return null;
@@ -3398,16 +3493,16 @@ function PaywallWebView({
3398
3493
  onReady,
3399
3494
  onError
3400
3495
  }) {
3401
- const paywallDataSent = useRef(false);
3402
- const loadEndReceived = useRef(false);
3496
+ const paywallDataSent = useRef2(false);
3497
+ const loadEndReceived = useRef2(false);
3403
3498
  const [scriptToInject, setScriptToInject] = useState(void 0);
3404
- const webViewRef = useRef(null);
3405
- const onErrorRef = useRef(onError);
3499
+ const webViewRef = useRef2(null);
3500
+ const onErrorRef = useRef2(onError);
3406
3501
  onErrorRef.current = onError;
3407
- const onReadyRef = useRef(onReady);
3502
+ const onReadyRef = useRef2(onReady);
3408
3503
  onReadyRef.current = onReady;
3409
3504
  const NativeWebView = useMemo(() => getNativeWebView(), []);
3410
- useEffect(() => {
3505
+ useEffect2(() => {
3411
3506
  if (!NativeWebView) {
3412
3507
  onErrorRef.current?.({
3413
3508
  code: -1,
@@ -3418,7 +3513,7 @@ function PaywallWebView({
3418
3513
  const [resolvedWebUrl, setResolvedWebUrl] = useState(
3419
3514
  () => webUrlProp ?? PaywalloClient.getWebUrl()
3420
3515
  );
3421
- useEffect(() => {
3516
+ useEffect2(() => {
3422
3517
  if (webUrlProp !== void 0) {
3423
3518
  setResolvedWebUrl(webUrlProp ?? null);
3424
3519
  return;
@@ -3437,7 +3532,7 @@ function PaywallWebView({
3437
3532
  }, 200);
3438
3533
  return () => clearInterval(interval);
3439
3534
  }, [webUrlProp]);
3440
- useEffect(() => {
3535
+ useEffect2(() => {
3441
3536
  if (!resolvedWebUrl) {
3442
3537
  onErrorRef.current?.({
3443
3538
  code: -1,
@@ -3465,9 +3560,9 @@ function PaywallWebView({
3465
3560
  });
3466
3561
  injectScript(script);
3467
3562
  }, [craftData, products, primaryProductId, secondaryProductId, injectScript]);
3468
- const processedIdsRef = useRef(/* @__PURE__ */ new Set());
3469
- const dedupeTimersRef = useRef(/* @__PURE__ */ new Map());
3470
- useEffect(() => {
3563
+ const processedIdsRef = useRef2(/* @__PURE__ */ new Set());
3564
+ const dedupeTimersRef = useRef2(/* @__PURE__ */ new Map());
3565
+ useEffect2(() => {
3471
3566
  return () => {
3472
3567
  for (const timerId of dedupeTimersRef.current.values()) clearTimeout(timerId);
3473
3568
  dedupeTimersRef.current.clear();
@@ -3519,7 +3614,7 @@ function PaywallWebView({
3519
3614
  },
3520
3615
  [buildAndSetInjectionScript, onPurchase, onClose, onRestore]
3521
3616
  );
3522
- useEffect(() => {
3617
+ useEffect2(() => {
3523
3618
  const emitter = getWebViewEmitter();
3524
3619
  if (!emitter) return;
3525
3620
  const loadEndSub = emitter.addListener("PaywalloWebViewLoadEnd", () => {
@@ -3544,7 +3639,7 @@ function PaywallWebView({
3544
3639
  errorSub.remove();
3545
3640
  };
3546
3641
  }, [buildAndSetInjectionScript, handleParsedMessage]);
3547
- useEffect(() => {
3642
+ useEffect2(() => {
3548
3643
  const timer = setTimeout(() => {
3549
3644
  if (!paywallDataSent.current) {
3550
3645
  onErrorRef.current?.({
@@ -3584,11 +3679,11 @@ function PaywallWebView({
3584
3679
  },
3585
3680
  [handleParsedMessage]
3586
3681
  );
3587
- useEffect(() => {
3682
+ useEffect2(() => {
3588
3683
  if (!paywallDataSent.current) return;
3589
3684
  injectScript(buildPurchaseStateScript(isPurchasing));
3590
3685
  }, [isPurchasing, injectScript]);
3591
- useEffect(() => {
3686
+ useEffect2(() => {
3592
3687
  if (Platform9.OS !== "android") return;
3593
3688
  const onBackPress = () => {
3594
3689
  if (!paywallDataSent.current) return false;
@@ -3600,7 +3695,7 @@ function PaywallWebView({
3600
3695
  }, [injectScript]);
3601
3696
  if (!resolvedWebUrl || !NativeWebView) return null;
3602
3697
  const paywallUrl = `${resolvedWebUrl}/paywall/${paywallId}`;
3603
- return /* @__PURE__ */ jsx(View, { style: styles.container, children: /* @__PURE__ */ jsx(
3698
+ return /* @__PURE__ */ jsx2(View2, { style: styles2.container, children: /* @__PURE__ */ jsx2(
3604
3699
  NativeWebView,
3605
3700
  {
3606
3701
  ref: webViewRef,
@@ -3609,11 +3704,11 @@ function PaywallWebView({
3609
3704
  onMessage: handleMessage,
3610
3705
  onLoadEnd: handleLoadEnd,
3611
3706
  onError: handleNativeError,
3612
- style: styles.webview
3707
+ style: styles2.webview
3613
3708
  }
3614
3709
  ) });
3615
3710
  }
3616
- var NativeWebViewComponent, nativeWebViewLookupFailed, webViewEmitter, styles;
3711
+ var NativeWebViewComponent, nativeWebViewLookupFailed, webViewEmitter, styles2;
3617
3712
  var init_PaywallWebView = __esm({
3618
3713
  "src/domains/paywall/PaywallWebView.tsx"() {
3619
3714
  "use strict";
@@ -3623,7 +3718,7 @@ var init_PaywallWebView = __esm({
3623
3718
  NativeWebViewComponent = null;
3624
3719
  nativeWebViewLookupFailed = false;
3625
3720
  webViewEmitter = null;
3626
- styles = StyleSheet.create({
3721
+ styles2 = StyleSheet2.create({
3627
3722
  container: { flex: 1 },
3628
3723
  webview: { flex: 1, backgroundColor: "transparent" }
3629
3724
  });
@@ -5264,21 +5359,21 @@ var init_usePaywallTracking = __esm({
5264
5359
  });
5265
5360
 
5266
5361
  // src/domains/paywall/usePaywallActions.ts
5267
- import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
5268
- import { Animated, AppState as AppState3, Dimensions as Dimensions2 } from "react-native";
5362
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
5363
+ import { Animated as Animated2, AppState as AppState3, Dimensions as Dimensions2 } from "react-native";
5269
5364
  function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId, openAnim, variantId) {
5270
5365
  const [isPurchasing, setIsPurchasing] = useState2(false);
5271
- const isPurchasingRef = useRef2(false);
5366
+ const isPurchasingRef = useRef3(false);
5272
5367
  const [isClosing, setIsClosing] = useState2(false);
5273
- const slideAnim = useRef2(new Animated.Value(0)).current;
5274
- const presentedAtRef = useRef2(Date.now());
5275
- const closedRef = useRef2(false);
5368
+ const slideAnim = useRef3(new Animated2.Value(0)).current;
5369
+ const presentedAtRef = useRef3(Date.now());
5370
+ const closedRef = useRef3(false);
5276
5371
  const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
5277
5372
  const closeWithReason = useCallback3((reason, resultOverride) => {
5278
5373
  if (closedRef.current) return;
5279
5374
  closedRef.current = true;
5280
5375
  const animTarget = openAnim ?? slideAnim;
5281
- Animated.timing(animTarget, {
5376
+ Animated2.timing(animTarget, {
5282
5377
  toValue: Dimensions2.get("window").height,
5283
5378
  duration: 350,
5284
5379
  useNativeDriver: true
@@ -5298,7 +5393,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
5298
5393
  setIsClosing(true);
5299
5394
  closeWithReason("dismissed");
5300
5395
  }, [isClosing, closeWithReason]);
5301
- useEffect2(() => {
5396
+ useEffect3(() => {
5302
5397
  const sub = AppState3.addEventListener("change", (nextState) => {
5303
5398
  if (isPurchasingRef.current) return;
5304
5399
  if (nextState === "background" || nextState === "inactive") {
@@ -5307,7 +5402,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
5307
5402
  });
5308
5403
  return () => sub.remove();
5309
5404
  }, [closeWithReason]);
5310
- useEffect2(() => {
5405
+ useEffect3(() => {
5311
5406
  const timer = setTimeout(() => {
5312
5407
  closeWithReason("timeout");
5313
5408
  }, PAYWALL_TIMEOUT_MS);
@@ -5378,17 +5473,17 @@ var init_usePaywallActions = __esm({
5378
5473
  });
5379
5474
 
5380
5475
  // src/domains/paywall/usePaywallLoader.ts
5381
- import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
5476
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
5382
5477
  function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts, variantKey, campaignId, variantId) {
5383
5478
  const [error, setError] = useState3(null);
5384
5479
  const [paywallConfig, setPaywallConfig] = useState3(null);
5385
5480
  const [products, setProducts] = useState3(/* @__PURE__ */ new Map());
5386
- const preloadedConfigRef = useRef3(preloadedConfig);
5481
+ const preloadedConfigRef = useRef4(preloadedConfig);
5387
5482
  preloadedConfigRef.current = preloadedConfig;
5388
- const preloadedProductsRef = useRef3(preloadedProducts);
5483
+ const preloadedProductsRef = useRef4(preloadedProducts);
5389
5484
  preloadedProductsRef.current = preloadedProducts;
5390
- const trackingDoneRef = useRef3(false);
5391
- useEffect3(() => {
5485
+ const trackingDoneRef = useRef4(false);
5486
+ useEffect4(() => {
5392
5487
  if (!visible) {
5393
5488
  trackingDoneRef.current = false;
5394
5489
  return;
@@ -5483,28 +5578,28 @@ var init_usePaywallLoader = __esm({
5483
5578
  });
5484
5579
 
5485
5580
  // src/domains/paywall/PaywallModal.tsx
5486
- import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
5581
+ import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
5487
5582
  import {
5488
- Animated as Animated2,
5583
+ Animated as Animated3,
5489
5584
  Dimensions as Dimensions3,
5490
5585
  Easing,
5491
5586
  Modal,
5492
5587
  Pressable,
5493
- StyleSheet as StyleSheet2,
5588
+ StyleSheet as StyleSheet3,
5494
5589
  Text,
5495
- View as View2
5590
+ View as View3
5496
5591
  } from "react-native";
5497
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
5592
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
5498
5593
  function ErrorFallback({ description, onRetry, onClose }) {
5499
5594
  const overrides = PaywalloClient.getConfig()?.errorStrings;
5500
5595
  const title = overrides?.title ?? getSdkString("paywallErrorTitle");
5501
5596
  const retryLabel = overrides?.retry ?? getSdkString("paywallRetryButton");
5502
5597
  const closeLabel = overrides?.close ?? getSdkString("paywallCloseButton");
5503
- return /* @__PURE__ */ jsx2(View2, { style: styles2.fallbackContainer, children: /* @__PURE__ */ jsxs(View2, { style: styles2.fallbackCard, children: [
5504
- /* @__PURE__ */ jsx2(Text, { style: styles2.fallbackTitle, children: title }),
5505
- /* @__PURE__ */ jsx2(Text, { style: styles2.fallbackDescription, children: description }),
5506
- /* @__PURE__ */ jsx2(Pressable, { style: styles2.retryButton, onPress: onRetry, children: /* @__PURE__ */ jsx2(Text, { style: styles2.retryButtonText, children: retryLabel }) }),
5507
- /* @__PURE__ */ jsx2(Pressable, { style: styles2.closeButton, onPress: onClose, children: /* @__PURE__ */ jsx2(Text, { style: styles2.closeButtonText, children: closeLabel }) })
5598
+ return /* @__PURE__ */ jsx3(View3, { style: styles3.fallbackContainer, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.fallbackCard, children: [
5599
+ /* @__PURE__ */ jsx3(Text, { style: styles3.fallbackTitle, children: title }),
5600
+ /* @__PURE__ */ jsx3(Text, { style: styles3.fallbackDescription, children: description }),
5601
+ /* @__PURE__ */ jsx3(Pressable, { style: styles3.retryButton, onPress: onRetry, children: /* @__PURE__ */ jsx3(Text, { style: styles3.retryButtonText, children: retryLabel }) }),
5602
+ /* @__PURE__ */ jsx3(Pressable, { style: styles3.closeButton, onPress: onClose, children: /* @__PURE__ */ jsx3(Text, { style: styles3.closeButtonText, children: closeLabel }) })
5508
5603
  ] }) });
5509
5604
  }
5510
5605
  function PaywallModal({
@@ -5527,7 +5622,7 @@ function PaywallModal({
5527
5622
  variantId
5528
5623
  );
5529
5624
  const screenHeight = Dimensions3.get("window").height;
5530
- const openAnim = useRef4(new Animated2.Value(screenHeight)).current;
5625
+ const openAnim = useRef5(new Animated3.Value(screenHeight)).current;
5531
5626
  const [modalVisible, setModalVisible] = useState4(false);
5532
5627
  const handleResult = useCallback4((result) => {
5533
5628
  setModalVisible(false);
@@ -5536,7 +5631,9 @@ function PaywallModal({
5536
5631
  const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, handleResult, variantKey, campaignId, openAnim, variantId);
5537
5632
  const [webViewError, setWebViewError] = useState4(null);
5538
5633
  const [retryCount, setRetryCount] = useState4(0);
5539
- const retryCountRef = useRef4(0);
5634
+ const [showSkeleton, setShowSkeleton] = useState4(true);
5635
+ const skeletonOpacity = useRef5(new Animated3.Value(1)).current;
5636
+ const retryCountRef = useRef5(0);
5540
5637
  const handleWebViewError = useCallback4(
5541
5638
  (err) => {
5542
5639
  if (retryCountRef.current >= 1) {
@@ -5552,21 +5649,39 @@ function PaywallModal({
5552
5649
  retryCountRef.current = 0;
5553
5650
  setWebViewError(null);
5554
5651
  setRetryCount(0);
5555
- }, []);
5556
- useEffect4(() => {
5652
+ setShowSkeleton(true);
5653
+ skeletonOpacity.setValue(1);
5654
+ }, [skeletonOpacity]);
5655
+ const handleWebViewReady = useCallback4(() => {
5656
+ Animated3.timing(skeletonOpacity, {
5657
+ toValue: 0,
5658
+ duration: 250,
5659
+ easing: Easing.out(Easing.cubic),
5660
+ useNativeDriver: true
5661
+ }).start(() => setShowSkeleton(false));
5662
+ }, [skeletonOpacity]);
5663
+ useEffect5(() => {
5664
+ if (!visible || !showSkeleton) return;
5665
+ const timer = setTimeout(handleWebViewReady, 8e3);
5666
+ return () => clearTimeout(timer);
5667
+ }, [visible, showSkeleton, handleWebViewReady]);
5668
+ useEffect5(() => {
5557
5669
  if (visible) {
5558
5670
  setModalVisible(true);
5671
+ setShowSkeleton(true);
5672
+ skeletonOpacity.setValue(1);
5559
5673
  openAnim.setValue(Dimensions3.get("window").height);
5560
- Animated2.timing(openAnim, {
5674
+ Animated3.timing(openAnim, {
5561
5675
  toValue: 0,
5562
5676
  duration: 550,
5563
5677
  easing: Easing.out(Easing.cubic),
5564
5678
  useNativeDriver: true
5565
5679
  }).start();
5566
5680
  }
5567
- }, [visible, openAnim]);
5681
+ }, [visible, openAnim, skeletonOpacity]);
5568
5682
  if (!modalVisible && !visible) return null;
5569
- return /* @__PURE__ */ jsx2(
5683
+ const skeletonBackground = paywallConfig && "craftData" in paywallConfig.config ? extractFirstPageBackground(paywallConfig.config.craftData) : "#FFFFFF";
5684
+ return /* @__PURE__ */ jsx3(
5570
5685
  Modal,
5571
5686
  {
5572
5687
  visible: modalVisible,
@@ -5575,16 +5690,16 @@ function PaywallModal({
5575
5690
  onRequestClose: handleClose,
5576
5691
  transparent: true,
5577
5692
  statusBarTranslucent: true,
5578
- children: /* @__PURE__ */ jsx2(View2, { style: styles2.overlay, children: /* @__PURE__ */ jsx2(Animated2.View, { style: [styles2.animatedContainer, { transform: [{ translateY: openAnim }] }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.container, children: [
5579
- error && /* @__PURE__ */ jsx2(View2, { style: styles2.errorContainer, children: /* @__PURE__ */ jsx2(Text, { style: styles2.errorText, children: error.message }) }),
5580
- !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ jsx2(Fragment, { children: webViewError ? /* @__PURE__ */ jsx2(
5693
+ children: /* @__PURE__ */ jsx3(View3, { style: styles3.overlay, children: /* @__PURE__ */ jsx3(Animated3.View, { style: [styles3.animatedContainer, { transform: [{ translateY: openAnim }] }], children: /* @__PURE__ */ jsxs2(View3, { style: styles3.container, children: [
5694
+ error && /* @__PURE__ */ jsx3(View3, { style: styles3.errorContainer, children: /* @__PURE__ */ jsx3(Text, { style: styles3.errorText, children: error.message }) }),
5695
+ !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ jsx3(Fragment, { children: webViewError ? /* @__PURE__ */ jsx3(
5581
5696
  ErrorFallback,
5582
5697
  {
5583
5698
  description: webViewError.description,
5584
5699
  onRetry: handleRetry,
5585
5700
  onClose: handleClose
5586
5701
  }
5587
- ) : /* @__PURE__ */ jsx2(
5702
+ ) : /* @__PURE__ */ jsx3(
5588
5703
  PaywallWebView,
5589
5704
  {
5590
5705
  paywallId: paywallConfig.id,
@@ -5593,27 +5708,38 @@ function PaywallModal({
5593
5708
  primaryProductId: paywallConfig.primaryProductId,
5594
5709
  secondaryProductId: paywallConfig.secondaryProductId,
5595
5710
  isPurchasing,
5711
+ onReady: handleWebViewReady,
5596
5712
  onClose: handleClose,
5597
5713
  onPurchase: handlePurchase,
5598
5714
  onRestore: handleRestore,
5599
5715
  onError: handleWebViewError
5600
5716
  },
5601
5717
  retryCount
5602
- ) })
5718
+ ) }),
5719
+ showSkeleton && !error && !webViewError && /* @__PURE__ */ jsx3(
5720
+ Animated3.View,
5721
+ {
5722
+ style: [StyleSheet3.absoluteFill, { opacity: skeletonOpacity }],
5723
+ pointerEvents: "none",
5724
+ children: /* @__PURE__ */ jsx3(PaywallSkeleton, { backgroundColor: skeletonBackground })
5725
+ }
5726
+ )
5603
5727
  ] }) }) })
5604
5728
  }
5605
5729
  );
5606
5730
  }
5607
- var styles2;
5731
+ var styles3;
5608
5732
  var init_PaywallModal = __esm({
5609
5733
  "src/domains/paywall/PaywallModal.tsx"() {
5610
5734
  "use strict";
5611
5735
  init_PaywalloClient();
5612
5736
  init_localization();
5737
+ init_extractFirstPageBackground();
5738
+ init_PaywallSkeleton();
5613
5739
  init_PaywallWebView();
5614
5740
  init_usePaywallActions();
5615
5741
  init_usePaywallLoader();
5616
- styles2 = StyleSheet2.create({
5742
+ styles3 = StyleSheet3.create({
5617
5743
  overlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.3)" },
5618
5744
  animatedContainer: { flex: 1, backgroundColor: "#fff" },
5619
5745
  container: { flex: 1, backgroundColor: "#fff" },
@@ -5669,13 +5795,176 @@ var init_PaywallModal = __esm({
5669
5795
  }
5670
5796
  });
5671
5797
 
5798
+ // src/domains/paywall/PaywallPreloadService.ts
5799
+ var PRELOAD_TTL_MS, STALE_THRESHOLD, PaywallPreloadService, paywallPreloadService;
5800
+ var init_PaywallPreloadService = __esm({
5801
+ "src/domains/paywall/PaywallPreloadService.ts"() {
5802
+ "use strict";
5803
+ init_PaywalloClient();
5804
+ init_IAPService();
5805
+ init_PaywallError();
5806
+ PRELOAD_TTL_MS = 5 * 60 * 1e3;
5807
+ STALE_THRESHOLD = 0.75;
5808
+ PaywallPreloadService = class {
5809
+ constructor() {
5810
+ this.preloaded = /* @__PURE__ */ new Map();
5811
+ this.activePreloads = /* @__PURE__ */ new Map();
5812
+ this.apiClientRef = null;
5813
+ }
5814
+ async preload(apiClient, placement) {
5815
+ this.apiClientRef = apiClient;
5816
+ const existing = this.activePreloads.get(placement);
5817
+ if (existing) return existing;
5818
+ const promise = this.doPreload(apiClient, placement);
5819
+ this.activePreloads.set(placement, promise);
5820
+ try {
5821
+ return await promise;
5822
+ } finally {
5823
+ this.activePreloads.delete(placement);
5824
+ }
5825
+ }
5826
+ async preloadMany(apiClient, placements) {
5827
+ this.apiClientRef = apiClient;
5828
+ if (placements.length === 0) return;
5829
+ const debug = PaywalloClient.getConfig()?.debug;
5830
+ if (debug) console.log(`[Paywallo PRELOAD] paywalls: ${placements.length} placements`);
5831
+ for (let i = 0; i < placements.length; i++) {
5832
+ if (i > 0) await new Promise((r) => setTimeout(r, 200));
5833
+ void this.preload(apiClient, placements[i]).catch((err) => {
5834
+ if (debug) console.log(`[Paywallo PRELOAD] paywall failed ${placements[i]}:`, err);
5835
+ });
5836
+ }
5837
+ }
5838
+ getPreloaded(placement) {
5839
+ const cached2 = this.preloaded.get(placement);
5840
+ if (!cached2) return null;
5841
+ const age = Date.now() - cached2.preloadedAt;
5842
+ if (age >= PRELOAD_TTL_MS) {
5843
+ this.preloaded.delete(placement);
5844
+ return null;
5845
+ }
5846
+ if (age >= PRELOAD_TTL_MS * STALE_THRESHOLD && this.apiClientRef) {
5847
+ void this.preload(this.apiClientRef, placement).catch(() => {
5848
+ });
5849
+ }
5850
+ return { config: cached2.config, products: new Map(cached2.products) };
5851
+ }
5852
+ isPreloaded(placement) {
5853
+ return this.getPreloaded(placement) !== null;
5854
+ }
5855
+ clear() {
5856
+ this.preloaded.clear();
5857
+ this.activePreloads.clear();
5858
+ }
5859
+ async doPreload(apiClient, placement) {
5860
+ try {
5861
+ const config = await apiClient.getPaywall(placement);
5862
+ if (!config) {
5863
+ return {
5864
+ success: false,
5865
+ error: new PaywallError(
5866
+ PAYWALL_ERROR_CODES.NOT_FOUND,
5867
+ `Paywall not found for placement: ${placement}`
5868
+ )
5869
+ };
5870
+ }
5871
+ const products = await this.loadProductsFor(config);
5872
+ this.preloaded.set(placement, { config, products, preloadedAt: Date.now() });
5873
+ if (PaywalloClient.getConfig()?.debug) {
5874
+ console.log(`[Paywallo PRELOAD] paywall cached \u2014 placement: ${placement}`);
5875
+ }
5876
+ return { success: true };
5877
+ } catch (error) {
5878
+ return {
5879
+ success: false,
5880
+ error: error instanceof Error ? error : new PaywallError(PAYWALL_ERROR_CODES.LOAD_FAILED, String(error))
5881
+ };
5882
+ }
5883
+ }
5884
+ async loadProductsFor(config) {
5885
+ const productIds = [];
5886
+ if (config.primaryProductId) productIds.push(config.primaryProductId);
5887
+ if (config.secondaryProductId) productIds.push(config.secondaryProductId);
5888
+ if (productIds.length === 0) return /* @__PURE__ */ new Map();
5889
+ try {
5890
+ const loaded = await getIAPService().loadProducts(productIds);
5891
+ const map = /* @__PURE__ */ new Map();
5892
+ for (const p of loaded) map.set(p.productId, p);
5893
+ return map;
5894
+ } catch (err) {
5895
+ if (PaywalloClient.getConfig()?.debug) {
5896
+ console.log("[Paywallo PRELOAD] products failed, caching config only:", err);
5897
+ }
5898
+ return /* @__PURE__ */ new Map();
5899
+ }
5900
+ }
5901
+ };
5902
+ paywallPreloadService = new PaywallPreloadService();
5903
+ }
5904
+ });
5905
+
5906
+ // src/domains/paywall/PaywallWebViewPrewarmer.tsx
5907
+ import { useEffect as useEffect6, useMemo as useMemo2, useState as useState5 } from "react";
5908
+ import { requireNativeComponent as requireNativeComponent2, StyleSheet as StyleSheet4, View as View4 } from "react-native";
5909
+ import { jsx as jsx4 } from "react/jsx-runtime";
5910
+ function getNativeWebView2() {
5911
+ if (NativeWebViewComponent2) return NativeWebViewComponent2;
5912
+ if (nativeLookupFailed) return null;
5913
+ try {
5914
+ NativeWebViewComponent2 = requireNativeComponent2("PaywalloWebView");
5915
+ return NativeWebViewComponent2;
5916
+ } catch {
5917
+ nativeLookupFailed = true;
5918
+ return null;
5919
+ }
5920
+ }
5921
+ function PaywallWebViewPrewarmer({
5922
+ webUrl,
5923
+ durationMs = DEFAULT_DURATION_MS
5924
+ }) {
5925
+ const NativeWebView = useMemo2(() => getNativeWebView2(), []);
5926
+ const [mounted, setMounted] = useState5(true);
5927
+ useEffect6(() => {
5928
+ if (!NativeWebView) return;
5929
+ const timer = setTimeout(() => setMounted(false), durationMs);
5930
+ return () => clearTimeout(timer);
5931
+ }, [NativeWebView, durationMs]);
5932
+ if (!NativeWebView || !mounted) return null;
5933
+ const preheatUrl = `${webUrl}/paywall/preheat`;
5934
+ if (PaywalloClient.getConfig()?.debug) {
5935
+ console.log(`[Paywallo PREWARM] mounting hidden WebView: ${preheatUrl}`);
5936
+ }
5937
+ return /* @__PURE__ */ jsx4(View4, { pointerEvents: "none", style: styles4.hidden, children: /* @__PURE__ */ jsx4(NativeWebView, { sourceUrl: preheatUrl, style: styles4.fill }) });
5938
+ }
5939
+ var NativeWebViewComponent2, nativeLookupFailed, DEFAULT_DURATION_MS, styles4;
5940
+ var init_PaywallWebViewPrewarmer = __esm({
5941
+ "src/domains/paywall/PaywallWebViewPrewarmer.tsx"() {
5942
+ "use strict";
5943
+ init_PaywalloClient();
5944
+ NativeWebViewComponent2 = null;
5945
+ nativeLookupFailed = false;
5946
+ DEFAULT_DURATION_MS = 60 * 1e3;
5947
+ styles4 = StyleSheet4.create({
5948
+ hidden: {
5949
+ height: 1,
5950
+ left: -9999,
5951
+ opacity: 0,
5952
+ position: "absolute",
5953
+ top: -9999,
5954
+ width: 1
5955
+ },
5956
+ fill: { flex: 1 }
5957
+ });
5958
+ }
5959
+ });
5960
+
5672
5961
  // src/domains/paywall/usePaywallSession.ts
5673
- import { useCallback as useCallback5, useEffect as useEffect5, useRef as useRef5 } from "react";
5962
+ import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef6 } from "react";
5674
5963
  function usePaywallSession(options) {
5675
- const closedRef = useRef5(false);
5676
- const optsRef = useRef5(options);
5964
+ const closedRef = useRef6(false);
5965
+ const optsRef = useRef6(options);
5677
5966
  optsRef.current = options;
5678
- useEffect5(() => {
5967
+ useEffect7(() => {
5679
5968
  if (!options) return;
5680
5969
  closedRef.current = false;
5681
5970
  startHeartbeat({
@@ -5719,11 +6008,11 @@ var init_usePaywallSession = __esm({
5719
6008
  });
5720
6009
 
5721
6010
  // src/domains/paywall/usePaywallPresenter.ts
5722
- import { useCallback as useCallback6, useEffect as useEffect6, useRef as useRef6, useState as useState5 } from "react";
6011
+ import { useCallback as useCallback6, useEffect as useEffect8, useRef as useRef7, useState as useState6 } from "react";
5723
6012
  function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
5724
- const [showingPreloadedWebView, setShowingPreloadedWebView] = useState5(false);
5725
- const resolverRef = useRef6(null);
5726
- const presentedAtRef = useRef6(Date.now());
6013
+ const [showingPreloadedWebView, setShowingPreloadedWebView] = useState6(false);
6014
+ const resolverRef = useRef7(null);
6015
+ const presentedAtRef = useRef7(Date.now());
5727
6016
  const sessionOpts = showingPreloadedWebView && preloadedWebView ? {
5728
6017
  placement: preloadedWebView.placement,
5729
6018
  paywallId: preloadedWebView.paywallId,
@@ -5733,7 +6022,7 @@ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
5733
6022
  ...preloadedWebView.campaignId && { campaignId: preloadedWebView.campaignId }
5734
6023
  } : null;
5735
6024
  const { markClosed } = usePaywallSession(sessionOpts);
5736
- useEffect6(() => {
6025
+ useEffect8(() => {
5737
6026
  return () => {
5738
6027
  if (resolverRef.current) {
5739
6028
  resolverRef.current({ presented: false, purchased: false, cancelled: true, restored: false });
@@ -5820,7 +6109,9 @@ var init_paywall = __esm({
5820
6109
  init_PaywallError();
5821
6110
  init_PaywallErrorBoundary();
5822
6111
  init_PaywallModal();
6112
+ init_PaywallPreloadService();
5823
6113
  init_PaywallWebView();
6114
+ init_PaywallWebViewPrewarmer();
5824
6115
  init_usePaywallPresenter();
5825
6116
  init_usePaywallTracking();
5826
6117
  }
@@ -8392,6 +8683,7 @@ async function doFullReset(state) {
8392
8683
  offlineQueue.dispose();
8393
8684
  networkMonitor.dispose();
8394
8685
  campaignGateService.clearPreloaded();
8686
+ paywallPreloadService.clear();
8395
8687
  await notificationsManager.invalidateLocalToken().catch(() => void 0);
8396
8688
  notificationsManager.destroy();
8397
8689
  deepLinkAttributionCapture.stop();
@@ -8426,6 +8718,7 @@ var init_PaywalloLifecycle = __esm({
8426
8718
  init_identity();
8427
8719
  init_DeepLinkAttributionCapture();
8428
8720
  init_notifications();
8721
+ init_paywall();
8429
8722
  init_session();
8430
8723
  init_SubscriptionCache();
8431
8724
  init_network();
@@ -8729,6 +9022,15 @@ var init_PaywalloClient = __esm({
8729
9022
  isPreloaded(placement) {
8730
9023
  return campaignGateService.getPreloadedCampaign(placement) !== null;
8731
9024
  }
9025
+ async preloadPaywall(placement) {
9026
+ return paywallPreloadService.preload(this.getApiClientOrThrow(), placement);
9027
+ }
9028
+ async preloadPaywalls(placements) {
9029
+ return paywallPreloadService.preloadMany(this.getApiClientOrThrow(), placements);
9030
+ }
9031
+ isPaywallPreloaded(placement) {
9032
+ return paywallPreloadService.isPreloaded(placement);
9033
+ }
8732
9034
  getAutoPreloadedPlacement() {
8733
9035
  return this.state.autoPreloadedPlacement;
8734
9036
  }
@@ -8943,7 +9245,7 @@ init_session();
8943
9245
  init_PaywalloError();
8944
9246
 
8945
9247
  // src/hooks/useOfferingPurchase.ts
8946
- import { useCallback as useCallback7, useState as useState6 } from "react";
9248
+ import { useCallback as useCallback7, useState as useState7 } from "react";
8947
9249
 
8948
9250
  // src/domains/plan/invalidateCache.ts
8949
9251
  async function invalidatePlanCache() {
@@ -8956,7 +9258,7 @@ async function invalidatePlanCache() {
8956
9258
 
8957
9259
  // src/hooks/useOfferingPurchase.ts
8958
9260
  function useOfferingPurchase() {
8959
- const [state, setState] = useState6("idle");
9261
+ const [state, setState] = useState7("idle");
8960
9262
  const purchase = useCallback7(async (productId) => {
8961
9263
  setState("purchasing");
8962
9264
  try {
@@ -8981,7 +9283,7 @@ function useOfferingPurchase() {
8981
9283
  }
8982
9284
 
8983
9285
  // src/hooks/useOfferings.ts
8984
- import { useCallback as useCallback8, useEffect as useEffect7, useState as useState7 } from "react";
9286
+ import { useCallback as useCallback8, useEffect as useEffect9, useState as useState8 } from "react";
8985
9287
  async function getOfferingService() {
8986
9288
  try {
8987
9289
  const mod = await Promise.resolve().then(() => (init_OfferingService(), OfferingService_exports));
@@ -8991,8 +9293,8 @@ async function getOfferingService() {
8991
9293
  }
8992
9294
  }
8993
9295
  function useOfferings(ids) {
8994
- const [offerings, setOfferings] = useState7([]);
8995
- const [isLoading, setIsLoading] = useState7(true);
9296
+ const [offerings, setOfferings] = useState8([]);
9297
+ const [isLoading, setIsLoading] = useState8(true);
8996
9298
  const idsKey = ids?.join(",") ?? "";
8997
9299
  const fetchOfferings = useCallback8(async () => {
8998
9300
  setIsLoading(true);
@@ -9010,7 +9312,7 @@ function useOfferings(ids) {
9010
9312
  setIsLoading(false);
9011
9313
  }
9012
9314
  }, [idsKey]);
9013
- useEffect7(() => {
9315
+ useEffect9(() => {
9014
9316
  void fetchOfferings();
9015
9317
  }, [fetchOfferings]);
9016
9318
  return { offerings, isLoading };
@@ -9047,10 +9349,10 @@ function usePaywallo() {
9047
9349
  }
9048
9350
 
9049
9351
  // src/hooks/usePlanPurchase.ts
9050
- import { useCallback as useCallback10, useState as useState8 } from "react";
9352
+ import { useCallback as useCallback10, useState as useState9 } from "react";
9051
9353
  function usePlanPurchase() {
9052
- const [state, setState] = useState8("idle");
9053
- const [error, setError] = useState8(null);
9354
+ const [state, setState] = useState9("idle");
9355
+ const [error, setError] = useState9(null);
9054
9356
  const purchase = useCallback10(async (productId) => {
9055
9357
  setState("purchasing");
9056
9358
  setError(null);
@@ -9103,7 +9405,7 @@ function usePlanPurchase() {
9103
9405
  }
9104
9406
 
9105
9407
  // src/hooks/usePlans.ts
9106
- import { useCallback as useCallback11, useEffect as useEffect8, useState as useState9 } from "react";
9408
+ import { useCallback as useCallback11, useEffect as useEffect10, useState as useState10 } from "react";
9107
9409
  async function getPlanService() {
9108
9410
  try {
9109
9411
  const mod = await Promise.resolve().then(() => (init_PlanService(), PlanService_exports));
@@ -9113,10 +9415,10 @@ async function getPlanService() {
9113
9415
  }
9114
9416
  }
9115
9417
  function usePlans() {
9116
- const [allPlans, setAllPlans] = useState9([]);
9117
- const [currentPlan, setCurrentPlan] = useState9(null);
9118
- const [isLoading, setIsLoading] = useState9(true);
9119
- const [error, setError] = useState9(null);
9418
+ const [allPlans, setAllPlans] = useState10([]);
9419
+ const [currentPlan, setCurrentPlan] = useState10(null);
9420
+ const [isLoading, setIsLoading] = useState10(true);
9421
+ const [error, setError] = useState10(null);
9120
9422
  const fetchPlans = useCallback11(async (forceRefresh = false) => {
9121
9423
  setIsLoading(true);
9122
9424
  setError(null);
@@ -9141,14 +9443,14 @@ function usePlans() {
9141
9443
  const refresh = useCallback11(() => {
9142
9444
  return fetchPlans(true);
9143
9445
  }, [fetchPlans]);
9144
- useEffect8(() => {
9446
+ useEffect10(() => {
9145
9447
  void fetchPlans(false);
9146
9448
  }, [fetchPlans]);
9147
9449
  return { currentPlan, allPlans, isLoading, error, refresh };
9148
9450
  }
9149
9451
 
9150
9452
  // src/hooks/useProducts.ts
9151
- import { useCallback as useCallback12, useEffect as useEffect9, useState as useState10 } from "react";
9453
+ import { useCallback as useCallback12, useEffect as useEffect11, useState as useState11 } from "react";
9152
9454
 
9153
9455
  // src/utils/productMappers.ts
9154
9456
  function mapToIAPProduct(product) {
@@ -9187,10 +9489,10 @@ function mapToFormattedProduct(product) {
9187
9489
 
9188
9490
  // src/hooks/useProducts.ts
9189
9491
  function useProducts(initialProductIds) {
9190
- const [products, setProducts] = useState10([]);
9191
- const [formattedProducts, setFormattedProducts] = useState10([]);
9192
- const [isLoading, setIsLoading] = useState10(false);
9193
- const [error, setError] = useState10(null);
9492
+ const [products, setProducts] = useState11([]);
9493
+ const [formattedProducts, setFormattedProducts] = useState11([]);
9494
+ const [isLoading, setIsLoading] = useState11(false);
9495
+ const [error, setError] = useState11(null);
9194
9496
  const loadProducts = useCallback12(async (productIds) => {
9195
9497
  setIsLoading(true);
9196
9498
  setError(null);
@@ -9205,7 +9507,7 @@ function useProducts(initialProductIds) {
9205
9507
  setIsLoading(false);
9206
9508
  }
9207
9509
  }, []);
9208
- useEffect9(() => {
9510
+ useEffect11(() => {
9209
9511
  if (initialProductIds && initialProductIds.length > 0) {
9210
9512
  void loadProducts(initialProductIds);
9211
9513
  }
@@ -9251,13 +9553,13 @@ function useProducts(initialProductIds) {
9251
9553
  }
9252
9554
 
9253
9555
  // src/hooks/usePurchase.ts
9254
- import { useCallback as useCallback13, useState as useState11 } from "react";
9556
+ import { useCallback as useCallback13, useState as useState12 } from "react";
9255
9557
  function toIAPPurchase(p) {
9256
9558
  return { productId: p.productId, transactionId: p.transactionId, transactionDate: p.transactionDate, transactionReceipt: p.receipt };
9257
9559
  }
9258
9560
  function usePurchase() {
9259
- const [state, setState] = useState11("idle");
9260
- const [error, setError] = useState11(null);
9561
+ const [state, setState] = useState12("idle");
9562
+ const [error, setError] = useState12(null);
9261
9563
  const purchase = useCallback13(async (productId) => {
9262
9564
  setState("purchasing");
9263
9565
  setError(null);
@@ -9305,13 +9607,13 @@ function usePurchase() {
9305
9607
  }
9306
9608
 
9307
9609
  // src/hooks/useSubscription.ts
9308
- import { useCallback as useCallback14, useEffect as useEffect10, useState as useState12 } from "react";
9610
+ import { useCallback as useCallback14, useEffect as useEffect12, useState as useState13 } from "react";
9309
9611
  init_SubscriptionManager();
9310
9612
  var EMPTY_ENTITLEMENTS = [];
9311
9613
  function useSubscription() {
9312
- const [status, setStatus] = useState12(null);
9313
- const [isLoading, setIsLoading] = useState12(true);
9314
- const [error, setError] = useState12(null);
9614
+ const [status, setStatus] = useState13(null);
9615
+ const [isLoading, setIsLoading] = useState13(true);
9616
+ const [error, setError] = useState13(null);
9315
9617
  const refresh = useCallback14(async () => {
9316
9618
  setIsLoading(true);
9317
9619
  setError(null);
@@ -9338,7 +9640,7 @@ function useSubscription() {
9338
9640
  setIsLoading(false);
9339
9641
  }
9340
9642
  }, []);
9341
- useEffect10(() => {
9643
+ useEffect12(() => {
9342
9644
  void subscriptionManager.getSubscriptionStatus().then((s) => {
9343
9645
  setStatus(s);
9344
9646
  setIsLoading(false);
@@ -9366,94 +9668,288 @@ function useSubscription() {
9366
9668
  }
9367
9669
 
9368
9670
  // src/PaywalloProvider.tsx
9369
- import { useCallback as useCallback19, useEffect as useEffect12, useLayoutEffect, useMemo as useMemo2, useRef as useRef10, useState as useState16 } from "react";
9370
- import { Animated as Animated3, Dimensions as Dimensions5, Easing as Easing2, StyleSheet as StyleSheet3 } from "react-native";
9671
+ import { useCallback as useCallback19, useEffect as useEffect14, useLayoutEffect, useMemo as useMemo3, useRef as useRef11, useState as useState17 } from "react";
9672
+ import { Animated as Animated4, Dimensions as Dimensions5, Easing as Easing2, StyleSheet as StyleSheet5 } from "react-native";
9371
9673
  init_PaywalloClient();
9372
- init_SecureStorage();
9373
9674
  init_NativeStorage();
9675
+ init_SecureStorage();
9374
9676
  init_campaign();
9375
9677
  init_paywall();
9376
9678
  init_paywall();
9377
9679
  init_paywall();
9378
9680
  init_paywall();
9379
- init_session();
9380
- init_ClientError();
9681
+ init_paywall();
9682
+ init_paywall();
9381
9683
 
9382
- // src/hooks/useAppAutoEvents.ts
9383
- import { useEffect as useEffect11, useRef as useRef7 } from "react";
9684
+ // src/domains/paywall/SuperwallAutoBridge.ts
9685
+ init_PaywalloClient();
9384
9686
 
9385
- // src/utils/platformDetection.ts
9386
- async function detectPlatform3() {
9387
- try {
9388
- const rn = await import("react-native");
9389
- return rn.Platform?.OS === "ios" ? "ios" : "android";
9390
- } catch {
9391
- return "android";
9687
+ // src/core/utils/eventId.ts
9688
+ function hashString(input) {
9689
+ let h1 = 3735928559;
9690
+ let h2 = 1103547991;
9691
+ for (let i = 0; i < input.length; i++) {
9692
+ const ch = input.charCodeAt(i);
9693
+ h1 = Math.imul(h1 ^ ch, 2654435769);
9694
+ h2 = Math.imul(h2 ^ ch, 1598716949);
9392
9695
  }
9696
+ h1 = Math.imul(h1 ^ h1 >>> 16, 73244475);
9697
+ h1 ^= Math.imul(h2 ^ h2 >>> 13, 351452291);
9698
+ h2 = Math.imul(h2 ^ h2 >>> 16, 73244475);
9699
+ h2 ^= Math.imul(h1 ^ h1 >>> 13, 351452291);
9700
+ const toHex = (n) => (n >>> 0).toString(16).padStart(8, "0");
9701
+ return toHex(h1) + toHex(h2);
9393
9702
  }
9394
- async function detectOsVersion() {
9395
- try {
9396
- const rn = await import("react-native");
9397
- const version = rn.Platform?.Version;
9398
- return version != null ? String(version) : "unknown";
9399
- } catch {
9400
- return "unknown";
9401
- }
9703
+ function buildDeterministicEventId(parts) {
9704
+ const raw = hashString(parts.join("|"));
9705
+ const p1 = raw.slice(0, 8);
9706
+ const p2 = raw.slice(8, 12);
9707
+ const p3 = "4" + raw.slice(13, 16);
9708
+ const p4 = (parseInt(raw[16], 16) & 3 | 8).toString(16) + raw.slice(17, 20);
9709
+ const p5 = raw.padEnd(32, "0").slice(20, 32);
9710
+ return `${p1}-${p2}-${p3}-${p4}-${p5}`;
9402
9711
  }
9403
9712
 
9404
- // src/hooks/useAppAutoEvents.ts
9405
- var FIRST_SEEN_KEY = "@paywallo:firstSeen_v226";
9406
- var DEBOUNCE_MS = 1e3;
9407
- function useAppAutoEvents(config) {
9408
- const { isInitialized, getAppVersion: getAppVersion2, getSessionId, emitLifecycle, storageGet, storageSet, debug } = config;
9409
- const didRunRef = useRef7(false);
9410
- useEffect11(() => {
9411
- if (debug) console.log("[Paywallo EVENTS] useEffect fired", { isInitialized, didRunRefCurrent: didRunRef.current });
9412
- if (!isInitialized || didRunRef.current) return;
9413
- const timer = setTimeout(() => {
9414
- if (didRunRef.current) return;
9415
- didRunRef.current = true;
9416
- void (async () => {
9417
- const sessionId = getSessionId();
9418
- const appVersion = getAppVersion2();
9419
- const deviceType = await detectPlatform3();
9420
- const osVersion = await detectOsVersion();
9421
- if (debug) console.log("[Paywallo EVENTS] computed device context", { deviceType, osVersion, appVersion, sessionId, hasSessionId: !!sessionId });
9422
- try {
9423
- if (debug) console.log("[Paywallo EVENTS] checking install \u2014 about to read FIRST_SEEN_KEY");
9424
- const firstSeen = await storageGet(FIRST_SEEN_KEY);
9425
- if (!firstSeen) {
9426
- await emitLifecycle({ type: "install", device_type: deviceType, os_version: osVersion, app_version: appVersion, ...sessionId ? { session_id: sessionId } : {} }, "critical");
9427
- await storageSet(FIRST_SEEN_KEY, (/* @__PURE__ */ new Date()).toISOString());
9428
- if (debug) console.log("[Paywallo EVENTS] install event emitted (first time)");
9429
- if (debug) console.log("[Paywallo EVENTS] lifecycle install emitted");
9430
- } else {
9431
- if (debug) console.log("[Paywallo EVENTS] install skipped \u2014 firstSeen already exists", { firstSeen });
9432
- }
9433
- } catch (err) {
9434
- if (debug) console.log("[Paywallo EVENTS] lifecycle install check failed", err);
9435
- }
9436
- try {
9437
- if (debug) console.log("[Paywallo EVENTS] emitting cold_start");
9438
- await emitLifecycle({ type: "cold_start", app_version: appVersion, device_type: deviceType, os_version: osVersion, ...sessionId ? { session_id: sessionId } : {} }, "normal");
9439
- if (debug) console.log("[Paywallo EVENTS] lifecycle cold_start emitted");
9440
- } catch (err) {
9441
- if (debug) console.log("[Paywallo EVENTS] lifecycle cold_start failed", err);
9442
- }
9443
- })();
9444
- }, DEBOUNCE_MS);
9445
- return () => clearTimeout(timer);
9446
- }, [isInitialized]);
9713
+ // src/domains/paywall/SuperwallAutoBridge.ts
9714
+ var bridgeStarted = false;
9715
+ var unsubscribe = null;
9716
+ var debugEnabled = false;
9717
+ function log(msg, data) {
9718
+ if (!debugEnabled) return;
9719
+ if (data !== void 0) {
9720
+ console.log(`[Paywallo:SuperwallBridge] ${msg}`, data);
9721
+ } else {
9722
+ console.log(`[Paywallo:SuperwallBridge] ${msg}`);
9723
+ }
9447
9724
  }
9725
+ function minuteBucket(ts) {
9726
+ return String(Math.floor(ts / 6e4));
9727
+ }
9728
+ function buildEventId(type, identifier, ts) {
9729
+ return buildDeterministicEventId(["sw", type, identifier, minuteBucket(ts)]);
9730
+ }
9731
+ async function trackPaywallEvent(type, identifier, ts, extra) {
9732
+ const _eventId = buildEventId(type, identifier, ts);
9733
+ try {
9734
+ const cleanExtra = {};
9735
+ if (extra) {
9736
+ for (const [k, v] of Object.entries(extra)) if (v !== void 0) cleanExtra[k] = v;
9737
+ }
9738
+ await PaywalloClient.track("paywall", {
9739
+ priority: "critical",
9740
+ properties: {
9741
+ type,
9742
+ paywall_id: identifier,
9743
+ ...cleanExtra
9744
+ }
9745
+ });
9746
+ } catch (err) {
9747
+ log("track error", { type, identifier, eventId: _eventId, err: String(err) });
9748
+ }
9749
+ }
9750
+ function mapDismissToCloseReason(resultType) {
9751
+ switch (resultType) {
9752
+ case "purchased":
9753
+ return "purchased";
9754
+ case "restored":
9755
+ return "purchased";
9756
+ // restore counts as a purchase outcome for funnel
9757
+ case "declined":
9758
+ return "dismissed";
9759
+ default:
9760
+ return void 0;
9761
+ }
9762
+ }
9763
+ function buildCallbacks() {
9764
+ return {
9765
+ onPaywallPresent: (info) => {
9766
+ const ts = Date.now();
9767
+ log("onPaywallPresent", { identifier: info.identifier });
9768
+ void trackPaywallEvent("viewed", info.identifier, ts, {
9769
+ placement: info.presentedByEventWithName,
9770
+ variant_id: info.experiment?.variant?.id,
9771
+ campaign_id: info.experiment?.id
9772
+ });
9773
+ },
9774
+ onPaywallDismiss: (info, result) => {
9775
+ const ts = Date.now();
9776
+ log("onPaywallDismiss", { identifier: info.identifier, result: result.type });
9777
+ void trackPaywallEvent("closed", info.identifier, ts, {
9778
+ placement: info.presentedByEventWithName,
9779
+ variant_id: info.experiment?.variant?.id,
9780
+ campaign_id: info.experiment?.id,
9781
+ close_reason: mapDismissToCloseReason(result.type),
9782
+ closed_at: new Date(ts).toISOString()
9783
+ });
9784
+ },
9785
+ onPurchase: (params) => {
9786
+ const ts = Date.now();
9787
+ const safe = params !== null && typeof params === "object" ? params : {};
9788
+ const productId = "productId" in safe && typeof safe.productId === "string" ? safe.productId : "unknown";
9789
+ const identifier = "paywallIdentifier" in safe && typeof safe.paywallIdentifier === "string" ? safe.paywallIdentifier : productId;
9790
+ log("onPurchase", { productId, identifier });
9791
+ void trackPaywallEvent("purchased", identifier, ts, { product_id: productId });
9792
+ },
9793
+ // Global Superwall event listener — used specifically for `transactionComplete`
9794
+ // which is the only event that exposes the full product (currency/price) +
9795
+ // transaction (storeTransactionId) shape needed to create a server-side
9796
+ // `transactions` row. Other events are skipped (paywallOpen/Dismiss already
9797
+ // handled above with their typed callbacks).
9798
+ onSuperwallEvent: (info) => {
9799
+ const ev = info?.event;
9800
+ if (!ev || ev.event !== "transactionComplete") return;
9801
+ const ts = Date.now();
9802
+ const product = ev.product;
9803
+ const transaction = ev.transaction;
9804
+ if (!product) {
9805
+ log("transactionComplete missing product \u2014 skipping transaction track");
9806
+ return;
9807
+ }
9808
+ const productId = product.productIdentifier ?? product.id ?? "unknown";
9809
+ const currency = product.currencyCode ?? "USD";
9810
+ const price = product.price ?? 0;
9811
+ const transactionId = transaction?.storeTransactionId ?? transaction?.originalTransactionIdentifier ?? `${productId}_${String(ts)}`;
9812
+ const isTrial = product.hasFreeTrial === true || (product.trialPeriodPrice ?? 0) > 0;
9813
+ const paywallIdentifier = ev.paywallInfo?.identifier;
9814
+ log("onSuperwallEvent: transactionComplete", { productId, currency, price, transactionId, isTrial });
9815
+ void (async () => {
9816
+ try {
9817
+ await PaywalloClient.track("transaction", {
9818
+ priority: "critical",
9819
+ properties: {
9820
+ type: isTrial ? "trial_started" : "completed",
9821
+ // Server's IngestTranslator looks for `transaction_id`/`product_id`
9822
+ // /`amount`/`currency`/`paywall_id` keys (not `tx_id`). PurchaseHandler
9823
+ // then resolves type=trial/new/renewal based on amount being 0 vs >0.
9824
+ transaction_id: transactionId,
9825
+ product_id: productId,
9826
+ amount: price,
9827
+ currency,
9828
+ ...paywallIdentifier ? { paywall_id: paywallIdentifier } : {}
9829
+ }
9830
+ });
9831
+ } catch (err) {
9832
+ log("transactionComplete track error", { err: String(err) });
9833
+ }
9834
+ })();
9835
+ }
9836
+ };
9837
+ }
9838
+ function startSuperwallAutoBridge(debug = false) {
9839
+ debugEnabled = debug;
9840
+ if (bridgeStarted) {
9841
+ log("already started, skipping");
9842
+ return;
9843
+ }
9844
+ void detectAndStart();
9845
+ }
9846
+ async function detectAndStart() {
9847
+ let subscribeFn;
9848
+ try {
9849
+ const mod = await import("expo-superwall");
9850
+ if (!mod || typeof mod.subscribeToSuperwallEvents !== "function") {
9851
+ const internal = await import("expo-superwall/build/src/internal/superwallEventBridge");
9852
+ if (typeof internal.subscribeToSuperwallEvents !== "function") {
9853
+ log("expo-superwall subscribeToSuperwallEvents not found \u2014 bridge inactive");
9854
+ return;
9855
+ }
9856
+ subscribeFn = internal.subscribeToSuperwallEvents;
9857
+ } else {
9858
+ subscribeFn = mod.subscribeToSuperwallEvents;
9859
+ }
9860
+ } catch {
9861
+ log("expo-superwall not installed \u2014 bridge inactive");
9862
+ return;
9863
+ }
9864
+ try {
9865
+ const callbacks = buildCallbacks();
9866
+ unsubscribe = subscribeFn(void 0, () => callbacks);
9867
+ bridgeStarted = true;
9868
+ log("bridge started");
9869
+ } catch (err) {
9870
+ log("subscribe failed", { err: String(err) });
9871
+ }
9872
+ }
9873
+
9874
+ // src/PaywalloProvider.tsx
9875
+ init_session();
9876
+ init_ClientError();
9877
+
9878
+ // src/hooks/useAppAutoEvents.ts
9879
+ import { useEffect as useEffect13, useRef as useRef8 } from "react";
9880
+
9881
+ // src/utils/platformDetection.ts
9882
+ async function detectPlatform3() {
9883
+ try {
9884
+ const rn = await import("react-native");
9885
+ return rn.Platform?.OS === "ios" ? "ios" : "android";
9886
+ } catch {
9887
+ return "android";
9888
+ }
9889
+ }
9890
+ async function detectOsVersion() {
9891
+ try {
9892
+ const rn = await import("react-native");
9893
+ const version = rn.Platform?.Version;
9894
+ return version != null ? String(version) : "unknown";
9895
+ } catch {
9896
+ return "unknown";
9897
+ }
9898
+ }
9899
+
9900
+ // src/hooks/useAppAutoEvents.ts
9901
+ var FIRST_SEEN_KEY = "@paywallo:firstSeen_v226";
9902
+ var DEBOUNCE_MS = 1e3;
9903
+ function useAppAutoEvents(config) {
9904
+ const { isInitialized, getAppVersion: getAppVersion2, getSessionId, emitLifecycle, storageGet, storageSet, debug } = config;
9905
+ const didRunRef = useRef8(false);
9906
+ useEffect13(() => {
9907
+ if (debug) console.log("[Paywallo EVENTS] useEffect fired", { isInitialized, didRunRefCurrent: didRunRef.current });
9908
+ if (!isInitialized || didRunRef.current) return;
9909
+ const timer = setTimeout(() => {
9910
+ if (didRunRef.current) return;
9911
+ didRunRef.current = true;
9912
+ void (async () => {
9913
+ const sessionId = getSessionId();
9914
+ const appVersion = getAppVersion2();
9915
+ const deviceType = await detectPlatform3();
9916
+ const osVersion = await detectOsVersion();
9917
+ if (debug) console.log("[Paywallo EVENTS] computed device context", { deviceType, osVersion, appVersion, sessionId, hasSessionId: !!sessionId });
9918
+ try {
9919
+ if (debug) console.log("[Paywallo EVENTS] checking install \u2014 about to read FIRST_SEEN_KEY");
9920
+ const firstSeen = await storageGet(FIRST_SEEN_KEY);
9921
+ if (!firstSeen) {
9922
+ await emitLifecycle({ type: "install", device_type: deviceType, os_version: osVersion, app_version: appVersion, ...sessionId ? { session_id: sessionId } : {} }, "critical");
9923
+ await storageSet(FIRST_SEEN_KEY, (/* @__PURE__ */ new Date()).toISOString());
9924
+ if (debug) console.log("[Paywallo EVENTS] install event emitted (first time)");
9925
+ if (debug) console.log("[Paywallo EVENTS] lifecycle install emitted");
9926
+ } else {
9927
+ if (debug) console.log("[Paywallo EVENTS] install skipped \u2014 firstSeen already exists", { firstSeen });
9928
+ }
9929
+ } catch (err) {
9930
+ if (debug) console.log("[Paywallo EVENTS] lifecycle install check failed", err);
9931
+ }
9932
+ try {
9933
+ if (debug) console.log("[Paywallo EVENTS] emitting cold_start");
9934
+ await emitLifecycle({ type: "cold_start", app_version: appVersion, device_type: deviceType, os_version: osVersion, ...sessionId ? { session_id: sessionId } : {} }, "normal");
9935
+ if (debug) console.log("[Paywallo EVENTS] lifecycle cold_start emitted");
9936
+ } catch (err) {
9937
+ if (debug) console.log("[Paywallo EVENTS] lifecycle cold_start failed", err);
9938
+ }
9939
+ })();
9940
+ }, DEBOUNCE_MS);
9941
+ return () => clearTimeout(timer);
9942
+ }, [isInitialized]);
9943
+ }
9944
+
9945
+ // src/hooks/useCampaignPreload.ts
9946
+ init_PaywalloClient();
9947
+ init_CampaignError();
9948
+ import { useCallback as useCallback15, useRef as useRef9, useState as useState14 } from "react";
9949
+
9950
+ // src/utils/loadCampaignProducts.ts
9951
+ init_IAPService();
9448
9952
 
9449
- // src/hooks/useCampaignPreload.ts
9450
- init_PaywalloClient();
9451
- init_CampaignError();
9452
- import { useCallback as useCallback15, useRef as useRef8, useState as useState13 } from "react";
9453
-
9454
- // src/utils/loadCampaignProducts.ts
9455
- init_IAPService();
9456
-
9457
9953
  // src/utils/serverProduct.ts
9458
9954
  var PERIOD_MAP = {
9459
9955
  P1Y: "ano",
@@ -9518,8 +10014,8 @@ function getCacheEntry(cache, placement) {
9518
10014
 
9519
10015
  // src/hooks/useCampaignPreload.ts
9520
10016
  function useCampaignPreload() {
9521
- const [preloadedWebView, setPreloadedWebView] = useState13(null);
9522
- const preloadedCampaignsRef = useRef8(/* @__PURE__ */ new Map());
10017
+ const [preloadedWebView, setPreloadedWebView] = useState14(null);
10018
+ const preloadedCampaignsRef = useRef9(/* @__PURE__ */ new Map());
9523
10019
  const isPreloadValid = useCallback15(
9524
10020
  (placement) => isCacheEntryValid(preloadedCampaignsRef.current.get(placement)),
9525
10021
  []
@@ -9597,10 +10093,10 @@ function useCampaignPreload() {
9597
10093
  }
9598
10094
 
9599
10095
  // src/hooks/useProductLoader.ts
9600
- import { useCallback as useCallback16, useState as useState14 } from "react";
10096
+ import { useCallback as useCallback16, useState as useState15 } from "react";
9601
10097
  function useProductLoader() {
9602
- const [products, setProducts] = useState14(/* @__PURE__ */ new Map());
9603
- const [isLoadingProducts, setIsLoadingProducts] = useState14(false);
10098
+ const [products, setProducts] = useState15(/* @__PURE__ */ new Map());
10099
+ const [isLoadingProducts, setIsLoadingProducts] = useState15(false);
9604
10100
  const refreshProducts = useCallback16(async (productIds) => {
9605
10101
  setIsLoadingProducts(true);
9606
10102
  try {
@@ -9686,7 +10182,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
9686
10182
  // src/hooks/useSubscriptionSync.ts
9687
10183
  init_PaywalloClient();
9688
10184
  init_SubscriptionCache();
9689
- import { useCallback as useCallback18, useRef as useRef9, useState as useState15 } from "react";
10185
+ import { useCallback as useCallback18, useRef as useRef10, useState as useState16 } from "react";
9690
10186
 
9691
10187
  // src/hooks/subscriptionSyncHelpers.ts
9692
10188
  init_SubscriptionCache();
@@ -9794,8 +10290,8 @@ async function checkPersistedCache(distinctId, onHit) {
9794
10290
 
9795
10291
  // src/hooks/useSubscriptionSync.ts
9796
10292
  function useSubscriptionSync() {
9797
- const [subscription, setSubscription] = useState15(null);
9798
- const cacheRef = useRef9(null);
10293
+ const [subscription, setSubscription] = useState16(null);
10294
+ const cacheRef = useRef10(null);
9799
10295
  const setCacheAndState = useCallback18((sub) => {
9800
10296
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
9801
10297
  setSubscription(sub);
@@ -9856,214 +10352,24 @@ function useSubscriptionSync() {
9856
10352
  return { subscription, hasActiveSubscription, getSubscription, invalidateCache };
9857
10353
  }
9858
10354
 
9859
- // src/domains/paywall/SuperwallAutoBridge.ts
9860
- init_PaywalloClient();
9861
-
9862
- // src/core/utils/eventId.ts
9863
- function hashString(input) {
9864
- let h1 = 3735928559;
9865
- let h2 = 1103547991;
9866
- for (let i = 0; i < input.length; i++) {
9867
- const ch = input.charCodeAt(i);
9868
- h1 = Math.imul(h1 ^ ch, 2654435769);
9869
- h2 = Math.imul(h2 ^ ch, 1598716949);
9870
- }
9871
- h1 = Math.imul(h1 ^ h1 >>> 16, 73244475);
9872
- h1 ^= Math.imul(h2 ^ h2 >>> 13, 351452291);
9873
- h2 = Math.imul(h2 ^ h2 >>> 16, 73244475);
9874
- h2 ^= Math.imul(h1 ^ h1 >>> 13, 351452291);
9875
- const toHex = (n) => (n >>> 0).toString(16).padStart(8, "0");
9876
- return toHex(h1) + toHex(h2);
9877
- }
9878
- function buildDeterministicEventId(parts) {
9879
- const raw = hashString(parts.join("|"));
9880
- const p1 = raw.slice(0, 8);
9881
- const p2 = raw.slice(8, 12);
9882
- const p3 = "4" + raw.slice(13, 16);
9883
- const p4 = (parseInt(raw[16], 16) & 3 | 8).toString(16) + raw.slice(17, 20);
9884
- const p5 = raw.padEnd(32, "0").slice(20, 32);
9885
- return `${p1}-${p2}-${p3}-${p4}-${p5}`;
9886
- }
9887
-
9888
- // src/domains/paywall/SuperwallAutoBridge.ts
9889
- var bridgeStarted = false;
9890
- var unsubscribe = null;
9891
- var debugEnabled = false;
9892
- function log(msg, data) {
9893
- if (!debugEnabled) return;
9894
- if (data !== void 0) {
9895
- console.log(`[Paywallo:SuperwallBridge] ${msg}`, data);
9896
- } else {
9897
- console.log(`[Paywallo:SuperwallBridge] ${msg}`);
9898
- }
9899
- }
9900
- function minuteBucket(ts) {
9901
- return String(Math.floor(ts / 6e4));
9902
- }
9903
- function buildEventId(type, identifier, ts) {
9904
- return buildDeterministicEventId(["sw", type, identifier, minuteBucket(ts)]);
9905
- }
9906
- async function trackPaywallEvent(type, identifier, ts, extra) {
9907
- const _eventId = buildEventId(type, identifier, ts);
9908
- try {
9909
- const cleanExtra = {};
9910
- if (extra) {
9911
- for (const [k, v] of Object.entries(extra)) if (v !== void 0) cleanExtra[k] = v;
9912
- }
9913
- await PaywalloClient.track("paywall", {
9914
- priority: "critical",
9915
- properties: {
9916
- type,
9917
- paywall_id: identifier,
9918
- ...cleanExtra
9919
- }
9920
- });
9921
- } catch (err) {
9922
- log("track error", { type, identifier, eventId: _eventId, err: String(err) });
9923
- }
9924
- }
9925
- function mapDismissToCloseReason(resultType) {
9926
- switch (resultType) {
9927
- case "purchased":
9928
- return "purchased";
9929
- case "restored":
9930
- return "purchased";
9931
- // restore counts as a purchase outcome for funnel
9932
- case "declined":
9933
- return "dismissed";
9934
- default:
9935
- return void 0;
9936
- }
9937
- }
9938
- function buildCallbacks() {
9939
- return {
9940
- onPaywallPresent: (info) => {
9941
- const ts = Date.now();
9942
- log("onPaywallPresent", { identifier: info.identifier });
9943
- void trackPaywallEvent("viewed", info.identifier, ts, {
9944
- placement: info.presentedByEventWithName,
9945
- variant_id: info.experiment?.variant?.id,
9946
- campaign_id: info.experiment?.id
9947
- });
9948
- },
9949
- onPaywallDismiss: (info, result) => {
9950
- const ts = Date.now();
9951
- log("onPaywallDismiss", { identifier: info.identifier, result: result.type });
9952
- void trackPaywallEvent("closed", info.identifier, ts, {
9953
- placement: info.presentedByEventWithName,
9954
- variant_id: info.experiment?.variant?.id,
9955
- campaign_id: info.experiment?.id,
9956
- close_reason: mapDismissToCloseReason(result.type),
9957
- closed_at: new Date(ts).toISOString()
9958
- });
9959
- },
9960
- onPurchase: (params) => {
9961
- const ts = Date.now();
9962
- const safe = params !== null && typeof params === "object" ? params : {};
9963
- const productId = "productId" in safe && typeof safe.productId === "string" ? safe.productId : "unknown";
9964
- const identifier = "paywallIdentifier" in safe && typeof safe.paywallIdentifier === "string" ? safe.paywallIdentifier : productId;
9965
- log("onPurchase", { productId, identifier });
9966
- void trackPaywallEvent("purchased", identifier, ts, { product_id: productId });
9967
- },
9968
- // Global Superwall event listener — used specifically for `transactionComplete`
9969
- // which is the only event that exposes the full product (currency/price) +
9970
- // transaction (storeTransactionId) shape needed to create a server-side
9971
- // `transactions` row. Other events are skipped (paywallOpen/Dismiss already
9972
- // handled above with their typed callbacks).
9973
- onSuperwallEvent: (info) => {
9974
- const ev = info?.event;
9975
- if (!ev || ev.event !== "transactionComplete") return;
9976
- const ts = Date.now();
9977
- const product = ev.product;
9978
- const transaction = ev.transaction;
9979
- if (!product) {
9980
- log("transactionComplete missing product \u2014 skipping transaction track");
9981
- return;
9982
- }
9983
- const productId = product.productIdentifier ?? product.id ?? "unknown";
9984
- const currency = product.currencyCode ?? "USD";
9985
- const price = product.price ?? 0;
9986
- const transactionId = transaction?.storeTransactionId ?? transaction?.originalTransactionIdentifier ?? `${productId}_${String(ts)}`;
9987
- const isTrial = product.hasFreeTrial === true || (product.trialPeriodPrice ?? 0) > 0;
9988
- const paywallIdentifier = ev.paywallInfo?.identifier;
9989
- log("onSuperwallEvent: transactionComplete", { productId, currency, price, transactionId, isTrial });
9990
- void (async () => {
9991
- try {
9992
- await PaywalloClient.track("transaction", {
9993
- priority: "critical",
9994
- properties: {
9995
- type: isTrial ? "trial_started" : "completed",
9996
- // Server's IngestTranslator looks for `transaction_id`/`product_id`
9997
- // /`amount`/`currency`/`paywall_id` keys (not `tx_id`). PurchaseHandler
9998
- // then resolves type=trial/new/renewal based on amount being 0 vs >0.
9999
- transaction_id: transactionId,
10000
- product_id: productId,
10001
- amount: price,
10002
- currency,
10003
- ...paywallIdentifier ? { paywall_id: paywallIdentifier } : {}
10004
- }
10005
- });
10006
- } catch (err) {
10007
- log("transactionComplete track error", { err: String(err) });
10008
- }
10009
- })();
10010
- }
10011
- };
10012
- }
10013
- function startSuperwallAutoBridge(debug = false) {
10014
- debugEnabled = debug;
10015
- if (bridgeStarted) {
10016
- log("already started, skipping");
10017
- return;
10018
- }
10019
- void detectAndStart();
10020
- }
10021
- async function detectAndStart() {
10022
- let subscribeFn;
10023
- try {
10024
- const mod = await import("expo-superwall");
10025
- if (!mod || typeof mod.subscribeToSuperwallEvents !== "function") {
10026
- const internal = await import("expo-superwall/build/src/internal/superwallEventBridge");
10027
- if (typeof internal.subscribeToSuperwallEvents !== "function") {
10028
- log("expo-superwall subscribeToSuperwallEvents not found \u2014 bridge inactive");
10029
- return;
10030
- }
10031
- subscribeFn = internal.subscribeToSuperwallEvents;
10032
- } else {
10033
- subscribeFn = mod.subscribeToSuperwallEvents;
10034
- }
10035
- } catch {
10036
- log("expo-superwall not installed \u2014 bridge inactive");
10037
- return;
10038
- }
10039
- try {
10040
- const callbacks = buildCallbacks();
10041
- unsubscribe = subscribeFn(void 0, () => callbacks);
10042
- bridgeStarted = true;
10043
- log("bridge started");
10044
- } catch (err) {
10045
- log("subscribe failed", { err: String(err) });
10046
- }
10047
- }
10048
-
10049
10355
  // src/PaywalloProvider.tsx
10050
10356
  init_localization();
10051
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
10357
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
10052
10358
  function PaywalloProvider({ children, config }) {
10053
- const [isInitialized, setIsInitialized] = useState16(false);
10054
- const [initError, setInitError] = useState16(null);
10055
- const [distinctId, setDistinctId] = useState16(null);
10056
- const [activePaywall, setActivePaywall] = useState16(null);
10359
+ const [isInitialized, setIsInitialized] = useState17(false);
10360
+ const [initError, setInitError] = useState17(null);
10361
+ const [distinctId, setDistinctId] = useState17(null);
10362
+ const [activePaywall, setActivePaywall] = useState17(null);
10057
10363
  const { products, isLoadingProducts, refreshProducts } = useProductLoader();
10058
10364
  const { subscription, hasActiveSubscription, getSubscription, invalidateCache } = useSubscriptionSync();
10059
10365
  const { preloadedWebView, setPreloadedWebView, preloadCampaign, consumePreloadedCampaign, isPreloadValid } = useCampaignPreload();
10060
10366
  const clearPreloadedWebView = useCallback19(() => setPreloadedWebView(null), [setPreloadedWebView]);
10061
- const autoPreloadTriggeredRef = useRef10(false);
10062
- const preloadedWebViewRef = useRef10(preloadedWebView);
10063
- const pollTimerRef = useRef10(null);
10064
- const pollCancelledRef = useRef10(false);
10065
- const secureStorageRef = useRef10(new SecureStorage(config.debug));
10066
- useEffect12(() => {
10367
+ const autoPreloadTriggeredRef = useRef11(false);
10368
+ const preloadedWebViewRef = useRef11(preloadedWebView);
10369
+ const pollTimerRef = useRef11(null);
10370
+ const pollCancelledRef = useRef11(false);
10371
+ const secureStorageRef = useRef11(new SecureStorage(config.debug));
10372
+ useEffect14(() => {
10067
10373
  preloadedWebViewRef.current = preloadedWebView;
10068
10374
  }, [preloadedWebView]);
10069
10375
  useAppAutoEvents({
@@ -10094,8 +10400,8 @@ function PaywalloProvider({ children, config }) {
10094
10400
  });
10095
10401
  }, []);
10096
10402
  const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady: baseHandlePreloadedWebViewReady, handlePreloadedClose: baseHandlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, setPreloadedWebView);
10097
- const SCREEN_HEIGHT = useMemo2(() => Dimensions5.get("window").height, []);
10098
- const slideAnim = useRef10(new Animated3.Value(SCREEN_HEIGHT)).current;
10403
+ const SCREEN_HEIGHT = useMemo3(() => Dimensions5.get("window").height, []);
10404
+ const slideAnim = useRef11(new Animated4.Value(SCREEN_HEIGHT)).current;
10099
10405
  const handlePreloadedWebViewReady = useCallback19(() => {
10100
10406
  if (PaywalloClient.getConfig()?.debug) {
10101
10407
  console.log(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
@@ -10104,7 +10410,7 @@ function PaywalloProvider({ children, config }) {
10104
10410
  }, [preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
10105
10411
  const handlePreloadedClose = useCallback19(() => {
10106
10412
  const placement = preloadedWebView?.placement;
10107
- Animated3.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 200, easing: Easing2.in(Easing2.cubic), useNativeDriver: true }).start(() => {
10413
+ Animated4.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 200, easing: Easing2.in(Easing2.cubic), useNativeDriver: true }).start(() => {
10108
10414
  baseHandlePreloadedClose();
10109
10415
  if (placement) triggerRepreload(placement);
10110
10416
  });
@@ -10117,7 +10423,7 @@ function PaywalloProvider({ children, config }) {
10117
10423
  presentedAtRef,
10118
10424
  invalidateCache
10119
10425
  );
10120
- const [isPreloadPurchasing, setIsPreloadPurchasing] = useState16(false);
10426
+ const [isPreloadPurchasing, setIsPreloadPurchasing] = useState17(false);
10121
10427
  const handlePreloadedPurchase = useCallback19(async (productId) => {
10122
10428
  setIsPreloadPurchasing(true);
10123
10429
  try {
@@ -10126,8 +10432,8 @@ function PaywalloProvider({ children, config }) {
10126
10432
  setIsPreloadPurchasing(false);
10127
10433
  }
10128
10434
  }, [rawPreloadedPurchase]);
10129
- const configRef = useRef10(config);
10130
- useEffect12(() => {
10435
+ const configRef = useRef11(config);
10436
+ useEffect14(() => {
10131
10437
  let mounted = true;
10132
10438
  initLocalization({ detectDevice: true });
10133
10439
  PaywalloClient.init(configRef.current).then(async () => {
@@ -10140,6 +10446,11 @@ function PaywalloProvider({ children, config }) {
10140
10446
  const placement = await PaywalloClient.waitForAutoPreloadedPlacement();
10141
10447
  if (mounted && placement) void preloadCampaign(placement).catch(() => {
10142
10448
  });
10449
+ const preloadPlacements = configRef.current.preloadPaywalls;
10450
+ if (mounted && preloadPlacements && preloadPlacements.length > 0) {
10451
+ void PaywalloClient.preloadPaywalls(preloadPlacements).catch(() => {
10452
+ });
10453
+ }
10143
10454
  }
10144
10455
  }).catch((err) => {
10145
10456
  if (!mounted) return;
@@ -10154,7 +10465,21 @@ function PaywalloProvider({ children, config }) {
10154
10465
  return api ? api.getPaywall(p) : null;
10155
10466
  }, []);
10156
10467
  const presentPaywall = useCallback19((placement) => {
10157
- return new Promise((resolve) => setActivePaywall({ placement, resolver: resolve }));
10468
+ const preloaded = paywallPreloadService.getPreloaded(placement);
10469
+ return new Promise((resolve) => setActivePaywall({
10470
+ placement,
10471
+ resolver: resolve,
10472
+ ...preloaded && {
10473
+ paywallConfig: {
10474
+ id: preloaded.config.id,
10475
+ placement: preloaded.config.placement,
10476
+ config: preloaded.config.config,
10477
+ primaryProductId: preloaded.config.primaryProductId,
10478
+ secondaryProductId: preloaded.config.secondaryProductId
10479
+ },
10480
+ preloadedProducts: preloaded.products
10481
+ }
10482
+ }));
10158
10483
  }, []);
10159
10484
  const trackCampaignImpression = useCallback19((placement, variantKey, campaignId) => {
10160
10485
  const api = PaywalloClient.getApiClient();
@@ -10288,7 +10613,7 @@ function PaywalloProvider({ children, config }) {
10288
10613
  }
10289
10614
  return presentCampaign(placement);
10290
10615
  }, [presentPreloadedWebView, presentCampaign]);
10291
- useEffect12(() => {
10616
+ useEffect14(() => {
10292
10617
  if (!isInitialized) return;
10293
10618
  const onEmergency = async (id) => {
10294
10619
  try {
@@ -10317,7 +10642,7 @@ function PaywalloProvider({ children, config }) {
10317
10642
  const isReady = isInitialized && !isLoadingProducts;
10318
10643
  const noOp = useCallback19(() => {
10319
10644
  }, []);
10320
- const contextValue = useMemo2(() => ({
10645
+ const contextValue = useMemo3(() => ({
10321
10646
  config: PaywalloClient.getConfig(),
10322
10647
  isInitialized,
10323
10648
  isReady,
@@ -10354,17 +10679,17 @@ function PaywalloProvider({ children, config }) {
10354
10679
  useLayoutEffect(() => {
10355
10680
  if (showingPreloadedWebView) {
10356
10681
  slideAnim.setValue(SCREEN_HEIGHT);
10357
- Animated3.timing(slideAnim, { toValue: 0, duration: 200, easing: Easing2.out(Easing2.cubic), useNativeDriver: true }).start();
10682
+ Animated4.timing(slideAnim, { toValue: 0, duration: 200, easing: Easing2.out(Easing2.cubic), useNativeDriver: true }).start();
10358
10683
  }
10359
10684
  }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT]);
10360
10685
  const preloadStyle = [
10361
- styles3.preloadOverlay,
10362
- showingPreloadedWebView ? styles3.visible : styles3.hidden,
10686
+ styles5.preloadOverlay,
10687
+ showingPreloadedWebView ? styles5.visible : styles5.hidden,
10363
10688
  showingPreloadedWebView ? { transform: [{ translateY: slideAnim }] } : void 0
10364
10689
  ];
10365
- return /* @__PURE__ */ jsxs2(PaywalloContext.Provider, { value: contextValue, children: [
10690
+ return /* @__PURE__ */ jsxs3(PaywalloContext.Provider, { value: contextValue, children: [
10366
10691
  children,
10367
- /* @__PURE__ */ jsx3(PaywallErrorBoundary, { children: !!preloadedWebView?.craftData && /* @__PURE__ */ jsx3(Animated3.View, { style: preloadStyle, pointerEvents: showingPreloadedWebView ? "auto" : "none", children: /* @__PURE__ */ jsx3(
10692
+ /* @__PURE__ */ jsx5(PaywallErrorBoundary, { children: !!preloadedWebView?.craftData && /* @__PURE__ */ jsx5(Animated4.View, { style: preloadStyle, pointerEvents: showingPreloadedWebView ? "auto" : "none", children: /* @__PURE__ */ jsx5(
10368
10693
  PaywallWebView,
10369
10694
  {
10370
10695
  paywallId: preloadedWebView.paywallId,
@@ -10380,7 +10705,7 @@ function PaywalloProvider({ children, config }) {
10380
10705
  onRestore: showingPreloadedWebView ? handlePreloadedRestore : noOp
10381
10706
  }
10382
10707
  ) }) }),
10383
- /* @__PURE__ */ jsx3(PaywallErrorBoundary, { children: activePaywall && /* @__PURE__ */ jsx3(
10708
+ /* @__PURE__ */ jsx5(PaywallErrorBoundary, { children: activePaywall && /* @__PURE__ */ jsx5(
10384
10709
  PaywallModal,
10385
10710
  {
10386
10711
  placement: activePaywall.placement,
@@ -10392,10 +10717,11 @@ function PaywalloProvider({ children, config }) {
10392
10717
  campaignId: activePaywall.campaignId,
10393
10718
  variantId: activePaywall.variantId
10394
10719
  }
10395
- ) })
10720
+ ) }),
10721
+ isInitialized && (config.preloadPaywalls?.length ?? 0) > 0 && PaywalloClient.getWebUrl() !== null && /* @__PURE__ */ jsx5(PaywallWebViewPrewarmer, { webUrl: PaywalloClient.getWebUrl() })
10396
10722
  ] });
10397
10723
  }
10398
- var styles3 = StyleSheet3.create({
10724
+ var styles5 = StyleSheet5.create({
10399
10725
  preloadOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#ffffff" },
10400
10726
  hidden: { zIndex: -1, opacity: 0, width: 1, height: 1, overflow: "hidden" },
10401
10727
  visible: { zIndex: 9999, opacity: 1 }