@virex-tech/paywallo-sdk 2.2.6 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.3.0") {
2374
+ return "2.3.0";
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 = {
@@ -3292,6 +3387,7 @@ function buildPaywallInjectionScript(data) {
3292
3387
  products: data.products,
3293
3388
  primaryProductId: data.primaryProductId,
3294
3389
  secondaryProductId: data.secondaryProductId,
3390
+ tertiaryProductId: data.tertiaryProductId,
3295
3391
  currentLanguage: getCurrentLanguage(),
3296
3392
  defaultLanguage: getDefaultLanguage()
3297
3393
  }
@@ -3327,7 +3423,7 @@ var init_paywallScripts = __esm({
3327
3423
  });
3328
3424
 
3329
3425
  // src/domains/paywall/PaywallWebView.tsx
3330
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3426
+ import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState } from "react";
3331
3427
  import {
3332
3428
  BackHandler,
3333
3429
  findNodeHandle,
@@ -3336,11 +3432,11 @@ import {
3336
3432
  NativeModules as NativeModules6,
3337
3433
  Platform as Platform9,
3338
3434
  requireNativeComponent,
3339
- StyleSheet,
3435
+ StyleSheet as StyleSheet2,
3340
3436
  UIManager,
3341
- View
3437
+ View as View2
3342
3438
  } from "react-native";
3343
- import { jsx } from "react/jsx-runtime";
3439
+ import { jsx as jsx2 } from "react/jsx-runtime";
3344
3440
  function getNativeWebView() {
3345
3441
  if (NativeWebViewComponent) return NativeWebViewComponent;
3346
3442
  if (nativeWebViewLookupFailed) return null;
@@ -3390,6 +3486,7 @@ function PaywallWebView({
3390
3486
  products,
3391
3487
  primaryProductId,
3392
3488
  secondaryProductId,
3489
+ tertiaryProductId,
3393
3490
  isPurchasing,
3394
3491
  webUrl: webUrlProp,
3395
3492
  onPurchase,
@@ -3398,16 +3495,16 @@ function PaywallWebView({
3398
3495
  onReady,
3399
3496
  onError
3400
3497
  }) {
3401
- const paywallDataSent = useRef(false);
3402
- const loadEndReceived = useRef(false);
3498
+ const paywallDataSent = useRef2(false);
3499
+ const loadEndReceived = useRef2(false);
3403
3500
  const [scriptToInject, setScriptToInject] = useState(void 0);
3404
- const webViewRef = useRef(null);
3405
- const onErrorRef = useRef(onError);
3501
+ const webViewRef = useRef2(null);
3502
+ const onErrorRef = useRef2(onError);
3406
3503
  onErrorRef.current = onError;
3407
- const onReadyRef = useRef(onReady);
3504
+ const onReadyRef = useRef2(onReady);
3408
3505
  onReadyRef.current = onReady;
3409
3506
  const NativeWebView = useMemo(() => getNativeWebView(), []);
3410
- useEffect(() => {
3507
+ useEffect2(() => {
3411
3508
  if (!NativeWebView) {
3412
3509
  onErrorRef.current?.({
3413
3510
  code: -1,
@@ -3418,7 +3515,7 @@ function PaywallWebView({
3418
3515
  const [resolvedWebUrl, setResolvedWebUrl] = useState(
3419
3516
  () => webUrlProp ?? PaywalloClient.getWebUrl()
3420
3517
  );
3421
- useEffect(() => {
3518
+ useEffect2(() => {
3422
3519
  if (webUrlProp !== void 0) {
3423
3520
  setResolvedWebUrl(webUrlProp ?? null);
3424
3521
  return;
@@ -3437,7 +3534,7 @@ function PaywallWebView({
3437
3534
  }, 200);
3438
3535
  return () => clearInterval(interval);
3439
3536
  }, [webUrlProp]);
3440
- useEffect(() => {
3537
+ useEffect2(() => {
3441
3538
  if (!resolvedWebUrl) {
3442
3539
  onErrorRef.current?.({
3443
3540
  code: -1,
@@ -3461,13 +3558,14 @@ function PaywallWebView({
3461
3558
  craftData,
3462
3559
  products: Array.from(products.values()),
3463
3560
  primaryProductId,
3464
- secondaryProductId
3561
+ secondaryProductId,
3562
+ tertiaryProductId
3465
3563
  });
3466
3564
  injectScript(script);
3467
- }, [craftData, products, primaryProductId, secondaryProductId, injectScript]);
3468
- const processedIdsRef = useRef(/* @__PURE__ */ new Set());
3469
- const dedupeTimersRef = useRef(/* @__PURE__ */ new Map());
3470
- useEffect(() => {
3565
+ }, [craftData, products, primaryProductId, secondaryProductId, tertiaryProductId, injectScript]);
3566
+ const processedIdsRef = useRef2(/* @__PURE__ */ new Set());
3567
+ const dedupeTimersRef = useRef2(/* @__PURE__ */ new Map());
3568
+ useEffect2(() => {
3471
3569
  return () => {
3472
3570
  for (const timerId of dedupeTimersRef.current.values()) clearTimeout(timerId);
3473
3571
  dedupeTimersRef.current.clear();
@@ -3519,7 +3617,7 @@ function PaywallWebView({
3519
3617
  },
3520
3618
  [buildAndSetInjectionScript, onPurchase, onClose, onRestore]
3521
3619
  );
3522
- useEffect(() => {
3620
+ useEffect2(() => {
3523
3621
  const emitter = getWebViewEmitter();
3524
3622
  if (!emitter) return;
3525
3623
  const loadEndSub = emitter.addListener("PaywalloWebViewLoadEnd", () => {
@@ -3544,7 +3642,7 @@ function PaywallWebView({
3544
3642
  errorSub.remove();
3545
3643
  };
3546
3644
  }, [buildAndSetInjectionScript, handleParsedMessage]);
3547
- useEffect(() => {
3645
+ useEffect2(() => {
3548
3646
  const timer = setTimeout(() => {
3549
3647
  if (!paywallDataSent.current) {
3550
3648
  onErrorRef.current?.({
@@ -3584,11 +3682,11 @@ function PaywallWebView({
3584
3682
  },
3585
3683
  [handleParsedMessage]
3586
3684
  );
3587
- useEffect(() => {
3685
+ useEffect2(() => {
3588
3686
  if (!paywallDataSent.current) return;
3589
3687
  injectScript(buildPurchaseStateScript(isPurchasing));
3590
3688
  }, [isPurchasing, injectScript]);
3591
- useEffect(() => {
3689
+ useEffect2(() => {
3592
3690
  if (Platform9.OS !== "android") return;
3593
3691
  const onBackPress = () => {
3594
3692
  if (!paywallDataSent.current) return false;
@@ -3600,7 +3698,7 @@ function PaywallWebView({
3600
3698
  }, [injectScript]);
3601
3699
  if (!resolvedWebUrl || !NativeWebView) return null;
3602
3700
  const paywallUrl = `${resolvedWebUrl}/paywall/${paywallId}`;
3603
- return /* @__PURE__ */ jsx(View, { style: styles.container, children: /* @__PURE__ */ jsx(
3701
+ return /* @__PURE__ */ jsx2(View2, { style: styles2.container, children: /* @__PURE__ */ jsx2(
3604
3702
  NativeWebView,
3605
3703
  {
3606
3704
  ref: webViewRef,
@@ -3609,11 +3707,11 @@ function PaywallWebView({
3609
3707
  onMessage: handleMessage,
3610
3708
  onLoadEnd: handleLoadEnd,
3611
3709
  onError: handleNativeError,
3612
- style: styles.webview
3710
+ style: styles2.webview
3613
3711
  }
3614
3712
  ) });
3615
3713
  }
3616
- var NativeWebViewComponent, nativeWebViewLookupFailed, webViewEmitter, styles;
3714
+ var NativeWebViewComponent, nativeWebViewLookupFailed, webViewEmitter, styles2;
3617
3715
  var init_PaywallWebView = __esm({
3618
3716
  "src/domains/paywall/PaywallWebView.tsx"() {
3619
3717
  "use strict";
@@ -3623,7 +3721,7 @@ var init_PaywallWebView = __esm({
3623
3721
  NativeWebViewComponent = null;
3624
3722
  nativeWebViewLookupFailed = false;
3625
3723
  webViewEmitter = null;
3626
- styles = StyleSheet.create({
3724
+ styles2 = StyleSheet2.create({
3627
3725
  container: { flex: 1 },
3628
3726
  webview: { flex: 1, backgroundColor: "transparent" }
3629
3727
  });
@@ -5264,21 +5362,21 @@ var init_usePaywallTracking = __esm({
5264
5362
  });
5265
5363
 
5266
5364
  // 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";
5365
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
5366
+ import { Animated as Animated2, AppState as AppState3, Dimensions as Dimensions2 } from "react-native";
5269
5367
  function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId, openAnim, variantId) {
5270
5368
  const [isPurchasing, setIsPurchasing] = useState2(false);
5271
- const isPurchasingRef = useRef2(false);
5369
+ const isPurchasingRef = useRef3(false);
5272
5370
  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);
5371
+ const slideAnim = useRef3(new Animated2.Value(0)).current;
5372
+ const presentedAtRef = useRef3(Date.now());
5373
+ const closedRef = useRef3(false);
5276
5374
  const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
5277
5375
  const closeWithReason = useCallback3((reason, resultOverride) => {
5278
5376
  if (closedRef.current) return;
5279
5377
  closedRef.current = true;
5280
5378
  const animTarget = openAnim ?? slideAnim;
5281
- Animated.timing(animTarget, {
5379
+ Animated2.timing(animTarget, {
5282
5380
  toValue: Dimensions2.get("window").height,
5283
5381
  duration: 350,
5284
5382
  useNativeDriver: true
@@ -5298,7 +5396,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
5298
5396
  setIsClosing(true);
5299
5397
  closeWithReason("dismissed");
5300
5398
  }, [isClosing, closeWithReason]);
5301
- useEffect2(() => {
5399
+ useEffect3(() => {
5302
5400
  const sub = AppState3.addEventListener("change", (nextState) => {
5303
5401
  if (isPurchasingRef.current) return;
5304
5402
  if (nextState === "background" || nextState === "inactive") {
@@ -5307,7 +5405,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
5307
5405
  });
5308
5406
  return () => sub.remove();
5309
5407
  }, [closeWithReason]);
5310
- useEffect2(() => {
5408
+ useEffect3(() => {
5311
5409
  const timer = setTimeout(() => {
5312
5410
  closeWithReason("timeout");
5313
5411
  }, PAYWALL_TIMEOUT_MS);
@@ -5378,17 +5476,17 @@ var init_usePaywallActions = __esm({
5378
5476
  });
5379
5477
 
5380
5478
  // src/domains/paywall/usePaywallLoader.ts
5381
- import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
5479
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
5382
5480
  function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts, variantKey, campaignId, variantId) {
5383
5481
  const [error, setError] = useState3(null);
5384
5482
  const [paywallConfig, setPaywallConfig] = useState3(null);
5385
5483
  const [products, setProducts] = useState3(/* @__PURE__ */ new Map());
5386
- const preloadedConfigRef = useRef3(preloadedConfig);
5484
+ const preloadedConfigRef = useRef4(preloadedConfig);
5387
5485
  preloadedConfigRef.current = preloadedConfig;
5388
- const preloadedProductsRef = useRef3(preloadedProducts);
5486
+ const preloadedProductsRef = useRef4(preloadedProducts);
5389
5487
  preloadedProductsRef.current = preloadedProducts;
5390
- const trackingDoneRef = useRef3(false);
5391
- useEffect3(() => {
5488
+ const trackingDoneRef = useRef4(false);
5489
+ useEffect4(() => {
5392
5490
  if (!visible) {
5393
5491
  trackingDoneRef.current = false;
5394
5492
  return;
@@ -5407,7 +5505,8 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
5407
5505
  placement: snap.placement,
5408
5506
  config: snap.config,
5409
5507
  primaryProductId: snap.primaryProductId,
5410
- secondaryProductId: snap.secondaryProductId
5508
+ secondaryProductId: snap.secondaryProductId,
5509
+ tertiaryProductId: snap.tertiaryProductId
5411
5510
  };
5412
5511
  } else {
5413
5512
  const fetched = await apiClient.getPaywall(placement);
@@ -5419,6 +5518,7 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
5419
5518
  const productIds = [];
5420
5519
  if (config.primaryProductId) productIds.push(config.primaryProductId);
5421
5520
  if (config.secondaryProductId) productIds.push(config.secondaryProductId);
5521
+ if (config.tertiaryProductId) productIds.push(config.tertiaryProductId);
5422
5522
  const snapProducts = preloadedProductsRef.current;
5423
5523
  if (snapProducts && snapProducts.size > 0) {
5424
5524
  if (!cancelled) setProducts(snapProducts);
@@ -5483,28 +5583,28 @@ var init_usePaywallLoader = __esm({
5483
5583
  });
5484
5584
 
5485
5585
  // src/domains/paywall/PaywallModal.tsx
5486
- import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
5586
+ import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
5487
5587
  import {
5488
- Animated as Animated2,
5588
+ Animated as Animated3,
5489
5589
  Dimensions as Dimensions3,
5490
5590
  Easing,
5491
5591
  Modal,
5492
5592
  Pressable,
5493
- StyleSheet as StyleSheet2,
5593
+ StyleSheet as StyleSheet3,
5494
5594
  Text,
5495
- View as View2
5595
+ View as View3
5496
5596
  } from "react-native";
5497
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
5597
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
5498
5598
  function ErrorFallback({ description, onRetry, onClose }) {
5499
5599
  const overrides = PaywalloClient.getConfig()?.errorStrings;
5500
5600
  const title = overrides?.title ?? getSdkString("paywallErrorTitle");
5501
5601
  const retryLabel = overrides?.retry ?? getSdkString("paywallRetryButton");
5502
5602
  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 }) })
5603
+ return /* @__PURE__ */ jsx3(View3, { style: styles3.fallbackContainer, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.fallbackCard, children: [
5604
+ /* @__PURE__ */ jsx3(Text, { style: styles3.fallbackTitle, children: title }),
5605
+ /* @__PURE__ */ jsx3(Text, { style: styles3.fallbackDescription, children: description }),
5606
+ /* @__PURE__ */ jsx3(Pressable, { style: styles3.retryButton, onPress: onRetry, children: /* @__PURE__ */ jsx3(Text, { style: styles3.retryButtonText, children: retryLabel }) }),
5607
+ /* @__PURE__ */ jsx3(Pressable, { style: styles3.closeButton, onPress: onClose, children: /* @__PURE__ */ jsx3(Text, { style: styles3.closeButtonText, children: closeLabel }) })
5508
5608
  ] }) });
5509
5609
  }
5510
5610
  function PaywallModal({
@@ -5527,7 +5627,7 @@ function PaywallModal({
5527
5627
  variantId
5528
5628
  );
5529
5629
  const screenHeight = Dimensions3.get("window").height;
5530
- const openAnim = useRef4(new Animated2.Value(screenHeight)).current;
5630
+ const openAnim = useRef5(new Animated3.Value(screenHeight)).current;
5531
5631
  const [modalVisible, setModalVisible] = useState4(false);
5532
5632
  const handleResult = useCallback4((result) => {
5533
5633
  setModalVisible(false);
@@ -5536,7 +5636,9 @@ function PaywallModal({
5536
5636
  const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, handleResult, variantKey, campaignId, openAnim, variantId);
5537
5637
  const [webViewError, setWebViewError] = useState4(null);
5538
5638
  const [retryCount, setRetryCount] = useState4(0);
5539
- const retryCountRef = useRef4(0);
5639
+ const [showSkeleton, setShowSkeleton] = useState4(true);
5640
+ const skeletonOpacity = useRef5(new Animated3.Value(1)).current;
5641
+ const retryCountRef = useRef5(0);
5540
5642
  const handleWebViewError = useCallback4(
5541
5643
  (err) => {
5542
5644
  if (retryCountRef.current >= 1) {
@@ -5552,21 +5654,39 @@ function PaywallModal({
5552
5654
  retryCountRef.current = 0;
5553
5655
  setWebViewError(null);
5554
5656
  setRetryCount(0);
5555
- }, []);
5556
- useEffect4(() => {
5657
+ setShowSkeleton(true);
5658
+ skeletonOpacity.setValue(1);
5659
+ }, [skeletonOpacity]);
5660
+ const handleWebViewReady = useCallback4(() => {
5661
+ Animated3.timing(skeletonOpacity, {
5662
+ toValue: 0,
5663
+ duration: 250,
5664
+ easing: Easing.out(Easing.cubic),
5665
+ useNativeDriver: true
5666
+ }).start(() => setShowSkeleton(false));
5667
+ }, [skeletonOpacity]);
5668
+ useEffect5(() => {
5669
+ if (!visible || !showSkeleton) return;
5670
+ const timer = setTimeout(handleWebViewReady, 8e3);
5671
+ return () => clearTimeout(timer);
5672
+ }, [visible, showSkeleton, handleWebViewReady]);
5673
+ useEffect5(() => {
5557
5674
  if (visible) {
5558
5675
  setModalVisible(true);
5676
+ setShowSkeleton(true);
5677
+ skeletonOpacity.setValue(1);
5559
5678
  openAnim.setValue(Dimensions3.get("window").height);
5560
- Animated2.timing(openAnim, {
5679
+ Animated3.timing(openAnim, {
5561
5680
  toValue: 0,
5562
5681
  duration: 550,
5563
5682
  easing: Easing.out(Easing.cubic),
5564
5683
  useNativeDriver: true
5565
5684
  }).start();
5566
5685
  }
5567
- }, [visible, openAnim]);
5686
+ }, [visible, openAnim, skeletonOpacity]);
5568
5687
  if (!modalVisible && !visible) return null;
5569
- return /* @__PURE__ */ jsx2(
5688
+ const skeletonBackground = paywallConfig && "craftData" in paywallConfig.config ? extractFirstPageBackground(paywallConfig.config.craftData) : "#FFFFFF";
5689
+ return /* @__PURE__ */ jsx3(
5570
5690
  Modal,
5571
5691
  {
5572
5692
  visible: modalVisible,
@@ -5575,16 +5695,16 @@ function PaywallModal({
5575
5695
  onRequestClose: handleClose,
5576
5696
  transparent: true,
5577
5697
  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(
5698
+ 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: [
5699
+ error && /* @__PURE__ */ jsx3(View3, { style: styles3.errorContainer, children: /* @__PURE__ */ jsx3(Text, { style: styles3.errorText, children: error.message }) }),
5700
+ !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ jsx3(Fragment, { children: webViewError ? /* @__PURE__ */ jsx3(
5581
5701
  ErrorFallback,
5582
5702
  {
5583
5703
  description: webViewError.description,
5584
5704
  onRetry: handleRetry,
5585
5705
  onClose: handleClose
5586
5706
  }
5587
- ) : /* @__PURE__ */ jsx2(
5707
+ ) : /* @__PURE__ */ jsx3(
5588
5708
  PaywallWebView,
5589
5709
  {
5590
5710
  paywallId: paywallConfig.id,
@@ -5592,28 +5712,40 @@ function PaywallModal({
5592
5712
  products,
5593
5713
  primaryProductId: paywallConfig.primaryProductId,
5594
5714
  secondaryProductId: paywallConfig.secondaryProductId,
5715
+ tertiaryProductId: paywallConfig.tertiaryProductId,
5595
5716
  isPurchasing,
5717
+ onReady: handleWebViewReady,
5596
5718
  onClose: handleClose,
5597
5719
  onPurchase: handlePurchase,
5598
5720
  onRestore: handleRestore,
5599
5721
  onError: handleWebViewError
5600
5722
  },
5601
5723
  retryCount
5602
- ) })
5724
+ ) }),
5725
+ showSkeleton && !error && !webViewError && /* @__PURE__ */ jsx3(
5726
+ Animated3.View,
5727
+ {
5728
+ style: [StyleSheet3.absoluteFill, { opacity: skeletonOpacity }],
5729
+ pointerEvents: "none",
5730
+ children: /* @__PURE__ */ jsx3(PaywallSkeleton, { backgroundColor: skeletonBackground })
5731
+ }
5732
+ )
5603
5733
  ] }) }) })
5604
5734
  }
5605
5735
  );
5606
5736
  }
5607
- var styles2;
5737
+ var styles3;
5608
5738
  var init_PaywallModal = __esm({
5609
5739
  "src/domains/paywall/PaywallModal.tsx"() {
5610
5740
  "use strict";
5611
5741
  init_PaywalloClient();
5612
5742
  init_localization();
5743
+ init_extractFirstPageBackground();
5744
+ init_PaywallSkeleton();
5613
5745
  init_PaywallWebView();
5614
5746
  init_usePaywallActions();
5615
5747
  init_usePaywallLoader();
5616
- styles2 = StyleSheet2.create({
5748
+ styles3 = StyleSheet3.create({
5617
5749
  overlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.3)" },
5618
5750
  animatedContainer: { flex: 1, backgroundColor: "#fff" },
5619
5751
  container: { flex: 1, backgroundColor: "#fff" },
@@ -5669,13 +5801,177 @@ var init_PaywallModal = __esm({
5669
5801
  }
5670
5802
  });
5671
5803
 
5804
+ // src/domains/paywall/PaywallPreloadService.ts
5805
+ var PRELOAD_TTL_MS, STALE_THRESHOLD, PaywallPreloadService, paywallPreloadService;
5806
+ var init_PaywallPreloadService = __esm({
5807
+ "src/domains/paywall/PaywallPreloadService.ts"() {
5808
+ "use strict";
5809
+ init_PaywalloClient();
5810
+ init_IAPService();
5811
+ init_PaywallError();
5812
+ PRELOAD_TTL_MS = 5 * 60 * 1e3;
5813
+ STALE_THRESHOLD = 0.75;
5814
+ PaywallPreloadService = class {
5815
+ constructor() {
5816
+ this.preloaded = /* @__PURE__ */ new Map();
5817
+ this.activePreloads = /* @__PURE__ */ new Map();
5818
+ this.apiClientRef = null;
5819
+ }
5820
+ async preload(apiClient, placement) {
5821
+ this.apiClientRef = apiClient;
5822
+ const existing = this.activePreloads.get(placement);
5823
+ if (existing) return existing;
5824
+ const promise = this.doPreload(apiClient, placement);
5825
+ this.activePreloads.set(placement, promise);
5826
+ try {
5827
+ return await promise;
5828
+ } finally {
5829
+ this.activePreloads.delete(placement);
5830
+ }
5831
+ }
5832
+ async preloadMany(apiClient, placements) {
5833
+ this.apiClientRef = apiClient;
5834
+ if (placements.length === 0) return;
5835
+ const debug = PaywalloClient.getConfig()?.debug;
5836
+ if (debug) console.log(`[Paywallo PRELOAD] paywalls: ${placements.length} placements`);
5837
+ for (let i = 0; i < placements.length; i++) {
5838
+ if (i > 0) await new Promise((r) => setTimeout(r, 200));
5839
+ void this.preload(apiClient, placements[i]).catch((err) => {
5840
+ if (debug) console.log(`[Paywallo PRELOAD] paywall failed ${placements[i]}:`, err);
5841
+ });
5842
+ }
5843
+ }
5844
+ getPreloaded(placement) {
5845
+ const cached2 = this.preloaded.get(placement);
5846
+ if (!cached2) return null;
5847
+ const age = Date.now() - cached2.preloadedAt;
5848
+ if (age >= PRELOAD_TTL_MS) {
5849
+ this.preloaded.delete(placement);
5850
+ return null;
5851
+ }
5852
+ if (age >= PRELOAD_TTL_MS * STALE_THRESHOLD && this.apiClientRef) {
5853
+ void this.preload(this.apiClientRef, placement).catch(() => {
5854
+ });
5855
+ }
5856
+ return { config: cached2.config, products: new Map(cached2.products) };
5857
+ }
5858
+ isPreloaded(placement) {
5859
+ return this.getPreloaded(placement) !== null;
5860
+ }
5861
+ clear() {
5862
+ this.preloaded.clear();
5863
+ this.activePreloads.clear();
5864
+ }
5865
+ async doPreload(apiClient, placement) {
5866
+ try {
5867
+ const config = await apiClient.getPaywall(placement);
5868
+ if (!config) {
5869
+ return {
5870
+ success: false,
5871
+ error: new PaywallError(
5872
+ PAYWALL_ERROR_CODES.NOT_FOUND,
5873
+ `Paywall not found for placement: ${placement}`
5874
+ )
5875
+ };
5876
+ }
5877
+ const products = await this.loadProductsFor(config);
5878
+ this.preloaded.set(placement, { config, products, preloadedAt: Date.now() });
5879
+ if (PaywalloClient.getConfig()?.debug) {
5880
+ console.log(`[Paywallo PRELOAD] paywall cached \u2014 placement: ${placement}`);
5881
+ }
5882
+ return { success: true };
5883
+ } catch (error) {
5884
+ return {
5885
+ success: false,
5886
+ error: error instanceof Error ? error : new PaywallError(PAYWALL_ERROR_CODES.LOAD_FAILED, String(error))
5887
+ };
5888
+ }
5889
+ }
5890
+ async loadProductsFor(config) {
5891
+ const productIds = [];
5892
+ if (config.primaryProductId) productIds.push(config.primaryProductId);
5893
+ if (config.secondaryProductId) productIds.push(config.secondaryProductId);
5894
+ if (config.tertiaryProductId) productIds.push(config.tertiaryProductId);
5895
+ if (productIds.length === 0) return /* @__PURE__ */ new Map();
5896
+ try {
5897
+ const loaded = await getIAPService().loadProducts(productIds);
5898
+ const map = /* @__PURE__ */ new Map();
5899
+ for (const p of loaded) map.set(p.productId, p);
5900
+ return map;
5901
+ } catch (err) {
5902
+ if (PaywalloClient.getConfig()?.debug) {
5903
+ console.log("[Paywallo PRELOAD] products failed, caching config only:", err);
5904
+ }
5905
+ return /* @__PURE__ */ new Map();
5906
+ }
5907
+ }
5908
+ };
5909
+ paywallPreloadService = new PaywallPreloadService();
5910
+ }
5911
+ });
5912
+
5913
+ // src/domains/paywall/PaywallWebViewPrewarmer.tsx
5914
+ import { useEffect as useEffect6, useMemo as useMemo2, useState as useState5 } from "react";
5915
+ import { requireNativeComponent as requireNativeComponent2, StyleSheet as StyleSheet4, View as View4 } from "react-native";
5916
+ import { jsx as jsx4 } from "react/jsx-runtime";
5917
+ function getNativeWebView2() {
5918
+ if (NativeWebViewComponent2) return NativeWebViewComponent2;
5919
+ if (nativeLookupFailed) return null;
5920
+ try {
5921
+ NativeWebViewComponent2 = requireNativeComponent2("PaywalloWebView");
5922
+ return NativeWebViewComponent2;
5923
+ } catch {
5924
+ nativeLookupFailed = true;
5925
+ return null;
5926
+ }
5927
+ }
5928
+ function PaywallWebViewPrewarmer({
5929
+ webUrl,
5930
+ durationMs = DEFAULT_DURATION_MS
5931
+ }) {
5932
+ const NativeWebView = useMemo2(() => getNativeWebView2(), []);
5933
+ const [mounted, setMounted] = useState5(true);
5934
+ useEffect6(() => {
5935
+ if (!NativeWebView) return;
5936
+ const timer = setTimeout(() => setMounted(false), durationMs);
5937
+ return () => clearTimeout(timer);
5938
+ }, [NativeWebView, durationMs]);
5939
+ if (!NativeWebView || !mounted) return null;
5940
+ const preheatUrl = `${webUrl}/paywall/preheat`;
5941
+ if (PaywalloClient.getConfig()?.debug) {
5942
+ console.log(`[Paywallo PREWARM] mounting hidden WebView: ${preheatUrl}`);
5943
+ }
5944
+ return /* @__PURE__ */ jsx4(View4, { pointerEvents: "none", style: styles4.hidden, children: /* @__PURE__ */ jsx4(NativeWebView, { sourceUrl: preheatUrl, style: styles4.fill }) });
5945
+ }
5946
+ var NativeWebViewComponent2, nativeLookupFailed, DEFAULT_DURATION_MS, styles4;
5947
+ var init_PaywallWebViewPrewarmer = __esm({
5948
+ "src/domains/paywall/PaywallWebViewPrewarmer.tsx"() {
5949
+ "use strict";
5950
+ init_PaywalloClient();
5951
+ NativeWebViewComponent2 = null;
5952
+ nativeLookupFailed = false;
5953
+ DEFAULT_DURATION_MS = 60 * 1e3;
5954
+ styles4 = StyleSheet4.create({
5955
+ hidden: {
5956
+ height: 1,
5957
+ left: -9999,
5958
+ opacity: 0,
5959
+ position: "absolute",
5960
+ top: -9999,
5961
+ width: 1
5962
+ },
5963
+ fill: { flex: 1 }
5964
+ });
5965
+ }
5966
+ });
5967
+
5672
5968
  // src/domains/paywall/usePaywallSession.ts
5673
- import { useCallback as useCallback5, useEffect as useEffect5, useRef as useRef5 } from "react";
5969
+ import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef6 } from "react";
5674
5970
  function usePaywallSession(options) {
5675
- const closedRef = useRef5(false);
5676
- const optsRef = useRef5(options);
5971
+ const closedRef = useRef6(false);
5972
+ const optsRef = useRef6(options);
5677
5973
  optsRef.current = options;
5678
- useEffect5(() => {
5974
+ useEffect7(() => {
5679
5975
  if (!options) return;
5680
5976
  closedRef.current = false;
5681
5977
  startHeartbeat({
@@ -5719,11 +6015,11 @@ var init_usePaywallSession = __esm({
5719
6015
  });
5720
6016
 
5721
6017
  // src/domains/paywall/usePaywallPresenter.ts
5722
- import { useCallback as useCallback6, useEffect as useEffect6, useRef as useRef6, useState as useState5 } from "react";
6018
+ import { useCallback as useCallback6, useEffect as useEffect8, useRef as useRef7, useState as useState6 } from "react";
5723
6019
  function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
5724
- const [showingPreloadedWebView, setShowingPreloadedWebView] = useState5(false);
5725
- const resolverRef = useRef6(null);
5726
- const presentedAtRef = useRef6(Date.now());
6020
+ const [showingPreloadedWebView, setShowingPreloadedWebView] = useState6(false);
6021
+ const resolverRef = useRef7(null);
6022
+ const presentedAtRef = useRef7(Date.now());
5727
6023
  const sessionOpts = showingPreloadedWebView && preloadedWebView ? {
5728
6024
  placement: preloadedWebView.placement,
5729
6025
  paywallId: preloadedWebView.paywallId,
@@ -5733,7 +6029,7 @@ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
5733
6029
  ...preloadedWebView.campaignId && { campaignId: preloadedWebView.campaignId }
5734
6030
  } : null;
5735
6031
  const { markClosed } = usePaywallSession(sessionOpts);
5736
- useEffect6(() => {
6032
+ useEffect8(() => {
5737
6033
  return () => {
5738
6034
  if (resolverRef.current) {
5739
6035
  resolverRef.current({ presented: false, purchased: false, cancelled: true, restored: false });
@@ -5820,7 +6116,9 @@ var init_paywall = __esm({
5820
6116
  init_PaywallError();
5821
6117
  init_PaywallErrorBoundary();
5822
6118
  init_PaywallModal();
6119
+ init_PaywallPreloadService();
5823
6120
  init_PaywallWebView();
6121
+ init_PaywallWebViewPrewarmer();
5824
6122
  init_usePaywallPresenter();
5825
6123
  init_usePaywallTracking();
5826
6124
  }
@@ -7811,6 +8109,8 @@ var init_ApiClient = __esm({
7811
8109
  }
7812
8110
  }
7813
8111
  const attribution = Object.keys(rawAttribution).length > 0 ? rawAttribution : void 0;
8112
+ const rawUserId = properties?.["userId"];
8113
+ const externalUserId = typeof rawUserId === "string" && rawUserId.length > 0 ? rawUserId : void 0;
7814
8114
  await postWithQueue(
7815
8115
  {
7816
8116
  isOfflineQueueEnabled: () => this.offlineQueueEnabled,
@@ -7821,6 +8121,7 @@ var init_ApiClient = __esm({
7821
8121
  "/sdk/identity/identify",
7822
8122
  {
7823
8123
  distinct_id: distinctId,
8124
+ ...externalUserId && { external_user_id: externalUserId },
7824
8125
  traits,
7825
8126
  ...attribution && { attribution },
7826
8127
  ...deviceId && { deviceId },
@@ -8392,6 +8693,7 @@ async function doFullReset(state) {
8392
8693
  offlineQueue.dispose();
8393
8694
  networkMonitor.dispose();
8394
8695
  campaignGateService.clearPreloaded();
8696
+ paywallPreloadService.clear();
8395
8697
  await notificationsManager.invalidateLocalToken().catch(() => void 0);
8396
8698
  notificationsManager.destroy();
8397
8699
  deepLinkAttributionCapture.stop();
@@ -8426,6 +8728,7 @@ var init_PaywalloLifecycle = __esm({
8426
8728
  init_identity();
8427
8729
  init_DeepLinkAttributionCapture();
8428
8730
  init_notifications();
8731
+ init_paywall();
8429
8732
  init_session();
8430
8733
  init_SubscriptionCache();
8431
8734
  init_network();
@@ -8729,6 +9032,15 @@ var init_PaywalloClient = __esm({
8729
9032
  isPreloaded(placement) {
8730
9033
  return campaignGateService.getPreloadedCampaign(placement) !== null;
8731
9034
  }
9035
+ async preloadPaywall(placement) {
9036
+ return paywallPreloadService.preload(this.getApiClientOrThrow(), placement);
9037
+ }
9038
+ async preloadPaywalls(placements) {
9039
+ return paywallPreloadService.preloadMany(this.getApiClientOrThrow(), placements);
9040
+ }
9041
+ isPaywallPreloaded(placement) {
9042
+ return paywallPreloadService.isPreloaded(placement);
9043
+ }
8732
9044
  getAutoPreloadedPlacement() {
8733
9045
  return this.state.autoPreloadedPlacement;
8734
9046
  }
@@ -8943,7 +9255,7 @@ init_session();
8943
9255
  init_PaywalloError();
8944
9256
 
8945
9257
  // src/hooks/useOfferingPurchase.ts
8946
- import { useCallback as useCallback7, useState as useState6 } from "react";
9258
+ import { useCallback as useCallback7, useState as useState7 } from "react";
8947
9259
 
8948
9260
  // src/domains/plan/invalidateCache.ts
8949
9261
  async function invalidatePlanCache() {
@@ -8956,7 +9268,7 @@ async function invalidatePlanCache() {
8956
9268
 
8957
9269
  // src/hooks/useOfferingPurchase.ts
8958
9270
  function useOfferingPurchase() {
8959
- const [state, setState] = useState6("idle");
9271
+ const [state, setState] = useState7("idle");
8960
9272
  const purchase = useCallback7(async (productId) => {
8961
9273
  setState("purchasing");
8962
9274
  try {
@@ -8981,7 +9293,7 @@ function useOfferingPurchase() {
8981
9293
  }
8982
9294
 
8983
9295
  // src/hooks/useOfferings.ts
8984
- import { useCallback as useCallback8, useEffect as useEffect7, useState as useState7 } from "react";
9296
+ import { useCallback as useCallback8, useEffect as useEffect9, useState as useState8 } from "react";
8985
9297
  async function getOfferingService() {
8986
9298
  try {
8987
9299
  const mod = await Promise.resolve().then(() => (init_OfferingService(), OfferingService_exports));
@@ -8991,8 +9303,8 @@ async function getOfferingService() {
8991
9303
  }
8992
9304
  }
8993
9305
  function useOfferings(ids) {
8994
- const [offerings, setOfferings] = useState7([]);
8995
- const [isLoading, setIsLoading] = useState7(true);
9306
+ const [offerings, setOfferings] = useState8([]);
9307
+ const [isLoading, setIsLoading] = useState8(true);
8996
9308
  const idsKey = ids?.join(",") ?? "";
8997
9309
  const fetchOfferings = useCallback8(async () => {
8998
9310
  setIsLoading(true);
@@ -9010,7 +9322,7 @@ function useOfferings(ids) {
9010
9322
  setIsLoading(false);
9011
9323
  }
9012
9324
  }, [idsKey]);
9013
- useEffect7(() => {
9325
+ useEffect9(() => {
9014
9326
  void fetchOfferings();
9015
9327
  }, [fetchOfferings]);
9016
9328
  return { offerings, isLoading };
@@ -9047,10 +9359,10 @@ function usePaywallo() {
9047
9359
  }
9048
9360
 
9049
9361
  // src/hooks/usePlanPurchase.ts
9050
- import { useCallback as useCallback10, useState as useState8 } from "react";
9362
+ import { useCallback as useCallback10, useState as useState9 } from "react";
9051
9363
  function usePlanPurchase() {
9052
- const [state, setState] = useState8("idle");
9053
- const [error, setError] = useState8(null);
9364
+ const [state, setState] = useState9("idle");
9365
+ const [error, setError] = useState9(null);
9054
9366
  const purchase = useCallback10(async (productId) => {
9055
9367
  setState("purchasing");
9056
9368
  setError(null);
@@ -9103,7 +9415,7 @@ function usePlanPurchase() {
9103
9415
  }
9104
9416
 
9105
9417
  // src/hooks/usePlans.ts
9106
- import { useCallback as useCallback11, useEffect as useEffect8, useState as useState9 } from "react";
9418
+ import { useCallback as useCallback11, useEffect as useEffect10, useState as useState10 } from "react";
9107
9419
  async function getPlanService() {
9108
9420
  try {
9109
9421
  const mod = await Promise.resolve().then(() => (init_PlanService(), PlanService_exports));
@@ -9113,10 +9425,10 @@ async function getPlanService() {
9113
9425
  }
9114
9426
  }
9115
9427
  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);
9428
+ const [allPlans, setAllPlans] = useState10([]);
9429
+ const [currentPlan, setCurrentPlan] = useState10(null);
9430
+ const [isLoading, setIsLoading] = useState10(true);
9431
+ const [error, setError] = useState10(null);
9120
9432
  const fetchPlans = useCallback11(async (forceRefresh = false) => {
9121
9433
  setIsLoading(true);
9122
9434
  setError(null);
@@ -9141,14 +9453,14 @@ function usePlans() {
9141
9453
  const refresh = useCallback11(() => {
9142
9454
  return fetchPlans(true);
9143
9455
  }, [fetchPlans]);
9144
- useEffect8(() => {
9456
+ useEffect10(() => {
9145
9457
  void fetchPlans(false);
9146
9458
  }, [fetchPlans]);
9147
9459
  return { currentPlan, allPlans, isLoading, error, refresh };
9148
9460
  }
9149
9461
 
9150
9462
  // src/hooks/useProducts.ts
9151
- import { useCallback as useCallback12, useEffect as useEffect9, useState as useState10 } from "react";
9463
+ import { useCallback as useCallback12, useEffect as useEffect11, useState as useState11 } from "react";
9152
9464
 
9153
9465
  // src/utils/productMappers.ts
9154
9466
  function mapToIAPProduct(product) {
@@ -9187,10 +9499,10 @@ function mapToFormattedProduct(product) {
9187
9499
 
9188
9500
  // src/hooks/useProducts.ts
9189
9501
  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);
9502
+ const [products, setProducts] = useState11([]);
9503
+ const [formattedProducts, setFormattedProducts] = useState11([]);
9504
+ const [isLoading, setIsLoading] = useState11(false);
9505
+ const [error, setError] = useState11(null);
9194
9506
  const loadProducts = useCallback12(async (productIds) => {
9195
9507
  setIsLoading(true);
9196
9508
  setError(null);
@@ -9205,7 +9517,7 @@ function useProducts(initialProductIds) {
9205
9517
  setIsLoading(false);
9206
9518
  }
9207
9519
  }, []);
9208
- useEffect9(() => {
9520
+ useEffect11(() => {
9209
9521
  if (initialProductIds && initialProductIds.length > 0) {
9210
9522
  void loadProducts(initialProductIds);
9211
9523
  }
@@ -9251,13 +9563,13 @@ function useProducts(initialProductIds) {
9251
9563
  }
9252
9564
 
9253
9565
  // src/hooks/usePurchase.ts
9254
- import { useCallback as useCallback13, useState as useState11 } from "react";
9566
+ import { useCallback as useCallback13, useState as useState12 } from "react";
9255
9567
  function toIAPPurchase(p) {
9256
9568
  return { productId: p.productId, transactionId: p.transactionId, transactionDate: p.transactionDate, transactionReceipt: p.receipt };
9257
9569
  }
9258
9570
  function usePurchase() {
9259
- const [state, setState] = useState11("idle");
9260
- const [error, setError] = useState11(null);
9571
+ const [state, setState] = useState12("idle");
9572
+ const [error, setError] = useState12(null);
9261
9573
  const purchase = useCallback13(async (productId) => {
9262
9574
  setState("purchasing");
9263
9575
  setError(null);
@@ -9305,13 +9617,13 @@ function usePurchase() {
9305
9617
  }
9306
9618
 
9307
9619
  // src/hooks/useSubscription.ts
9308
- import { useCallback as useCallback14, useEffect as useEffect10, useState as useState12 } from "react";
9620
+ import { useCallback as useCallback14, useEffect as useEffect12, useState as useState13 } from "react";
9309
9621
  init_SubscriptionManager();
9310
9622
  var EMPTY_ENTITLEMENTS = [];
9311
9623
  function useSubscription() {
9312
- const [status, setStatus] = useState12(null);
9313
- const [isLoading, setIsLoading] = useState12(true);
9314
- const [error, setError] = useState12(null);
9624
+ const [status, setStatus] = useState13(null);
9625
+ const [isLoading, setIsLoading] = useState13(true);
9626
+ const [error, setError] = useState13(null);
9315
9627
  const refresh = useCallback14(async () => {
9316
9628
  setIsLoading(true);
9317
9629
  setError(null);
@@ -9338,7 +9650,7 @@ function useSubscription() {
9338
9650
  setIsLoading(false);
9339
9651
  }
9340
9652
  }, []);
9341
- useEffect10(() => {
9653
+ useEffect12(() => {
9342
9654
  void subscriptionManager.getSubscriptionStatus().then((s) => {
9343
9655
  setStatus(s);
9344
9656
  setIsLoading(false);
@@ -9366,94 +9678,288 @@ function useSubscription() {
9366
9678
  }
9367
9679
 
9368
9680
  // 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";
9681
+ import { useCallback as useCallback19, useEffect as useEffect14, useLayoutEffect, useMemo as useMemo3, useRef as useRef11, useState as useState17 } from "react";
9682
+ import { Animated as Animated4, Dimensions as Dimensions5, Easing as Easing2, StyleSheet as StyleSheet5 } from "react-native";
9371
9683
  init_PaywalloClient();
9372
- init_SecureStorage();
9373
9684
  init_NativeStorage();
9685
+ init_SecureStorage();
9374
9686
  init_campaign();
9375
9687
  init_paywall();
9376
9688
  init_paywall();
9377
9689
  init_paywall();
9378
9690
  init_paywall();
9379
- init_session();
9380
- init_ClientError();
9691
+ init_paywall();
9692
+ init_paywall();
9381
9693
 
9382
- // src/hooks/useAppAutoEvents.ts
9383
- import { useEffect as useEffect11, useRef as useRef7 } from "react";
9694
+ // src/domains/paywall/SuperwallAutoBridge.ts
9695
+ init_PaywalloClient();
9384
9696
 
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";
9697
+ // src/core/utils/eventId.ts
9698
+ function hashString(input) {
9699
+ let h1 = 3735928559;
9700
+ let h2 = 1103547991;
9701
+ for (let i = 0; i < input.length; i++) {
9702
+ const ch = input.charCodeAt(i);
9703
+ h1 = Math.imul(h1 ^ ch, 2654435769);
9704
+ h2 = Math.imul(h2 ^ ch, 1598716949);
9392
9705
  }
9706
+ h1 = Math.imul(h1 ^ h1 >>> 16, 73244475);
9707
+ h1 ^= Math.imul(h2 ^ h2 >>> 13, 351452291);
9708
+ h2 = Math.imul(h2 ^ h2 >>> 16, 73244475);
9709
+ h2 ^= Math.imul(h1 ^ h1 >>> 13, 351452291);
9710
+ const toHex = (n) => (n >>> 0).toString(16).padStart(8, "0");
9711
+ return toHex(h1) + toHex(h2);
9393
9712
  }
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
- }
9713
+ function buildDeterministicEventId(parts) {
9714
+ const raw = hashString(parts.join("|"));
9715
+ const p1 = raw.slice(0, 8);
9716
+ const p2 = raw.slice(8, 12);
9717
+ const p3 = "4" + raw.slice(13, 16);
9718
+ const p4 = (parseInt(raw[16], 16) & 3 | 8).toString(16) + raw.slice(17, 20);
9719
+ const p5 = raw.padEnd(32, "0").slice(20, 32);
9720
+ return `${p1}-${p2}-${p3}-${p4}-${p5}`;
9402
9721
  }
9403
9722
 
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]);
9723
+ // src/domains/paywall/SuperwallAutoBridge.ts
9724
+ var bridgeStarted = false;
9725
+ var unsubscribe = null;
9726
+ var debugEnabled = false;
9727
+ function log(msg, data) {
9728
+ if (!debugEnabled) return;
9729
+ if (data !== void 0) {
9730
+ console.log(`[Paywallo:SuperwallBridge] ${msg}`, data);
9731
+ } else {
9732
+ console.log(`[Paywallo:SuperwallBridge] ${msg}`);
9733
+ }
9447
9734
  }
9735
+ function minuteBucket(ts) {
9736
+ return String(Math.floor(ts / 6e4));
9737
+ }
9738
+ function buildEventId(type, identifier, ts) {
9739
+ return buildDeterministicEventId(["sw", type, identifier, minuteBucket(ts)]);
9740
+ }
9741
+ async function trackPaywallEvent(type, identifier, ts, extra) {
9742
+ const _eventId = buildEventId(type, identifier, ts);
9743
+ try {
9744
+ const cleanExtra = {};
9745
+ if (extra) {
9746
+ for (const [k, v] of Object.entries(extra)) if (v !== void 0) cleanExtra[k] = v;
9747
+ }
9748
+ await PaywalloClient.track("paywall", {
9749
+ priority: "critical",
9750
+ properties: {
9751
+ type,
9752
+ paywall_id: identifier,
9753
+ ...cleanExtra
9754
+ }
9755
+ });
9756
+ } catch (err) {
9757
+ log("track error", { type, identifier, eventId: _eventId, err: String(err) });
9758
+ }
9759
+ }
9760
+ function mapDismissToCloseReason(resultType) {
9761
+ switch (resultType) {
9762
+ case "purchased":
9763
+ return "purchased";
9764
+ case "restored":
9765
+ return "purchased";
9766
+ // restore counts as a purchase outcome for funnel
9767
+ case "declined":
9768
+ return "dismissed";
9769
+ default:
9770
+ return void 0;
9771
+ }
9772
+ }
9773
+ function buildCallbacks() {
9774
+ return {
9775
+ onPaywallPresent: (info) => {
9776
+ const ts = Date.now();
9777
+ log("onPaywallPresent", { identifier: info.identifier });
9778
+ void trackPaywallEvent("viewed", info.identifier, ts, {
9779
+ placement: info.presentedByEventWithName,
9780
+ variant_id: info.experiment?.variant?.id,
9781
+ campaign_id: info.experiment?.id
9782
+ });
9783
+ },
9784
+ onPaywallDismiss: (info, result) => {
9785
+ const ts = Date.now();
9786
+ log("onPaywallDismiss", { identifier: info.identifier, result: result.type });
9787
+ void trackPaywallEvent("closed", info.identifier, ts, {
9788
+ placement: info.presentedByEventWithName,
9789
+ variant_id: info.experiment?.variant?.id,
9790
+ campaign_id: info.experiment?.id,
9791
+ close_reason: mapDismissToCloseReason(result.type),
9792
+ closed_at: new Date(ts).toISOString()
9793
+ });
9794
+ },
9795
+ onPurchase: (params) => {
9796
+ const ts = Date.now();
9797
+ const safe = params !== null && typeof params === "object" ? params : {};
9798
+ const productId = "productId" in safe && typeof safe.productId === "string" ? safe.productId : "unknown";
9799
+ const identifier = "paywallIdentifier" in safe && typeof safe.paywallIdentifier === "string" ? safe.paywallIdentifier : productId;
9800
+ log("onPurchase", { productId, identifier });
9801
+ void trackPaywallEvent("purchased", identifier, ts, { product_id: productId });
9802
+ },
9803
+ // Global Superwall event listener — used specifically for `transactionComplete`
9804
+ // which is the only event that exposes the full product (currency/price) +
9805
+ // transaction (storeTransactionId) shape needed to create a server-side
9806
+ // `transactions` row. Other events are skipped (paywallOpen/Dismiss already
9807
+ // handled above with their typed callbacks).
9808
+ onSuperwallEvent: (info) => {
9809
+ const ev = info?.event;
9810
+ if (!ev || ev.event !== "transactionComplete") return;
9811
+ const ts = Date.now();
9812
+ const product = ev.product;
9813
+ const transaction = ev.transaction;
9814
+ if (!product) {
9815
+ log("transactionComplete missing product \u2014 skipping transaction track");
9816
+ return;
9817
+ }
9818
+ const productId = product.productIdentifier ?? product.id ?? "unknown";
9819
+ const currency = product.currencyCode ?? "USD";
9820
+ const price = product.price ?? 0;
9821
+ const transactionId = transaction?.storeTransactionId ?? transaction?.originalTransactionIdentifier ?? `${productId}_${String(ts)}`;
9822
+ const isTrial = product.hasFreeTrial === true || (product.trialPeriodPrice ?? 0) > 0;
9823
+ const paywallIdentifier = ev.paywallInfo?.identifier;
9824
+ log("onSuperwallEvent: transactionComplete", { productId, currency, price, transactionId, isTrial });
9825
+ void (async () => {
9826
+ try {
9827
+ await PaywalloClient.track("transaction", {
9828
+ priority: "critical",
9829
+ properties: {
9830
+ type: isTrial ? "trial_started" : "completed",
9831
+ // Server's IngestTranslator looks for `transaction_id`/`product_id`
9832
+ // /`amount`/`currency`/`paywall_id` keys (not `tx_id`). PurchaseHandler
9833
+ // then resolves type=trial/new/renewal based on amount being 0 vs >0.
9834
+ transaction_id: transactionId,
9835
+ product_id: productId,
9836
+ amount: price,
9837
+ currency,
9838
+ ...paywallIdentifier ? { paywall_id: paywallIdentifier } : {}
9839
+ }
9840
+ });
9841
+ } catch (err) {
9842
+ log("transactionComplete track error", { err: String(err) });
9843
+ }
9844
+ })();
9845
+ }
9846
+ };
9847
+ }
9848
+ function startSuperwallAutoBridge(debug = false) {
9849
+ debugEnabled = debug;
9850
+ if (bridgeStarted) {
9851
+ log("already started, skipping");
9852
+ return;
9853
+ }
9854
+ void detectAndStart();
9855
+ }
9856
+ async function detectAndStart() {
9857
+ let subscribeFn;
9858
+ try {
9859
+ const mod = await import("expo-superwall");
9860
+ if (!mod || typeof mod.subscribeToSuperwallEvents !== "function") {
9861
+ const internal = await import("expo-superwall/build/src/internal/superwallEventBridge");
9862
+ if (typeof internal.subscribeToSuperwallEvents !== "function") {
9863
+ log("expo-superwall subscribeToSuperwallEvents not found \u2014 bridge inactive");
9864
+ return;
9865
+ }
9866
+ subscribeFn = internal.subscribeToSuperwallEvents;
9867
+ } else {
9868
+ subscribeFn = mod.subscribeToSuperwallEvents;
9869
+ }
9870
+ } catch {
9871
+ log("expo-superwall not installed \u2014 bridge inactive");
9872
+ return;
9873
+ }
9874
+ try {
9875
+ const callbacks = buildCallbacks();
9876
+ unsubscribe = subscribeFn(void 0, () => callbacks);
9877
+ bridgeStarted = true;
9878
+ log("bridge started");
9879
+ } catch (err) {
9880
+ log("subscribe failed", { err: String(err) });
9881
+ }
9882
+ }
9883
+
9884
+ // src/PaywalloProvider.tsx
9885
+ init_session();
9886
+ init_ClientError();
9887
+
9888
+ // src/hooks/useAppAutoEvents.ts
9889
+ import { useEffect as useEffect13, useRef as useRef8 } from "react";
9890
+
9891
+ // src/utils/platformDetection.ts
9892
+ async function detectPlatform3() {
9893
+ try {
9894
+ const rn = await import("react-native");
9895
+ return rn.Platform?.OS === "ios" ? "ios" : "android";
9896
+ } catch {
9897
+ return "android";
9898
+ }
9899
+ }
9900
+ async function detectOsVersion() {
9901
+ try {
9902
+ const rn = await import("react-native");
9903
+ const version = rn.Platform?.Version;
9904
+ return version != null ? String(version) : "unknown";
9905
+ } catch {
9906
+ return "unknown";
9907
+ }
9908
+ }
9909
+
9910
+ // src/hooks/useAppAutoEvents.ts
9911
+ var FIRST_SEEN_KEY = "@paywallo:firstSeen_v226";
9912
+ var DEBOUNCE_MS = 1e3;
9913
+ function useAppAutoEvents(config) {
9914
+ const { isInitialized, getAppVersion: getAppVersion2, getSessionId, emitLifecycle, storageGet, storageSet, debug } = config;
9915
+ const didRunRef = useRef8(false);
9916
+ useEffect13(() => {
9917
+ if (debug) console.log("[Paywallo EVENTS] useEffect fired", { isInitialized, didRunRefCurrent: didRunRef.current });
9918
+ if (!isInitialized || didRunRef.current) return;
9919
+ const timer = setTimeout(() => {
9920
+ if (didRunRef.current) return;
9921
+ didRunRef.current = true;
9922
+ void (async () => {
9923
+ const sessionId = getSessionId();
9924
+ const appVersion = getAppVersion2();
9925
+ const deviceType = await detectPlatform3();
9926
+ const osVersion = await detectOsVersion();
9927
+ if (debug) console.log("[Paywallo EVENTS] computed device context", { deviceType, osVersion, appVersion, sessionId, hasSessionId: !!sessionId });
9928
+ try {
9929
+ if (debug) console.log("[Paywallo EVENTS] checking install \u2014 about to read FIRST_SEEN_KEY");
9930
+ const firstSeen = await storageGet(FIRST_SEEN_KEY);
9931
+ if (!firstSeen) {
9932
+ await emitLifecycle({ type: "install", device_type: deviceType, os_version: osVersion, app_version: appVersion, ...sessionId ? { session_id: sessionId } : {} }, "critical");
9933
+ await storageSet(FIRST_SEEN_KEY, (/* @__PURE__ */ new Date()).toISOString());
9934
+ if (debug) console.log("[Paywallo EVENTS] install event emitted (first time)");
9935
+ if (debug) console.log("[Paywallo EVENTS] lifecycle install emitted");
9936
+ } else {
9937
+ if (debug) console.log("[Paywallo EVENTS] install skipped \u2014 firstSeen already exists", { firstSeen });
9938
+ }
9939
+ } catch (err) {
9940
+ if (debug) console.log("[Paywallo EVENTS] lifecycle install check failed", err);
9941
+ }
9942
+ try {
9943
+ if (debug) console.log("[Paywallo EVENTS] emitting cold_start");
9944
+ await emitLifecycle({ type: "cold_start", app_version: appVersion, device_type: deviceType, os_version: osVersion, ...sessionId ? { session_id: sessionId } : {} }, "normal");
9945
+ if (debug) console.log("[Paywallo EVENTS] lifecycle cold_start emitted");
9946
+ } catch (err) {
9947
+ if (debug) console.log("[Paywallo EVENTS] lifecycle cold_start failed", err);
9948
+ }
9949
+ })();
9950
+ }, DEBOUNCE_MS);
9951
+ return () => clearTimeout(timer);
9952
+ }, [isInitialized]);
9953
+ }
9954
+
9955
+ // src/hooks/useCampaignPreload.ts
9956
+ init_PaywalloClient();
9957
+ init_CampaignError();
9958
+ import { useCallback as useCallback15, useRef as useRef9, useState as useState14 } from "react";
9959
+
9960
+ // src/utils/loadCampaignProducts.ts
9961
+ init_IAPService();
9448
9962
 
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
9963
  // src/utils/serverProduct.ts
9458
9964
  var PERIOD_MAP = {
9459
9965
  P1Y: "ano",
@@ -9518,8 +10024,8 @@ function getCacheEntry(cache, placement) {
9518
10024
 
9519
10025
  // src/hooks/useCampaignPreload.ts
9520
10026
  function useCampaignPreload() {
9521
- const [preloadedWebView, setPreloadedWebView] = useState13(null);
9522
- const preloadedCampaignsRef = useRef8(/* @__PURE__ */ new Map());
10027
+ const [preloadedWebView, setPreloadedWebView] = useState14(null);
10028
+ const preloadedCampaignsRef = useRef9(/* @__PURE__ */ new Map());
9523
10029
  const isPreloadValid = useCallback15(
9524
10030
  (placement) => isCacheEntryValid(preloadedCampaignsRef.current.get(placement)),
9525
10031
  []
@@ -9597,10 +10103,10 @@ function useCampaignPreload() {
9597
10103
  }
9598
10104
 
9599
10105
  // src/hooks/useProductLoader.ts
9600
- import { useCallback as useCallback16, useState as useState14 } from "react";
10106
+ import { useCallback as useCallback16, useState as useState15 } from "react";
9601
10107
  function useProductLoader() {
9602
- const [products, setProducts] = useState14(/* @__PURE__ */ new Map());
9603
- const [isLoadingProducts, setIsLoadingProducts] = useState14(false);
10108
+ const [products, setProducts] = useState15(/* @__PURE__ */ new Map());
10109
+ const [isLoadingProducts, setIsLoadingProducts] = useState15(false);
9604
10110
  const refreshProducts = useCallback16(async (productIds) => {
9605
10111
  setIsLoadingProducts(true);
9606
10112
  try {
@@ -9686,7 +10192,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
9686
10192
  // src/hooks/useSubscriptionSync.ts
9687
10193
  init_PaywalloClient();
9688
10194
  init_SubscriptionCache();
9689
- import { useCallback as useCallback18, useRef as useRef9, useState as useState15 } from "react";
10195
+ import { useCallback as useCallback18, useRef as useRef10, useState as useState16 } from "react";
9690
10196
 
9691
10197
  // src/hooks/subscriptionSyncHelpers.ts
9692
10198
  init_SubscriptionCache();
@@ -9794,8 +10300,8 @@ async function checkPersistedCache(distinctId, onHit) {
9794
10300
 
9795
10301
  // src/hooks/useSubscriptionSync.ts
9796
10302
  function useSubscriptionSync() {
9797
- const [subscription, setSubscription] = useState15(null);
9798
- const cacheRef = useRef9(null);
10303
+ const [subscription, setSubscription] = useState16(null);
10304
+ const cacheRef = useRef10(null);
9799
10305
  const setCacheAndState = useCallback18((sub) => {
9800
10306
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
9801
10307
  setSubscription(sub);
@@ -9856,214 +10362,24 @@ function useSubscriptionSync() {
9856
10362
  return { subscription, hasActiveSubscription, getSubscription, invalidateCache };
9857
10363
  }
9858
10364
 
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
10365
  // src/PaywalloProvider.tsx
10050
10366
  init_localization();
10051
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
10367
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
10052
10368
  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);
10369
+ const [isInitialized, setIsInitialized] = useState17(false);
10370
+ const [initError, setInitError] = useState17(null);
10371
+ const [distinctId, setDistinctId] = useState17(null);
10372
+ const [activePaywall, setActivePaywall] = useState17(null);
10057
10373
  const { products, isLoadingProducts, refreshProducts } = useProductLoader();
10058
10374
  const { subscription, hasActiveSubscription, getSubscription, invalidateCache } = useSubscriptionSync();
10059
10375
  const { preloadedWebView, setPreloadedWebView, preloadCampaign, consumePreloadedCampaign, isPreloadValid } = useCampaignPreload();
10060
10376
  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(() => {
10377
+ const autoPreloadTriggeredRef = useRef11(false);
10378
+ const preloadedWebViewRef = useRef11(preloadedWebView);
10379
+ const pollTimerRef = useRef11(null);
10380
+ const pollCancelledRef = useRef11(false);
10381
+ const secureStorageRef = useRef11(new SecureStorage(config.debug));
10382
+ useEffect14(() => {
10067
10383
  preloadedWebViewRef.current = preloadedWebView;
10068
10384
  }, [preloadedWebView]);
10069
10385
  useAppAutoEvents({
@@ -10094,8 +10410,8 @@ function PaywalloProvider({ children, config }) {
10094
10410
  });
10095
10411
  }, []);
10096
10412
  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;
10413
+ const SCREEN_HEIGHT = useMemo3(() => Dimensions5.get("window").height, []);
10414
+ const slideAnim = useRef11(new Animated4.Value(SCREEN_HEIGHT)).current;
10099
10415
  const handlePreloadedWebViewReady = useCallback19(() => {
10100
10416
  if (PaywalloClient.getConfig()?.debug) {
10101
10417
  console.log(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
@@ -10104,7 +10420,7 @@ function PaywalloProvider({ children, config }) {
10104
10420
  }, [preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
10105
10421
  const handlePreloadedClose = useCallback19(() => {
10106
10422
  const placement = preloadedWebView?.placement;
10107
- Animated3.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 200, easing: Easing2.in(Easing2.cubic), useNativeDriver: true }).start(() => {
10423
+ Animated4.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 200, easing: Easing2.in(Easing2.cubic), useNativeDriver: true }).start(() => {
10108
10424
  baseHandlePreloadedClose();
10109
10425
  if (placement) triggerRepreload(placement);
10110
10426
  });
@@ -10117,7 +10433,7 @@ function PaywalloProvider({ children, config }) {
10117
10433
  presentedAtRef,
10118
10434
  invalidateCache
10119
10435
  );
10120
- const [isPreloadPurchasing, setIsPreloadPurchasing] = useState16(false);
10436
+ const [isPreloadPurchasing, setIsPreloadPurchasing] = useState17(false);
10121
10437
  const handlePreloadedPurchase = useCallback19(async (productId) => {
10122
10438
  setIsPreloadPurchasing(true);
10123
10439
  try {
@@ -10126,8 +10442,8 @@ function PaywalloProvider({ children, config }) {
10126
10442
  setIsPreloadPurchasing(false);
10127
10443
  }
10128
10444
  }, [rawPreloadedPurchase]);
10129
- const configRef = useRef10(config);
10130
- useEffect12(() => {
10445
+ const configRef = useRef11(config);
10446
+ useEffect14(() => {
10131
10447
  let mounted = true;
10132
10448
  initLocalization({ detectDevice: true });
10133
10449
  PaywalloClient.init(configRef.current).then(async () => {
@@ -10140,6 +10456,11 @@ function PaywalloProvider({ children, config }) {
10140
10456
  const placement = await PaywalloClient.waitForAutoPreloadedPlacement();
10141
10457
  if (mounted && placement) void preloadCampaign(placement).catch(() => {
10142
10458
  });
10459
+ const preloadPlacements = configRef.current.preloadPaywalls;
10460
+ if (mounted && preloadPlacements && preloadPlacements.length > 0) {
10461
+ void PaywalloClient.preloadPaywalls(preloadPlacements).catch(() => {
10462
+ });
10463
+ }
10143
10464
  }
10144
10465
  }).catch((err) => {
10145
10466
  if (!mounted) return;
@@ -10154,7 +10475,22 @@ function PaywalloProvider({ children, config }) {
10154
10475
  return api ? api.getPaywall(p) : null;
10155
10476
  }, []);
10156
10477
  const presentPaywall = useCallback19((placement) => {
10157
- return new Promise((resolve) => setActivePaywall({ placement, resolver: resolve }));
10478
+ const preloaded = paywallPreloadService.getPreloaded(placement);
10479
+ return new Promise((resolve) => setActivePaywall({
10480
+ placement,
10481
+ resolver: resolve,
10482
+ ...preloaded && {
10483
+ paywallConfig: {
10484
+ id: preloaded.config.id,
10485
+ placement: preloaded.config.placement,
10486
+ config: preloaded.config.config,
10487
+ primaryProductId: preloaded.config.primaryProductId,
10488
+ secondaryProductId: preloaded.config.secondaryProductId,
10489
+ tertiaryProductId: preloaded.config.tertiaryProductId
10490
+ },
10491
+ preloadedProducts: preloaded.products
10492
+ }
10493
+ }));
10158
10494
  }, []);
10159
10495
  const trackCampaignImpression = useCallback19((placement, variantKey, campaignId) => {
10160
10496
  const api = PaywalloClient.getApiClient();
@@ -10288,7 +10624,7 @@ function PaywalloProvider({ children, config }) {
10288
10624
  }
10289
10625
  return presentCampaign(placement);
10290
10626
  }, [presentPreloadedWebView, presentCampaign]);
10291
- useEffect12(() => {
10627
+ useEffect14(() => {
10292
10628
  if (!isInitialized) return;
10293
10629
  const onEmergency = async (id) => {
10294
10630
  try {
@@ -10317,7 +10653,7 @@ function PaywalloProvider({ children, config }) {
10317
10653
  const isReady = isInitialized && !isLoadingProducts;
10318
10654
  const noOp = useCallback19(() => {
10319
10655
  }, []);
10320
- const contextValue = useMemo2(() => ({
10656
+ const contextValue = useMemo3(() => ({
10321
10657
  config: PaywalloClient.getConfig(),
10322
10658
  isInitialized,
10323
10659
  isReady,
@@ -10354,17 +10690,17 @@ function PaywalloProvider({ children, config }) {
10354
10690
  useLayoutEffect(() => {
10355
10691
  if (showingPreloadedWebView) {
10356
10692
  slideAnim.setValue(SCREEN_HEIGHT);
10357
- Animated3.timing(slideAnim, { toValue: 0, duration: 200, easing: Easing2.out(Easing2.cubic), useNativeDriver: true }).start();
10693
+ Animated4.timing(slideAnim, { toValue: 0, duration: 200, easing: Easing2.out(Easing2.cubic), useNativeDriver: true }).start();
10358
10694
  }
10359
10695
  }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT]);
10360
10696
  const preloadStyle = [
10361
- styles3.preloadOverlay,
10362
- showingPreloadedWebView ? styles3.visible : styles3.hidden,
10697
+ styles5.preloadOverlay,
10698
+ showingPreloadedWebView ? styles5.visible : styles5.hidden,
10363
10699
  showingPreloadedWebView ? { transform: [{ translateY: slideAnim }] } : void 0
10364
10700
  ];
10365
- return /* @__PURE__ */ jsxs2(PaywalloContext.Provider, { value: contextValue, children: [
10701
+ return /* @__PURE__ */ jsxs3(PaywalloContext.Provider, { value: contextValue, children: [
10366
10702
  children,
10367
- /* @__PURE__ */ jsx3(PaywallErrorBoundary, { children: !!preloadedWebView?.craftData && /* @__PURE__ */ jsx3(Animated3.View, { style: preloadStyle, pointerEvents: showingPreloadedWebView ? "auto" : "none", children: /* @__PURE__ */ jsx3(
10703
+ /* @__PURE__ */ jsx5(PaywallErrorBoundary, { children: !!preloadedWebView?.craftData && /* @__PURE__ */ jsx5(Animated4.View, { style: preloadStyle, pointerEvents: showingPreloadedWebView ? "auto" : "none", children: /* @__PURE__ */ jsx5(
10368
10704
  PaywallWebView,
10369
10705
  {
10370
10706
  paywallId: preloadedWebView.paywallId,
@@ -10380,7 +10716,7 @@ function PaywalloProvider({ children, config }) {
10380
10716
  onRestore: showingPreloadedWebView ? handlePreloadedRestore : noOp
10381
10717
  }
10382
10718
  ) }) }),
10383
- /* @__PURE__ */ jsx3(PaywallErrorBoundary, { children: activePaywall && /* @__PURE__ */ jsx3(
10719
+ /* @__PURE__ */ jsx5(PaywallErrorBoundary, { children: activePaywall && /* @__PURE__ */ jsx5(
10384
10720
  PaywallModal,
10385
10721
  {
10386
10722
  placement: activePaywall.placement,
@@ -10392,10 +10728,11 @@ function PaywalloProvider({ children, config }) {
10392
10728
  campaignId: activePaywall.campaignId,
10393
10729
  variantId: activePaywall.variantId
10394
10730
  }
10395
- ) })
10731
+ ) }),
10732
+ isInitialized && (config.preloadPaywalls?.length ?? 0) > 0 && PaywalloClient.getWebUrl() !== null && /* @__PURE__ */ jsx5(PaywallWebViewPrewarmer, { webUrl: PaywalloClient.getWebUrl() })
10396
10733
  ] });
10397
10734
  }
10398
- var styles3 = StyleSheet3.create({
10735
+ var styles5 = StyleSheet5.create({
10399
10736
  preloadOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#ffffff" },
10400
10737
  hidden: { zIndex: -1, opacity: 0, width: 1, height: 1, overflow: "hidden" },
10401
10738
  visible: { zIndex: 9999, opacity: 1 }
@@ -10451,6 +10788,9 @@ function resolveProductsVariable(parts, context) {
10451
10788
  if (parts[1] === "secondary" && context.secondaryProductId) {
10452
10789
  return getProductVariable(context.products.get(context.secondaryProductId), parts[2]);
10453
10790
  }
10791
+ if (parts[1] === "tertiary" && context.tertiaryProductId) {
10792
+ return getProductVariable(context.products.get(context.tertiaryProductId), parts[2]);
10793
+ }
10454
10794
  if (parts[1] === "hasIntroductoryOffer") {
10455
10795
  return Array.from(context.products.values()).some((p) => p.introductoryPrice) ? "true" : "false";
10456
10796
  }