@virex-tech/paywallo-sdk 2.1.0 → 2.2.1

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
@@ -197,6 +197,9 @@ function isAvailable() {
197
197
  function clearCache() {
198
198
  cachedDeviceInfo = null;
199
199
  }
200
+ function getCachedDeviceInfo() {
201
+ return cachedDeviceInfo;
202
+ }
200
203
  var FALLBACK, _debug, warnedOnce, cachedDeviceInfo, nativeDeviceInfo;
201
204
  var init_NativeDeviceInfo = __esm({
202
205
  "src/utils/NativeDeviceInfo.ts"() {
@@ -223,6 +226,7 @@ var init_NativeDeviceInfo = __esm({
223
226
  cachedDeviceInfo = null;
224
227
  nativeDeviceInfo = {
225
228
  getDeviceInfo,
229
+ getCachedDeviceInfo,
226
230
  getInstallReferrer,
227
231
  isAvailable,
228
232
  clearCache
@@ -483,7 +487,7 @@ var init_IdentityStorage = __esm({
483
487
  });
484
488
 
485
489
  // src/domains/identity/IdentityManager.ts
486
- var EMAIL_REGEX, IdentityManager, identityManager;
490
+ var USER_PHONE_KEY, USER_FIRST_NAME_KEY, USER_LAST_NAME_KEY, USER_DOB_KEY, USER_GENDER_KEY, EMAIL_REGEX, IdentityManager, identityManager;
487
491
  var init_IdentityManager = __esm({
488
492
  "src/domains/identity/IdentityManager.ts"() {
489
493
  "use strict";
@@ -493,6 +497,11 @@ var init_IdentityManager = __esm({
493
497
  init_SecureStorage();
494
498
  init_StorageMigration();
495
499
  init_IdentityStorage();
500
+ USER_PHONE_KEY = "@panel:user_phone";
501
+ USER_FIRST_NAME_KEY = "@panel:user_first_name";
502
+ USER_LAST_NAME_KEY = "@panel:user_last_name";
503
+ USER_DOB_KEY = "@panel:user_dob";
504
+ USER_GENDER_KEY = "@panel:user_gender";
496
505
  EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
497
506
  IdentityManager = class {
498
507
  constructor() {
@@ -500,6 +509,11 @@ var init_IdentityManager = __esm({
500
509
  this.anonId = null;
501
510
  this.email = null;
502
511
  this.properties = {};
512
+ this.phone = null;
513
+ this.firstName = null;
514
+ this.lastName = null;
515
+ this.dateOfBirth = null;
516
+ this.gender = null;
503
517
  this.apiClient = null;
504
518
  this.secureStorage = null;
505
519
  this.debug = false;
@@ -555,7 +569,7 @@ var init_IdentityManager = __esm({
555
569
  let email;
556
570
  let properties;
557
571
  if (options) {
558
- if ("email" in options || "properties" in options) {
572
+ if ("email" in options || "properties" in options || "phone" in options || "firstName" in options || "lastName" in options || "dateOfBirth" in options || "gender" in options) {
559
573
  email = options.email;
560
574
  properties = options.properties;
561
575
  } else {
@@ -575,9 +589,36 @@ var init_IdentityManager = __esm({
575
589
  this.properties = { ...this.properties, ...properties };
576
590
  await this.secureStorage.set(USER_PROPERTIES_KEY, JSON.stringify(this.properties));
577
591
  }
592
+ const opts = options;
593
+ if (opts?.phone) {
594
+ this.phone = opts.phone;
595
+ await this.secureStorage.set(USER_PHONE_KEY, this.phone);
596
+ }
597
+ if (opts?.firstName) {
598
+ this.firstName = opts.firstName;
599
+ await this.secureStorage.set(USER_FIRST_NAME_KEY, this.firstName);
600
+ }
601
+ if (opts?.lastName) {
602
+ this.lastName = opts.lastName;
603
+ await this.secureStorage.set(USER_LAST_NAME_KEY, this.lastName);
604
+ }
605
+ if (opts?.dateOfBirth) {
606
+ this.dateOfBirth = opts.dateOfBirth;
607
+ await this.secureStorage.set(USER_DOB_KEY, this.dateOfBirth);
608
+ }
609
+ if (opts?.gender) {
610
+ this.gender = opts.gender;
611
+ await this.secureStorage.set(USER_GENDER_KEY, this.gender);
612
+ }
578
613
  if (this.apiClient && this.anonId) {
579
614
  try {
580
- await this.apiClient.identify(this.anonId, properties, email, this.deviceId ?? void 0);
615
+ await this.apiClient.identify(this.anonId, properties, email, this.deviceId ?? void 0, {
616
+ phone: this.phone ?? void 0,
617
+ firstName: this.firstName ?? void 0,
618
+ lastName: this.lastName ?? void 0,
619
+ dateOfBirth: this.dateOfBirth ?? void 0,
620
+ gender: this.gender ?? void 0
621
+ });
581
622
  } catch {
582
623
  }
583
624
  }
@@ -612,17 +653,32 @@ var init_IdentityManager = __esm({
612
653
  this.distinctId = null;
613
654
  this.email = null;
614
655
  this.properties = {};
656
+ this.phone = null;
657
+ this.firstName = null;
658
+ this.lastName = null;
659
+ this.dateOfBirth = null;
660
+ this.gender = null;
615
661
  await Promise.all([
616
662
  this.secureStorage?.set(ANON_ID_KEY, newAnonId) ?? Promise.resolve(false),
617
663
  this.secureStorage?.remove(USER_EMAIL_KEY) ?? Promise.resolve(false),
618
- this.secureStorage?.remove(USER_PROPERTIES_KEY) ?? Promise.resolve(false)
664
+ this.secureStorage?.remove(USER_PROPERTIES_KEY) ?? Promise.resolve(false),
665
+ this.secureStorage?.remove(USER_PHONE_KEY) ?? Promise.resolve(false),
666
+ this.secureStorage?.remove(USER_FIRST_NAME_KEY) ?? Promise.resolve(false),
667
+ this.secureStorage?.remove(USER_LAST_NAME_KEY) ?? Promise.resolve(false),
668
+ this.secureStorage?.remove(USER_DOB_KEY) ?? Promise.resolve(false),
669
+ this.secureStorage?.remove(USER_GENDER_KEY) ?? Promise.resolve(false)
619
670
  ]);
620
671
  }
621
672
  getState() {
622
673
  return {
623
674
  deviceId: this.deviceId ?? "",
624
675
  email: this.email,
625
- properties: this.getProperties()
676
+ properties: this.getProperties(),
677
+ phone: this.phone,
678
+ firstName: this.firstName,
679
+ lastName: this.lastName,
680
+ dateOfBirth: this.dateOfBirth,
681
+ gender: this.gender
626
682
  };
627
683
  }
628
684
  isSecureStorageAvailable() {
@@ -647,6 +703,18 @@ var init_IdentityManager = __esm({
647
703
  if (state.email) this.email = state.email;
648
704
  if (state.propertiesJson) this.properties = parseProperties(state.propertiesJson);
649
705
  if (state.distinctId) this.distinctId = state.distinctId;
706
+ const [phone, firstName, lastName, dateOfBirth, gender] = await Promise.all([
707
+ this.secureStorage.get(USER_PHONE_KEY),
708
+ this.secureStorage.get(USER_FIRST_NAME_KEY),
709
+ this.secureStorage.get(USER_LAST_NAME_KEY),
710
+ this.secureStorage.get(USER_DOB_KEY),
711
+ this.secureStorage.get(USER_GENDER_KEY)
712
+ ]);
713
+ if (phone) this.phone = phone;
714
+ if (firstName) this.firstName = firstName;
715
+ if (lastName) this.lastName = lastName;
716
+ if (dateOfBirth) this.dateOfBirth = dateOfBirth;
717
+ if (gender) this.gender = gender;
650
718
  }
651
719
  ensureInitialized() {
652
720
  if (!this.initialized) {
@@ -787,7 +855,8 @@ var init_NativeStoreKit = __esm({
787
855
  );
788
856
  }
789
857
  const distinctId = identityManager.getDistinctId();
790
- const appAccountToken = isValidUUIDv4(distinctId) ? distinctId : null;
858
+ const rawUuid = distinctId?.startsWith("$paywallo_anon:") ? distinctId.slice("$paywallo_anon:".length) : distinctId;
859
+ const appAccountToken = isValidUUIDv4(rawUuid) ? rawUuid : null;
791
860
  if (distinctId && appAccountToken === null) {
792
861
  if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] distinctId is not a valid UUID v4 \u2014 passing null to appAccountToken:", distinctId);
793
862
  }
@@ -2268,6 +2337,7 @@ import { useCallback as useCallback2, useEffect, useRef, useState } from "react"
2268
2337
  import { Animated, AppState as AppState2, Dimensions } from "react-native";
2269
2338
  function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId, openAnim, variantId) {
2270
2339
  const [isPurchasing, setIsPurchasing] = useState(false);
2340
+ const isPurchasingRef = useRef(false);
2271
2341
  const [isClosing, setIsClosing] = useState(false);
2272
2342
  const slideAnim = useRef(new Animated.Value(0)).current;
2273
2343
  const presentedAtRef = useRef(Date.now());
@@ -2299,6 +2369,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
2299
2369
  }, [isClosing, closeWithReason]);
2300
2370
  useEffect(() => {
2301
2371
  const sub = AppState2.addEventListener("change", (nextState) => {
2372
+ if (isPurchasingRef.current) return;
2302
2373
  if (nextState === "background" || nextState === "inactive") {
2303
2374
  closeWithReason("backgrounded");
2304
2375
  }
@@ -2314,6 +2385,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
2314
2385
  const handlePurchase = useCallback2(async (productId) => {
2315
2386
  if (isPurchasing) return;
2316
2387
  setIsPurchasing(true);
2388
+ isPurchasingRef.current = true;
2317
2389
  try {
2318
2390
  if (paywallConfig) trackProductSelected({ placement, paywallId: paywallConfig.id, productId, variantKey, campaignId, variantId });
2319
2391
  const result = await getIAPService().purchase(productId, {
@@ -2336,12 +2408,14 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
2336
2408
  } catch (error) {
2337
2409
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
2338
2410
  } finally {
2411
+ isPurchasingRef.current = false;
2339
2412
  setIsPurchasing(false);
2340
2413
  }
2341
2414
  }, [isPurchasing, onResult, paywallConfig, placement, trackDismissed, trackProductSelected, trackPurchased, variantKey, campaignId, variantId]);
2342
2415
  const handleRestore = useCallback2(async () => {
2343
2416
  if (isPurchasing) return;
2344
2417
  setIsPurchasing(true);
2418
+ isPurchasingRef.current = true;
2345
2419
  try {
2346
2420
  const transactions = await getIAPService().restore();
2347
2421
  if (transactions.length > 0) {
@@ -2356,6 +2430,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
2356
2430
  } catch (error) {
2357
2431
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
2358
2432
  } finally {
2433
+ isPurchasingRef.current = false;
2359
2434
  setIsPurchasing(false);
2360
2435
  }
2361
2436
  }, [isPurchasing, onResult, paywallConfig, placement, trackDismissed, variantKey, campaignId, variantId]);
@@ -2457,7 +2532,8 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
2457
2532
  if (!trackingDoneRef.current && !cancelled) {
2458
2533
  trackingDoneRef.current = true;
2459
2534
  const sessionId = PaywalloClient.getSessionId();
2460
- await apiClient.trackEvent("$paywall_viewed", PaywalloClient.getDistinctId(), {
2535
+ const distinctId = PaywalloClient.getDistinctId();
2536
+ await apiClient.trackEvent("$paywall_viewed", distinctId, {
2461
2537
  placement,
2462
2538
  paywallId: config.id,
2463
2539
  ...sessionId && { sessionId },
@@ -2465,6 +2541,16 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
2465
2541
  ...campaignId && { campaignId },
2466
2542
  ...variantId && { variant_id: variantId }
2467
2543
  });
2544
+ await apiClient.trackEvent("paywall", distinctId, {
2545
+ type: "open",
2546
+ paywall_id: config.id,
2547
+ placement,
2548
+ opened_at: (/* @__PURE__ */ new Date()).toISOString(),
2549
+ ...sessionId && { sessionId },
2550
+ ...variantKey && { variant_key: variantKey },
2551
+ ...campaignId && { campaign_id: campaignId },
2552
+ ...variantId && { variant_id: variantId }
2553
+ });
2468
2554
  }
2469
2555
  } catch (err) {
2470
2556
  if (cancelled) return;
@@ -2649,15 +2735,21 @@ import {
2649
2735
  } from "react-native";
2650
2736
  import { jsx } from "react/jsx-runtime";
2651
2737
  function getNativeWebView() {
2652
- if (!NativeWebViewComponent) {
2738
+ if (NativeWebViewComponent) return NativeWebViewComponent;
2739
+ if (nativeWebViewLookupFailed) return null;
2740
+ try {
2653
2741
  NativeWebViewComponent = requireNativeComponent("PaywalloWebView");
2742
+ return NativeWebViewComponent;
2743
+ } catch {
2744
+ nativeWebViewLookupFailed = true;
2745
+ return null;
2654
2746
  }
2655
- return NativeWebViewComponent;
2656
2747
  }
2657
2748
  function getWebViewEmitter() {
2658
- if (!webViewEmitter) {
2659
- webViewEmitter = new NativeEventEmitter2(NativeModules5.PaywalloWebViewModule);
2660
- }
2749
+ if (webViewEmitter) return webViewEmitter;
2750
+ const mod = NativeModules5.PaywalloWebViewModule;
2751
+ if (!mod) return null;
2752
+ webViewEmitter = new NativeEventEmitter2(mod);
2661
2753
  return webViewEmitter;
2662
2754
  }
2663
2755
  function parseErrorEvent(raw) {
@@ -2689,6 +2781,14 @@ function PaywallWebView({
2689
2781
  const onReadyRef = useRef3(onReady);
2690
2782
  onReadyRef.current = onReady;
2691
2783
  const NativeWebView = useMemo(() => getNativeWebView(), []);
2784
+ useEffect3(() => {
2785
+ if (!NativeWebView) {
2786
+ onErrorRef.current?.({
2787
+ code: -1,
2788
+ description: "Paywallo native module not linked. Run `pod install` (iOS) or rebuild the Android app."
2789
+ });
2790
+ }
2791
+ }, [NativeWebView]);
2692
2792
  const [resolvedWebUrl, setResolvedWebUrl] = useState3(
2693
2793
  () => webUrlProp ?? PaywalloClient.getWebUrl()
2694
2794
  );
@@ -2780,6 +2880,7 @@ function PaywallWebView({
2780
2880
  );
2781
2881
  useEffect3(() => {
2782
2882
  const emitter = getWebViewEmitter();
2883
+ if (!emitter) return;
2783
2884
  const loadEndSub = emitter.addListener("PaywalloWebViewLoadEnd", () => {
2784
2885
  if (!loadEndReceived.current) {
2785
2886
  loadEndReceived.current = true;
@@ -2846,7 +2947,7 @@ function PaywallWebView({
2846
2947
  if (!paywallDataSent.current) return;
2847
2948
  setScriptToInject(buildPurchaseStateScript(isPurchasing));
2848
2949
  }, [isPurchasing]);
2849
- if (!resolvedWebUrl) return null;
2950
+ if (!resolvedWebUrl || !NativeWebView) return null;
2850
2951
  const paywallUrl = `${resolvedWebUrl}/paywall/${paywallId}`;
2851
2952
  return /* @__PURE__ */ jsx(View, { style: styles.container, children: /* @__PURE__ */ jsx(
2852
2953
  NativeWebView,
@@ -2860,7 +2961,7 @@ function PaywallWebView({
2860
2961
  }
2861
2962
  ) });
2862
2963
  }
2863
- var NativeWebViewComponent, webViewEmitter, styles;
2964
+ var NativeWebViewComponent, nativeWebViewLookupFailed, webViewEmitter, styles;
2864
2965
  var init_PaywallWebView = __esm({
2865
2966
  "src/domains/paywall/PaywallWebView.tsx"() {
2866
2967
  "use strict";
@@ -2868,6 +2969,7 @@ var init_PaywallWebView = __esm({
2868
2969
  init_parseWebViewMessage();
2869
2970
  init_paywallScripts();
2870
2971
  NativeWebViewComponent = null;
2972
+ nativeWebViewLookupFailed = false;
2871
2973
  webViewEmitter = null;
2872
2974
  styles = StyleSheet.create({
2873
2975
  container: { flex: 1 },
@@ -3194,7 +3296,8 @@ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
3194
3296
  const apiClient = PaywalloClient.getApiClient();
3195
3297
  if (apiClient && preloadedWebView) {
3196
3298
  const sessionId = PaywalloClient.getSessionId();
3197
- void apiClient.trackEvent("$paywall_viewed", PaywalloClient.getDistinctId(), {
3299
+ const distinctId = PaywalloClient.getDistinctId();
3300
+ void apiClient.trackEvent("$paywall_viewed", distinctId, {
3198
3301
  placement: preloadedWebView.placement,
3199
3302
  paywallId: preloadedWebView.paywallId,
3200
3303
  preloaded: true,
@@ -3204,6 +3307,18 @@ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
3204
3307
  ...sessionId && { sessionId }
3205
3308
  }).catch(() => {
3206
3309
  });
3310
+ void apiClient.trackEvent("paywall", distinctId, {
3311
+ type: "open",
3312
+ paywall_id: preloadedWebView.paywallId,
3313
+ placement: preloadedWebView.placement,
3314
+ opened_at: (/* @__PURE__ */ new Date()).toISOString(),
3315
+ preloaded: true,
3316
+ ...preloadedWebView.variantKey && { variant_key: preloadedWebView.variantKey },
3317
+ ...preloadedWebView.campaignId && { campaign_id: preloadedWebView.campaignId },
3318
+ ...preloadedWebView.variantId && { variant_id: preloadedWebView.variantId },
3319
+ ...sessionId && { sessionId }
3320
+ }).catch(() => {
3321
+ });
3207
3322
  }
3208
3323
  });
3209
3324
  }, [preloadedWebView]);
@@ -3296,6 +3411,8 @@ var init_CampaignGateService = __esm({
3296
3411
  constructor() {
3297
3412
  this.preloadedCampaigns = /* @__PURE__ */ new Map();
3298
3413
  this.pendingPreloads = /* @__PURE__ */ new Set();
3414
+ this.apiClientRef = null;
3415
+ this.STALE_THRESHOLD = 0.8;
3299
3416
  this.activePreloadPromises = /* @__PURE__ */ new Map();
3300
3417
  }
3301
3418
  async getCampaign(apiClient, placement, context) {
@@ -3378,12 +3495,29 @@ var init_CampaignGateService = __esm({
3378
3495
  getPreloadedCampaign(placement) {
3379
3496
  const cached = this.preloadedCampaigns.get(placement);
3380
3497
  if (!cached) return null;
3381
- if (Date.now() - cached.preloadedAt >= PRELOAD_CACHE_TTL_MS) {
3498
+ const age = Date.now() - cached.preloadedAt;
3499
+ if (age >= PRELOAD_CACHE_TTL_MS) {
3382
3500
  this.preloadedCampaigns.delete(placement);
3383
3501
  return null;
3384
3502
  }
3503
+ if (age >= PRELOAD_CACHE_TTL_MS * this.STALE_THRESHOLD && this.apiClientRef) {
3504
+ void this.preloadCampaign(this.apiClientRef, placement).catch(() => {
3505
+ });
3506
+ }
3385
3507
  return cached.campaign;
3386
3508
  }
3509
+ async preloadAllActive(apiClient, debug) {
3510
+ this.apiClientRef = apiClient;
3511
+ const placements = await apiClient.getActivePlacements();
3512
+ if (placements.length === 0) return;
3513
+ if (debug) console.log(`[Paywallo PRELOAD] auto-preloading ${placements.length} placements`);
3514
+ for (let i = 0; i < placements.length; i++) {
3515
+ if (i > 0) await new Promise((r) => setTimeout(r, 500));
3516
+ void this.preloadCampaign(apiClient, placements[i]).catch((err) => {
3517
+ if (debug) console.log(`[Paywallo PRELOAD] failed ${placements[i]}:`, err);
3518
+ });
3519
+ }
3520
+ }
3387
3521
  clearPreloaded() {
3388
3522
  this.preloadedCampaigns.clear();
3389
3523
  this.pendingPreloads.clear();
@@ -3404,7 +3538,7 @@ var init_campaign = __esm({
3404
3538
 
3405
3539
  // src/domains/identity/AdvertisingIdManager.ts
3406
3540
  import { Platform as Platform5 } from "react-native";
3407
- var ZERO_IDFA, AdvertisingIdManager;
3541
+ var ZERO_IDFA, AdvertisingIdManager, advertisingIdManager;
3408
3542
  var init_AdvertisingIdManager = __esm({
3409
3543
  "src/domains/identity/AdvertisingIdManager.ts"() {
3410
3544
  "use strict";
@@ -3414,6 +3548,14 @@ var init_AdvertisingIdManager = __esm({
3414
3548
  constructor() {
3415
3549
  this.cached = null;
3416
3550
  }
3551
+ /**
3552
+ * Synchronous accessor for the cached collect() result. Returns null until
3553
+ * `collect()` resolves at least once. Used by the V2 envelope context
3554
+ * provider — which is sync — to populate `context.ids` without awaiting.
3555
+ */
3556
+ getCached() {
3557
+ return this.cached;
3558
+ }
3417
3559
  async collect(requestATT = false) {
3418
3560
  if (this.cached) return this.cached;
3419
3561
  const fallback = { idfv: null, idfa: null, gaid: null, attStatus: "unavailable" };
@@ -3464,136 +3606,26 @@ var init_AdvertisingIdManager = __esm({
3464
3606
  return id;
3465
3607
  }
3466
3608
  };
3609
+ advertisingIdManager = new AdvertisingIdManager();
3467
3610
  }
3468
3611
  });
3469
3612
 
3470
- // src/domains/identity/MetaBridge.ts
3471
- import { NativeModules as NativeModules6 } from "react-native";
3472
- var TIMEOUT_MS, NativeBridge, MetaBridge, metaBridge;
3473
- var init_MetaBridge = __esm({
3474
- "src/domains/identity/MetaBridge.ts"() {
3475
- "use strict";
3476
- TIMEOUT_MS = 2e3;
3477
- NativeBridge = NativeModules6.PaywalloFBBridge ?? null;
3478
- MetaBridge = class {
3479
- async getAnonymousID() {
3480
- if (!NativeBridge) return null;
3481
- try {
3482
- let timerId;
3483
- const timeout = new Promise((resolve) => {
3484
- timerId = setTimeout(() => resolve(null), TIMEOUT_MS);
3485
- });
3486
- const result = await Promise.race([NativeBridge.getAnonymousID(), timeout]);
3487
- clearTimeout(timerId);
3488
- return result;
3489
- } catch {
3490
- return null;
3491
- }
3492
- }
3493
- async logEvent(name, params = {}) {
3494
- if (!NativeBridge) return;
3495
- try {
3496
- await NativeBridge.logEvent(name, params);
3497
- } catch (error) {
3498
- console.warn("[Paywallo:MetaBridge] logEvent failed", { name, error });
3499
- }
3500
- }
3501
- async logPurchase(amount, currency, params = {}) {
3502
- if (!NativeBridge) return;
3503
- try {
3504
- await NativeBridge.logPurchase(amount, currency, params);
3505
- } catch (error) {
3506
- console.warn("[Paywallo:MetaBridge] logPurchase failed", { amount, currency, error });
3507
- }
3508
- }
3509
- setUserID(userId) {
3510
- if (!NativeBridge) return;
3511
- try {
3512
- NativeBridge.setUserID(userId);
3513
- } catch (error) {
3514
- console.warn("[Paywallo:MetaBridge] setUserID failed", { error });
3515
- }
3516
- }
3517
- flush() {
3518
- if (!NativeBridge) return;
3519
- try {
3520
- NativeBridge.flush();
3521
- } catch (error) {
3522
- console.warn("[Paywallo:MetaBridge] flush failed", { error });
3523
- }
3524
- }
3525
- };
3526
- metaBridge = new MetaBridge();
3613
+ // src/domains/session/distinctIdGuard.ts
3614
+ async function waitForDistinctId(provider, retries = DISTINCT_ID_WAIT_RETRIES, intervalMs = DISTINCT_ID_WAIT_INTERVAL_MS) {
3615
+ if (!provider) return true;
3616
+ if (provider()) return true;
3617
+ for (let attempt = 0; attempt < retries; attempt++) {
3618
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
3619
+ if (provider()) return true;
3527
3620
  }
3528
- });
3529
-
3530
- // src/domains/identity/InstallReferrerManager.ts
3531
- import { Platform as Platform6 } from "react-native";
3532
- var REFERRER_TIMEOUT_MS, InstallReferrerManager, installReferrerManager;
3533
- var init_InstallReferrerManager = __esm({
3534
- "src/domains/identity/InstallReferrerManager.ts"() {
3621
+ return false;
3622
+ }
3623
+ var DISTINCT_ID_WAIT_RETRIES, DISTINCT_ID_WAIT_INTERVAL_MS;
3624
+ var init_distinctIdGuard = __esm({
3625
+ "src/domains/session/distinctIdGuard.ts"() {
3535
3626
  "use strict";
3536
- init_NativeDeviceInfo();
3537
- REFERRER_TIMEOUT_MS = 5e3;
3538
- InstallReferrerManager = class {
3539
- async getReferrer() {
3540
- if (Platform6.OS !== "android") {
3541
- return this.emptyResult();
3542
- }
3543
- let timerId;
3544
- const timeoutPromise = new Promise((resolve) => {
3545
- timerId = setTimeout(() => resolve(this.emptyResult()), REFERRER_TIMEOUT_MS);
3546
- });
3547
- const fetchPromise = this.fetchReferrer();
3548
- const result = await Promise.race([fetchPromise, timeoutPromise]);
3549
- clearTimeout(timerId);
3550
- return result;
3551
- }
3552
- async fetchReferrer() {
3553
- try {
3554
- const referrer = await nativeDeviceInfo.getInstallReferrer();
3555
- if (!referrer) return this.emptyResult();
3556
- const capped = referrer.slice(0, 2048);
3557
- const params = this.parseReferrer(capped);
3558
- return {
3559
- rawReferrer: capped,
3560
- trackingId: params["tracking_id"] || null,
3561
- fbclid: params["fbclid"] || null,
3562
- utmSource: params["utm_source"] || null,
3563
- utmMedium: params["utm_medium"] || null,
3564
- utmCampaign: params["utm_campaign"] || null
3565
- };
3566
- } catch {
3567
- return this.emptyResult();
3568
- }
3569
- }
3570
- parseReferrer(referrer) {
3571
- const result = {};
3572
- for (const pair of referrer.split("&")) {
3573
- const eqIndex = pair.indexOf("=");
3574
- if (eqIndex === -1) continue;
3575
- try {
3576
- const key = decodeURIComponent(pair.slice(0, eqIndex).replace(/\+/g, " "));
3577
- const raw = pair.slice(eqIndex + 1).replace(/\+/g, " ");
3578
- const value = decodeURIComponent(raw);
3579
- if (key) result[key] = value;
3580
- } catch {
3581
- }
3582
- }
3583
- return result;
3584
- }
3585
- emptyResult() {
3586
- return {
3587
- rawReferrer: null,
3588
- trackingId: null,
3589
- fbclid: null,
3590
- utmSource: null,
3591
- utmMedium: null,
3592
- utmCampaign: null
3593
- };
3594
- }
3595
- };
3596
- installReferrerManager = new InstallReferrerManager();
3627
+ DISTINCT_ID_WAIT_RETRIES = 10;
3628
+ DISTINCT_ID_WAIT_INTERVAL_MS = 100;
3597
3629
  }
3598
3630
  });
3599
3631
 
@@ -3618,25 +3650,6 @@ var init_SessionError = __esm({
3618
3650
  }
3619
3651
  });
3620
3652
 
3621
- // src/domains/session/distinctIdGuard.ts
3622
- async function waitForDistinctId(provider, retries = DISTINCT_ID_WAIT_RETRIES, intervalMs = DISTINCT_ID_WAIT_INTERVAL_MS) {
3623
- if (!provider) return true;
3624
- if (provider()) return true;
3625
- for (let attempt = 0; attempt < retries; attempt++) {
3626
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
3627
- if (provider()) return true;
3628
- }
3629
- return false;
3630
- }
3631
- var DISTINCT_ID_WAIT_RETRIES, DISTINCT_ID_WAIT_INTERVAL_MS;
3632
- var init_distinctIdGuard = __esm({
3633
- "src/domains/session/distinctIdGuard.ts"() {
3634
- "use strict";
3635
- DISTINCT_ID_WAIT_RETRIES = 10;
3636
- DISTINCT_ID_WAIT_INTERVAL_MS = 100;
3637
- }
3638
- });
3639
-
3640
3653
  // src/utils/deviceInfo.ts
3641
3654
  var deviceInfo_exports = {};
3642
3655
  __export(deviceInfo_exports, {
@@ -3647,7 +3660,9 @@ async function collectDeviceInfo() {
3647
3660
  return {
3648
3661
  appVersion: result.appVersion,
3649
3662
  deviceModel: result.model,
3650
- osVersion: result.systemVersion
3663
+ osVersion: result.systemVersion,
3664
+ darwinVersion: result.darwinVersion,
3665
+ webkitVersion: result.webkitVersion
3651
3666
  };
3652
3667
  }
3653
3668
  var init_deviceInfo = __esm({
@@ -3947,57 +3962,6 @@ var init_SessionManager = __esm({
3947
3962
  }
3948
3963
  });
3949
3964
 
3950
- // src/domains/identity/InstallIdempotency.ts
3951
- async function checkAndArmInstallGuard(storage) {
3952
- const alreadyTracked = await storage.get(INSTALL_KEYS.INSTALL_TRACKED);
3953
- if (alreadyTracked) return true;
3954
- let fallbackTracked = null;
3955
- try {
3956
- fallbackTracked = await nativeStorage.get(INSTALL_KEYS.LEGACY_INSTALL_TRACKED);
3957
- } catch {
3958
- }
3959
- if (fallbackTracked) return true;
3960
- await nativeStorage.set(INSTALL_KEYS.INSTALL_SENT, "1");
3961
- return false;
3962
- }
3963
- async function markInstallTracked(storage, installedAt) {
3964
- await storage.set(INSTALL_KEYS.INSTALL_TRACKED, String(installedAt));
3965
- try {
3966
- await nativeStorage.set(INSTALL_KEYS.LEGACY_INSTALL_TRACKED, String(installedAt));
3967
- } catch {
3968
- }
3969
- }
3970
- async function getOrCreateInstallEventId() {
3971
- const existing = await nativeStorage.get(INSTALL_KEYS.INSTALL_EVENT_ID);
3972
- if (existing) return existing;
3973
- const { generateUUID: generateUUID2 } = await Promise.resolve().then(() => (init_uuid(), uuid_exports));
3974
- const id = generateUUID2();
3975
- await nativeStorage.set(INSTALL_KEYS.INSTALL_EVENT_ID, id).catch(() => {
3976
- });
3977
- return id;
3978
- }
3979
- var INSTALL_KEYS;
3980
- var init_InstallIdempotency = __esm({
3981
- "src/domains/identity/InstallIdempotency.ts"() {
3982
- "use strict";
3983
- init_NativeStorage();
3984
- INSTALL_KEYS = {
3985
- /** Primary flag — set AFTER a successful $app_installed dispatch. */
3986
- INSTALL_TRACKED: "@panel:install_tracked",
3987
- /** Legacy fallback flag written by older SDK versions. */
3988
- LEGACY_INSTALL_TRACKED: "@paywallo:install_tracked",
3989
- /** Pre-send guard — set BEFORE dispatching to survive concurrent cold-starts. */
3990
- INSTALL_SENT: "@paywallo:app_installed_sent",
3991
- /** Stable event ID for backend dedup — generated once, reused on retries. */
3992
- INSTALL_EVENT_ID: "@panel:install_event_id",
3993
- /** Deferred match flag — set once iOS fingerprint match succeeds. */
3994
- DEFERRED_MATCH_DONE: "@panel:deferred_match_done",
3995
- /** Anonymous ID fallback when FB SDK not available. */
3996
- ANON_ID: "@panel:anon_id"
3997
- };
3998
- }
3999
- });
4000
-
4001
3965
  // src/domains/identity/AttributionTracker.ts
4002
3966
  function hasAnyField(data) {
4003
3967
  return Object.values(data).some((v) => v !== void 0 && v !== null && v !== "");
@@ -4069,21 +4033,212 @@ var init_AttributionTracker = __esm({
4069
4033
  }
4070
4034
  });
4071
4035
 
4072
- // src/domains/identity/InstallTracker.ts
4073
- import { Dimensions as Dimensions3, Platform as Platform7 } from "react-native";
4074
- var InstallTracker;
4075
- var init_InstallTracker = __esm({
4076
- "src/domains/identity/InstallTracker.ts"() {
4077
- "use strict";
4078
- init_NativeDeviceInfo();
4079
- init_SecureStorage();
4080
- init_AdvertisingIdManager();
4081
- init_MetaBridge();
4082
- init_IdentityManager();
4083
- init_InstallReferrerManager();
4084
- init_SessionManager();
4036
+ // src/domains/identity/InstallIdempotency.ts
4037
+ async function checkAndArmInstallGuard(storage) {
4038
+ const alreadyTracked = await storage.get(INSTALL_KEYS.INSTALL_TRACKED);
4039
+ if (alreadyTracked) return true;
4040
+ let fallbackTracked = null;
4041
+ try {
4042
+ fallbackTracked = await nativeStorage.get(INSTALL_KEYS.LEGACY_INSTALL_TRACKED);
4043
+ } catch {
4044
+ }
4045
+ if (fallbackTracked) return true;
4046
+ await nativeStorage.set(INSTALL_KEYS.INSTALL_SENT, "1");
4047
+ return false;
4048
+ }
4049
+ async function markInstallTracked(storage, installedAt) {
4050
+ await storage.set(INSTALL_KEYS.INSTALL_TRACKED, String(installedAt));
4051
+ try {
4052
+ await nativeStorage.set(INSTALL_KEYS.LEGACY_INSTALL_TRACKED, String(installedAt));
4053
+ } catch {
4054
+ }
4055
+ }
4056
+ async function getOrCreateInstallEventId() {
4057
+ const existing = await nativeStorage.get(INSTALL_KEYS.INSTALL_EVENT_ID);
4058
+ if (existing) return existing;
4059
+ const { generateUUID: generateUUID2 } = await Promise.resolve().then(() => (init_uuid(), uuid_exports));
4060
+ const id = generateUUID2();
4061
+ await nativeStorage.set(INSTALL_KEYS.INSTALL_EVENT_ID, id).catch(() => {
4062
+ });
4063
+ return id;
4064
+ }
4065
+ var INSTALL_KEYS;
4066
+ var init_InstallIdempotency = __esm({
4067
+ "src/domains/identity/InstallIdempotency.ts"() {
4068
+ "use strict";
4069
+ init_NativeStorage();
4070
+ INSTALL_KEYS = {
4071
+ /** Primary flag — set AFTER a successful $app_installed dispatch. */
4072
+ INSTALL_TRACKED: "@panel:install_tracked",
4073
+ /** Legacy fallback flag written by older SDK versions. */
4074
+ LEGACY_INSTALL_TRACKED: "@paywallo:install_tracked",
4075
+ /** Pre-send guard — set BEFORE dispatching to survive concurrent cold-starts. */
4076
+ INSTALL_SENT: "@paywallo:app_installed_sent",
4077
+ /** Stable event ID for backend dedup — generated once, reused on retries. */
4078
+ INSTALL_EVENT_ID: "@panel:install_event_id",
4079
+ /** Deferred match flag — set once iOS fingerprint match succeeds. */
4080
+ DEFERRED_MATCH_DONE: "@panel:deferred_match_done",
4081
+ /** Anonymous ID fallback when FB SDK not available. */
4082
+ ANON_ID: "@panel:anon_id"
4083
+ };
4084
+ }
4085
+ });
4086
+
4087
+ // src/domains/identity/InstallReferrerManager.ts
4088
+ import { Platform as Platform6 } from "react-native";
4089
+ var REFERRER_TIMEOUT_MS, InstallReferrerManager, installReferrerManager;
4090
+ var init_InstallReferrerManager = __esm({
4091
+ "src/domains/identity/InstallReferrerManager.ts"() {
4092
+ "use strict";
4093
+ init_NativeDeviceInfo();
4094
+ REFERRER_TIMEOUT_MS = 5e3;
4095
+ InstallReferrerManager = class {
4096
+ async getReferrer() {
4097
+ if (Platform6.OS !== "android") {
4098
+ return this.emptyResult();
4099
+ }
4100
+ let timerId;
4101
+ const timeoutPromise = new Promise((resolve) => {
4102
+ timerId = setTimeout(() => resolve(this.emptyResult()), REFERRER_TIMEOUT_MS);
4103
+ });
4104
+ const fetchPromise = this.fetchReferrer();
4105
+ const result = await Promise.race([fetchPromise, timeoutPromise]);
4106
+ clearTimeout(timerId);
4107
+ return result;
4108
+ }
4109
+ async fetchReferrer() {
4110
+ try {
4111
+ const referrer = await nativeDeviceInfo.getInstallReferrer();
4112
+ if (!referrer) return this.emptyResult();
4113
+ const capped = referrer.slice(0, 2048);
4114
+ const params = this.parseReferrer(capped);
4115
+ return {
4116
+ rawReferrer: capped,
4117
+ trackingId: params["tracking_id"] || null,
4118
+ fbclid: params["fbclid"] || null,
4119
+ utmSource: params["utm_source"] || null,
4120
+ utmMedium: params["utm_medium"] || null,
4121
+ utmCampaign: params["utm_campaign"] || null
4122
+ };
4123
+ } catch {
4124
+ return this.emptyResult();
4125
+ }
4126
+ }
4127
+ parseReferrer(referrer) {
4128
+ const result = {};
4129
+ for (const pair of referrer.split("&")) {
4130
+ const eqIndex = pair.indexOf("=");
4131
+ if (eqIndex === -1) continue;
4132
+ try {
4133
+ const key = decodeURIComponent(pair.slice(0, eqIndex).replace(/\+/g, " "));
4134
+ const raw = pair.slice(eqIndex + 1).replace(/\+/g, " ");
4135
+ const value = decodeURIComponent(raw);
4136
+ if (key) result[key] = value;
4137
+ } catch {
4138
+ }
4139
+ }
4140
+ return result;
4141
+ }
4142
+ emptyResult() {
4143
+ return {
4144
+ rawReferrer: null,
4145
+ trackingId: null,
4146
+ fbclid: null,
4147
+ utmSource: null,
4148
+ utmMedium: null,
4149
+ utmCampaign: null
4150
+ };
4151
+ }
4152
+ };
4153
+ installReferrerManager = new InstallReferrerManager();
4154
+ }
4155
+ });
4156
+
4157
+ // src/domains/identity/MetaBridge.ts
4158
+ import { NativeModules as NativeModules6 } from "react-native";
4159
+ var TIMEOUT_MS, NativeBridge, MetaBridge, metaBridge;
4160
+ var init_MetaBridge = __esm({
4161
+ "src/domains/identity/MetaBridge.ts"() {
4162
+ "use strict";
4163
+ TIMEOUT_MS = 2e3;
4164
+ NativeBridge = NativeModules6.PaywalloFBBridge ?? null;
4165
+ MetaBridge = class {
4166
+ constructor() {
4167
+ this.cachedAnonId = null;
4168
+ }
4169
+ /** Sync read of the cached anonymous ID. Returns null until `getAnonymousID()` resolves once. */
4170
+ getCachedAnonymousID() {
4171
+ return this.cachedAnonId;
4172
+ }
4173
+ async getAnonymousID() {
4174
+ if (this.cachedAnonId) return this.cachedAnonId;
4175
+ if (!NativeBridge) return null;
4176
+ try {
4177
+ let timerId;
4178
+ const timeout = new Promise((resolve) => {
4179
+ timerId = setTimeout(() => resolve(null), TIMEOUT_MS);
4180
+ });
4181
+ const result = await Promise.race([NativeBridge.getAnonymousID(), timeout]);
4182
+ clearTimeout(timerId);
4183
+ if (result) this.cachedAnonId = result;
4184
+ return result;
4185
+ } catch {
4186
+ return null;
4187
+ }
4188
+ }
4189
+ async logEvent(name, params = {}) {
4190
+ if (!NativeBridge) return;
4191
+ try {
4192
+ await NativeBridge.logEvent(name, params);
4193
+ } catch (error) {
4194
+ console.warn("[Paywallo:MetaBridge] logEvent failed", { name, error });
4195
+ }
4196
+ }
4197
+ async logPurchase(amount, currency, params = {}) {
4198
+ if (!NativeBridge) return;
4199
+ try {
4200
+ await NativeBridge.logPurchase(amount, currency, params);
4201
+ } catch (error) {
4202
+ console.warn("[Paywallo:MetaBridge] logPurchase failed", { amount, currency, error });
4203
+ }
4204
+ }
4205
+ setUserID(userId) {
4206
+ if (!NativeBridge) return;
4207
+ try {
4208
+ NativeBridge.setUserID(userId);
4209
+ } catch (error) {
4210
+ console.warn("[Paywallo:MetaBridge] setUserID failed", { error });
4211
+ }
4212
+ }
4213
+ flush() {
4214
+ if (!NativeBridge) return;
4215
+ try {
4216
+ NativeBridge.flush();
4217
+ } catch (error) {
4218
+ console.warn("[Paywallo:MetaBridge] flush failed", { error });
4219
+ }
4220
+ }
4221
+ };
4222
+ metaBridge = new MetaBridge();
4223
+ }
4224
+ });
4225
+
4226
+ // src/domains/identity/InstallTracker.ts
4227
+ import { Dimensions as Dimensions3, Platform as Platform7, PixelRatio } from "react-native";
4228
+ var InstallTracker;
4229
+ var init_InstallTracker = __esm({
4230
+ "src/domains/identity/InstallTracker.ts"() {
4231
+ "use strict";
4232
+ init_SecureStorage();
4233
+ init_NativeDeviceInfo();
4085
4234
  init_distinctIdGuard();
4235
+ init_SessionManager();
4236
+ init_AdvertisingIdManager();
4237
+ init_AttributionTracker();
4238
+ init_IdentityManager();
4086
4239
  init_InstallIdempotency();
4240
+ init_InstallReferrerManager();
4241
+ init_MetaBridge();
4087
4242
  init_AttributionTracker();
4088
4243
  InstallTracker = class {
4089
4244
  constructor(debug = false, optionsOrManager) {
@@ -4094,7 +4249,7 @@ var init_InstallTracker = __esm({
4094
4249
  this.distinctIdProvider = () => identityManager.getDistinctId();
4095
4250
  } else {
4096
4251
  const opts = optionsOrManager ?? {};
4097
- this.advertisingIdManager = opts.advertisingIdManager ?? new AdvertisingIdManager();
4252
+ this.advertisingIdManager = opts.advertisingIdManager ?? advertisingIdManager;
4098
4253
  this.distinctIdProvider = opts.distinctIdProvider ?? (() => identityManager.getDistinctId());
4099
4254
  }
4100
4255
  }
@@ -4132,6 +4287,7 @@ var init_InstallTracker = __esm({
4132
4287
  const screen = Dimensions3.get("screen");
4133
4288
  const screenWidth = Math.round(screen.width ?? 0);
4134
4289
  const screenHeight = Math.round(screen.height ?? 0);
4290
+ const screenDensity = PixelRatio.get();
4135
4291
  const locale = Intl.DateTimeFormat().resolvedOptions().locale ?? "unknown";
4136
4292
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "unknown";
4137
4293
  const info = await nativeDeviceInfo.getDeviceInfo();
@@ -4178,6 +4334,7 @@ var init_InstallTracker = __esm({
4178
4334
  buildNumber,
4179
4335
  ...screenWidth > 0 && { screenWidth },
4180
4336
  ...screenHeight > 0 && { screenHeight },
4337
+ ...screenDensity > 0 && { screenDensity },
4181
4338
  timezone,
4182
4339
  locale,
4183
4340
  ...carrier !== "unknown" && { carrier },
@@ -4195,6 +4352,16 @@ var init_InstallTracker = __esm({
4195
4352
  this.advertisingIdManager.collect(requestATT),
4196
4353
  getOrCreateInstallEventId()
4197
4354
  ]);
4355
+ if (referrer.fbclid || referrer.utmSource || referrer.utmMedium || referrer.utmCampaign) {
4356
+ attributionTracker.capture({
4357
+ ...referrer.fbclid && { fbclid: referrer.fbclid },
4358
+ ...referrer.utmSource && { utmSource: referrer.utmSource },
4359
+ ...referrer.utmMedium && { utmMedium: referrer.utmMedium },
4360
+ ...referrer.utmCampaign && { utmCampaign: referrer.utmCampaign },
4361
+ ...referrer.rawReferrer && { referrer: referrer.rawReferrer }
4362
+ }).catch(() => {
4363
+ });
4364
+ }
4198
4365
  let effectiveAnonId = fbAnonId;
4199
4366
  if (!effectiveAnonId) {
4200
4367
  const stored = await storage.get(INSTALL_KEYS.ANON_ID);
@@ -4239,7 +4406,7 @@ var init_InstallTracker = __esm({
4239
4406
  const baseUrl = apiClient.getBaseUrl();
4240
4407
  const appKey = apiClient.getAppKey();
4241
4408
  const referrer = await installReferrerManager.getReferrer();
4242
- await fetch(`${baseUrl}/attribution/deferred-match/${appKey}`, {
4409
+ await fetch(`${baseUrl}/sdk/attribution/deferred-match/${appKey}`, {
4243
4410
  method: "POST",
4244
4411
  headers: { "Content-Type": "application/json", "X-App-Key": appKey },
4245
4412
  body: JSON.stringify({
@@ -4371,6 +4538,9 @@ var init_identity = __esm({
4371
4538
 
4372
4539
  // src/domains/notifications/bridges/NativePushBridge.ts
4373
4540
  import { NativeModules as NativeModules7, NativeEventEmitter as NativeEventEmitter3, Platform as Platform8 } from "react-native";
4541
+ function isNativePushAvailable() {
4542
+ return !!NativeModules7.PaywalloNotifications;
4543
+ }
4374
4544
  function mapStatusToNumber(status) {
4375
4545
  switch (status) {
4376
4546
  case "granted":
@@ -5034,8 +5204,8 @@ var init_utils = __esm({
5034
5204
  // src/core/version.ts
5035
5205
  function resolveVersion() {
5036
5206
  try {
5037
- if ("2.1.0") {
5038
- return "2.1.0";
5207
+ if ("2.2.1") {
5208
+ return "2.2.1";
5039
5209
  }
5040
5210
  } catch {
5041
5211
  }
@@ -5117,7 +5287,7 @@ var init_TokenRegistration = __esm({
5117
5287
  init_utils();
5118
5288
  init_version();
5119
5289
  FCM_TOKEN_KEY = "@panel:push_token";
5120
- TOKEN_ENDPOINT = "/push-tokens";
5290
+ TOKEN_ENDPOINT = "/sdk/push-tokens";
5121
5291
  }
5122
5292
  });
5123
5293
 
@@ -5396,71 +5566,413 @@ var init_NotificationsManager = __esm({
5396
5566
  registerBackgroundHandler(cb) {
5397
5567
  registerBackgroundMessageHandler(cb);
5398
5568
  }
5399
- destroy() {
5400
- this.tokenRefreshCleanup?.();
5401
- this.tokenRefreshCleanup = null;
5402
- this.foregroundCleanup?.();
5403
- this.foregroundCleanup = null;
5404
- this.tracker = null;
5405
- this.receivedCallbacks = [];
5406
- this.openedCallbacks = [];
5407
- this.dismissedCallbacks = [];
5408
- this.isInitialized = false;
5409
- this.currentToken = null;
5569
+ destroy() {
5570
+ this.tokenRefreshCleanup?.();
5571
+ this.tokenRefreshCleanup = null;
5572
+ this.foregroundCleanup?.();
5573
+ this.foregroundCleanup = null;
5574
+ this.tracker = null;
5575
+ this.receivedCallbacks = [];
5576
+ this.openedCallbacks = [];
5577
+ this.dismissedCallbacks = [];
5578
+ this.isInitialized = false;
5579
+ this.currentToken = null;
5580
+ }
5581
+ };
5582
+ notificationsManager = new NotificationsManager();
5583
+ }
5584
+ });
5585
+
5586
+ // src/domains/notifications/index.ts
5587
+ var init_notifications = __esm({
5588
+ "src/domains/notifications/index.ts"() {
5589
+ "use strict";
5590
+ init_NotificationsManager();
5591
+ init_NativePushBridge();
5592
+ init_NotificationsError();
5593
+ }
5594
+ });
5595
+
5596
+ // src/domains/session/index.ts
5597
+ var init_session = __esm({
5598
+ "src/domains/session/index.ts"() {
5599
+ "use strict";
5600
+ init_SessionManager();
5601
+ init_SessionError();
5602
+ }
5603
+ });
5604
+
5605
+ // src/core/PaywalloState.ts
5606
+ function createPaywalloState() {
5607
+ return {
5608
+ config: null,
5609
+ apiClient: null,
5610
+ initPromise: null,
5611
+ readyPromise: null,
5612
+ resolveReady: null,
5613
+ paywallPresenter: null,
5614
+ campaignPresenter: null,
5615
+ subscriptionGetter: null,
5616
+ activeChecker: null,
5617
+ restoreHandler: null,
5618
+ lastOnboardingStepTimestamp: null,
5619
+ pendingConfig: null,
5620
+ networkCleanup: null,
5621
+ initAttempts: 0,
5622
+ autoPreloadedPlacement: null,
5623
+ autoPreloadPlacementPromise: null,
5624
+ sessionFlagsMap: /* @__PURE__ */ new Map(),
5625
+ pendingIdentifies: []
5626
+ };
5627
+ }
5628
+ var MAX_INIT_RETRIES, RETRY_DELAYS;
5629
+ var init_PaywalloState = __esm({
5630
+ "src/core/PaywalloState.ts"() {
5631
+ "use strict";
5632
+ MAX_INIT_RETRIES = 3;
5633
+ RETRY_DELAYS = [2e3, 5e3, 1e4];
5634
+ }
5635
+ });
5636
+
5637
+ // src/domains/plan/PlanCache.ts
5638
+ var DEFAULT_TTL, PlanCache, planCache;
5639
+ var init_PlanCache = __esm({
5640
+ "src/domains/plan/PlanCache.ts"() {
5641
+ "use strict";
5642
+ DEFAULT_TTL = 5 * 60 * 1e3;
5643
+ PlanCache = class {
5644
+ constructor() {
5645
+ this.cache = null;
5646
+ this.ttl = DEFAULT_TTL;
5647
+ }
5648
+ get() {
5649
+ if (!this.cache) return null;
5650
+ const isStale = Date.now() - this.cache.cachedAt > this.ttl;
5651
+ return { ...this.cache, isStale };
5652
+ }
5653
+ set(data) {
5654
+ this.cache = { data, cachedAt: Date.now(), isStale: false };
5655
+ }
5656
+ invalidate() {
5657
+ this.cache = null;
5658
+ }
5659
+ setTTL(ttl) {
5660
+ this.ttl = ttl;
5661
+ }
5662
+ };
5663
+ planCache = new PlanCache();
5664
+ }
5665
+ });
5666
+
5667
+ // src/domains/plan/PlanError.ts
5668
+ var PlanError, PLAN_ERROR_CODES;
5669
+ var init_PlanError = __esm({
5670
+ "src/domains/plan/PlanError.ts"() {
5671
+ "use strict";
5672
+ init_PaywalloError();
5673
+ PlanError = class extends PaywalloError {
5674
+ constructor(code, message) {
5675
+ super("plan", code, message);
5676
+ this.name = "PlanError";
5677
+ }
5678
+ };
5679
+ PLAN_ERROR_CODES = {
5680
+ NOT_INITIALIZED: "PLAN_NOT_INITIALIZED",
5681
+ FETCH_FAILED: "PLAN_FETCH_FAILED"
5682
+ };
5683
+ }
5684
+ });
5685
+
5686
+ // src/domains/plan/PlanService.ts
5687
+ var PlanService_exports = {};
5688
+ __export(PlanService_exports, {
5689
+ PlanService: () => PlanService,
5690
+ planService: () => planService
5691
+ });
5692
+ function mapProduct(raw) {
5693
+ return {
5694
+ id: raw.id,
5695
+ name: raw.name,
5696
+ description: raw.description,
5697
+ type: raw.type,
5698
+ appleProductId: raw.apple_product_id,
5699
+ googleProductId: raw.google_product_id,
5700
+ billingPeriod: raw.billing_period,
5701
+ trialDays: raw.trial_days,
5702
+ priceUsd: raw.price_usd,
5703
+ regionalPrices: raw.regional_prices,
5704
+ displayOrder: raw.display_order,
5705
+ isActive: raw.is_active
5706
+ };
5707
+ }
5708
+ function mapPlan(raw) {
5709
+ return {
5710
+ id: raw.id,
5711
+ identifier: raw.identifier,
5712
+ name: raw.name,
5713
+ description: raw.description,
5714
+ metadata: raw.metadata,
5715
+ products: raw.products.map(mapProduct)
5716
+ };
5717
+ }
5718
+ var PlanService, planService;
5719
+ var init_PlanService = __esm({
5720
+ "src/domains/plan/PlanService.ts"() {
5721
+ "use strict";
5722
+ init_PlanCache();
5723
+ init_PlanError();
5724
+ PlanService = class {
5725
+ constructor() {
5726
+ this.config = null;
5727
+ this.cache = planCache;
5728
+ }
5729
+ init(config) {
5730
+ this.config = config;
5731
+ }
5732
+ async getCurrentPlan(forceRefresh = false) {
5733
+ if (!forceRefresh) {
5734
+ const cached = this.cache.get();
5735
+ if (cached && !cached.isStale) {
5736
+ const current = cached.data.plans.find(
5737
+ (p) => p.identifier === cached.data.currentIdentifier
5738
+ );
5739
+ return current ?? null;
5740
+ }
5741
+ }
5742
+ return this.fetchCurrentPlan();
5743
+ }
5744
+ async getAllPlans(forceRefresh = false) {
5745
+ if (!forceRefresh) {
5746
+ const cached = this.cache.get();
5747
+ if (cached && !cached.isStale) return cached.data;
5748
+ }
5749
+ return this.fetchAllPlans();
5750
+ }
5751
+ invalidateCache() {
5752
+ this.cache.invalidate();
5753
+ }
5754
+ getApiClient() {
5755
+ if (!this.config) {
5756
+ throw new PlanError(PLAN_ERROR_CODES.NOT_INITIALIZED, "PlanService not initialized. Call Paywallo.init() first");
5757
+ }
5758
+ return this.config.apiClient;
5759
+ }
5760
+ async fetchCurrentPlan() {
5761
+ try {
5762
+ const res = await this.getApiClient().get("/sdk/plans");
5763
+ if (!res.ok) {
5764
+ this.log("fetchCurrentPlan failed", res.status);
5765
+ return null;
5766
+ }
5767
+ const raw = res.data.data.plan;
5768
+ return raw ? mapPlan(raw) : null;
5769
+ } catch (err) {
5770
+ this.log("fetchCurrentPlan error", err);
5771
+ return null;
5772
+ }
5773
+ }
5774
+ async fetchAllPlans() {
5775
+ try {
5776
+ const res = await this.getApiClient().get("/sdk/plans/all");
5777
+ if (!res.ok) {
5778
+ this.log("fetchAllPlans failed", res.status);
5779
+ return { plans: [], currentIdentifier: null };
5780
+ }
5781
+ const result = {
5782
+ plans: res.data.data.plans.map(mapPlan),
5783
+ currentIdentifier: res.data.data.current_identifier
5784
+ };
5785
+ this.cache.set(result);
5786
+ return result;
5787
+ } catch (err) {
5788
+ this.log("fetchAllPlans error", err);
5789
+ return { plans: [], currentIdentifier: null };
5790
+ }
5791
+ }
5792
+ log(...args) {
5793
+ if (this.config?.debug) console.log("[Paywallo Plans]", ...args);
5794
+ }
5795
+ };
5796
+ planService = new PlanService();
5797
+ }
5798
+ });
5799
+
5800
+ // src/domains/plan/index.ts
5801
+ var init_plan = __esm({
5802
+ "src/domains/plan/index.ts"() {
5803
+ "use strict";
5804
+ init_PlanCache();
5805
+ init_PlanService();
5806
+ }
5807
+ });
5808
+
5809
+ // src/domains/onboarding/OnboardingError.ts
5810
+ var OnboardingError, ONBOARDING_ERROR_CODES;
5811
+ var init_OnboardingError = __esm({
5812
+ "src/domains/onboarding/OnboardingError.ts"() {
5813
+ "use strict";
5814
+ init_PaywalloError();
5815
+ OnboardingError = class extends PaywalloError {
5816
+ constructor(code, message) {
5817
+ super("onboarding", code, message);
5818
+ this.name = "OnboardingError";
5819
+ }
5820
+ };
5821
+ ONBOARDING_ERROR_CODES = {
5822
+ NOT_INITIALIZED: "ONBOARDING_NOT_INITIALIZED",
5823
+ INVALID_STEP_NAME: "ONBOARDING_INVALID_STEP_NAME"
5824
+ };
5825
+ }
5826
+ });
5827
+
5828
+ // src/domains/onboarding/OnboardingManager.ts
5829
+ var OnboardingManager, onboardingManager;
5830
+ var init_OnboardingManager = __esm({
5831
+ "src/domains/onboarding/OnboardingManager.ts"() {
5832
+ "use strict";
5833
+ init_OnboardingError();
5834
+ OnboardingManager = class {
5835
+ constructor() {
5836
+ this.pipeline = null;
5837
+ this.debug = false;
5838
+ this.lastStep = null;
5839
+ this.finished = false;
5840
+ }
5841
+ /**
5842
+ * Injects runtime dependencies. Called by PaywalloClient during init.
5843
+ */
5844
+ injectDeps(config) {
5845
+ this.pipeline = config.pipeline;
5846
+ this.debug = config.debug ?? false;
5847
+ }
5848
+ /**
5849
+ * Tracks an onboarding step view.
5850
+ * Saves the step name internally for implicit drop detection on the backend.
5851
+ *
5852
+ * @param stepName - Unique name for this onboarding step.
5853
+ */
5854
+ async step(stepName) {
5855
+ this.assertConfig();
5856
+ this.validateStepName(stepName, "step");
5857
+ this.lastStep = stepName;
5858
+ const payload = {
5859
+ family: "onboarding",
5860
+ type: "step",
5861
+ ...{ step_name: stepName }
5862
+ };
5863
+ return this.emit("onboarding", payload, "step");
5864
+ }
5865
+ /**
5866
+ * Tracks successful completion of the onboarding flow.
5867
+ * Marks the flow as finished.
5868
+ */
5869
+ async complete() {
5870
+ this.assertConfig();
5871
+ this.finished = true;
5872
+ return this.emit("onboarding", { family: "onboarding", type: "complete" }, "complete");
5873
+ }
5874
+ /**
5875
+ * Tracks an explicit drop (abandonment) at the given step.
5876
+ * Marks the flow as finished.
5877
+ *
5878
+ * @param stepName - The step name where the user dropped off.
5879
+ */
5880
+ async drop(stepName) {
5881
+ this.assertConfig();
5882
+ this.validateStepName(stepName, "drop");
5883
+ this.finished = true;
5884
+ const payload = {
5885
+ family: "onboarding",
5886
+ type: "drop",
5887
+ ...{ step_name: stepName }
5888
+ };
5889
+ return this.emit("onboarding", payload, "drop");
5890
+ }
5891
+ /** Returns the last step name seen, or null if no step was tracked yet. */
5892
+ getLastStep() {
5893
+ return this.lastStep;
5894
+ }
5895
+ /** Returns whether the flow has been marked as finished (complete or drop). */
5896
+ isFinished() {
5897
+ return this.finished;
5898
+ }
5899
+ async emit(eventName, properties, logLabel) {
5900
+ const pipeline = this.pipeline;
5901
+ const distinctId = pipeline.getDistinctId();
5902
+ if (!distinctId) {
5903
+ if (this.debug) console.log(`[Paywallo:Onboarding] skipped (no distinctId): ${logLabel}`);
5904
+ return;
5905
+ }
5906
+ await pipeline.trackEvent(eventName, distinctId, properties);
5907
+ if (this.debug) {
5908
+ console.log(`[Paywallo:Onboarding] event emitted: ${eventName}.${logLabel}`, properties);
5909
+ }
5910
+ }
5911
+ assertConfig() {
5912
+ if (!this.pipeline) {
5913
+ throw new OnboardingError(
5914
+ ONBOARDING_ERROR_CODES.NOT_INITIALIZED,
5915
+ "OnboardingManager not initialized. Call Paywallo.init() first."
5916
+ );
5917
+ }
5918
+ }
5919
+ validateStepName(stepName, method) {
5920
+ if (typeof stepName !== "string" || stepName.trim().length === 0) {
5921
+ throw new OnboardingError(
5922
+ ONBOARDING_ERROR_CODES.INVALID_STEP_NAME,
5923
+ `OnboardingManager.${method}(): stepName must be a non-empty string.`
5924
+ );
5925
+ }
5410
5926
  }
5411
5927
  };
5412
- notificationsManager = new NotificationsManager();
5928
+ onboardingManager = new OnboardingManager();
5413
5929
  }
5414
5930
  });
5415
5931
 
5416
- // src/domains/notifications/index.ts
5417
- var init_notifications = __esm({
5418
- "src/domains/notifications/index.ts"() {
5932
+ // src/domains/onboarding/index.ts
5933
+ var init_onboarding = __esm({
5934
+ "src/domains/onboarding/index.ts"() {
5419
5935
  "use strict";
5420
- init_NotificationsManager();
5421
- init_NativePushBridge();
5422
- init_NotificationsError();
5936
+ init_OnboardingManager();
5937
+ init_OnboardingError();
5423
5938
  }
5424
5939
  });
5425
5940
 
5426
- // src/domains/session/index.ts
5427
- var init_session = __esm({
5428
- "src/domains/session/index.ts"() {
5429
- "use strict";
5430
- init_SessionManager();
5431
- init_SessionError();
5941
+ // src/domains/paywall/paywallHeartbeatRecovery.ts
5942
+ async function recoverPaywallHeartbeat(apiClient, distinctId, sessionId, debug) {
5943
+ const snapshot = await readPersistedHeartbeat();
5944
+ if (!snapshot) return false;
5945
+ try {
5946
+ const closedAt = new Date(snapshot.lastSeen);
5947
+ const durationS = Math.max(0, Math.round((snapshot.lastSeen - snapshot.presentedAt) / 1e3));
5948
+ await apiClient.trackEvent("paywall", distinctId, {
5949
+ type: "closed",
5950
+ paywall_id: snapshot.paywallId,
5951
+ placement: snapshot.placement,
5952
+ closed_at: closedAt.toISOString(),
5953
+ duration_s: durationS,
5954
+ close_reason: "error",
5955
+ ...snapshot.variantId && { variant_id: snapshot.variantId },
5956
+ ...snapshot.variantKey && { variant_key: snapshot.variantKey },
5957
+ ...snapshot.campaignId && { campaign_id: snapshot.campaignId },
5958
+ ...sessionId && { sessionId }
5959
+ });
5960
+ if (debug) console.log("[Paywallo HEARTBEAT] recovered orphan paywall close", {
5961
+ paywallId: snapshot.paywallId,
5962
+ durationS
5963
+ });
5964
+ return true;
5965
+ } catch (err) {
5966
+ if (debug) console.log("[Paywallo HEARTBEAT] recovery failed", err);
5967
+ return false;
5968
+ } finally {
5969
+ await clearPersistedHeartbeat();
5432
5970
  }
5433
- });
5434
-
5435
- // src/core/PaywalloState.ts
5436
- function createPaywalloState() {
5437
- return {
5438
- config: null,
5439
- apiClient: null,
5440
- initPromise: null,
5441
- readyPromise: null,
5442
- resolveReady: null,
5443
- paywallPresenter: null,
5444
- campaignPresenter: null,
5445
- subscriptionGetter: null,
5446
- activeChecker: null,
5447
- restoreHandler: null,
5448
- lastOnboardingStepTimestamp: null,
5449
- pendingConfig: null,
5450
- networkCleanup: null,
5451
- initAttempts: 0,
5452
- autoPreloadedPlacement: null,
5453
- autoPreloadPlacementPromise: null,
5454
- sessionFlagsMap: /* @__PURE__ */ new Map(),
5455
- pendingIdentifies: []
5456
- };
5457
5971
  }
5458
- var MAX_INIT_RETRIES, RETRY_DELAYS;
5459
- var init_PaywalloState = __esm({
5460
- "src/core/PaywalloState.ts"() {
5972
+ var init_paywallHeartbeatRecovery = __esm({
5973
+ "src/domains/paywall/paywallHeartbeatRecovery.ts"() {
5461
5974
  "use strict";
5462
- MAX_INIT_RETRIES = 3;
5463
- RETRY_DELAYS = [2e3, 5e3, 1e4];
5975
+ init_paywallHeartbeat();
5464
5976
  }
5465
5977
  });
5466
5978
 
@@ -5468,7 +5980,7 @@ var init_PaywalloState = __esm({
5468
5980
  function keyFor(distinctId) {
5469
5981
  return `${CACHE_KEY_PREFIX}${distinctId}`;
5470
5982
  }
5471
- var LEGACY_CACHE_KEY, CACHE_KEY_PREFIX, CACHE_INDEX_KEY, DEFAULT_TTL, SubscriptionCache, subscriptionCache;
5983
+ var LEGACY_CACHE_KEY, CACHE_KEY_PREFIX, CACHE_INDEX_KEY, DEFAULT_TTL2, SubscriptionCache, subscriptionCache;
5472
5984
  var init_SubscriptionCache = __esm({
5473
5985
  "src/domains/subscription/SubscriptionCache.ts"() {
5474
5986
  "use strict";
@@ -5476,9 +5988,9 @@ var init_SubscriptionCache = __esm({
5476
5988
  LEGACY_CACHE_KEY = "@panel:subscription_cache";
5477
5989
  CACHE_KEY_PREFIX = "subscription_cache:";
5478
5990
  CACHE_INDEX_KEY = "subscription_cache:__index__";
5479
- DEFAULT_TTL = 24 * 60 * 60 * 1e3;
5991
+ DEFAULT_TTL2 = 24 * 60 * 60 * 1e3;
5480
5992
  SubscriptionCache = class {
5481
- constructor(ttl = DEFAULT_TTL) {
5993
+ constructor(ttl = DEFAULT_TTL2) {
5482
5994
  this.memoryCache = /* @__PURE__ */ new Map();
5483
5995
  this.legacyCleanupDone = false;
5484
5996
  this.ttl = ttl;
@@ -5685,7 +6197,7 @@ var init_SubscriptionManager = __esm({
5685
6197
  if (!this.config) {
5686
6198
  return this.getEmptyStatus();
5687
6199
  }
5688
- const url = new URL(`${this.config.serverUrl}/purchases/status/${this.config.appKey}`);
6200
+ const url = new URL(`${this.config.serverUrl}/sdk/purchases/status/${this.config.appKey}`);
5689
6201
  if (this.userId) {
5690
6202
  url.searchParams.set("distinctId", this.userId);
5691
6203
  }
@@ -5925,7 +6437,7 @@ async function getSubscriptionStatus(client, baseUrl, appKey, distinctId) {
5925
6437
  }
5926
6438
  async function getEmergencyPaywall(client) {
5927
6439
  try {
5928
- const response = await client.get("/emergency-paywall");
6440
+ const response = await client.get("/sdk/emergency-paywall");
5929
6441
  if (response.status === 404) return { enabled: false };
5930
6442
  return response.data;
5931
6443
  } catch {
@@ -6542,7 +7054,7 @@ var init_EventBatcher = __esm({
6542
7054
  BATCH_MAX_SIZE = 25;
6543
7055
  BATCH_FLUSH_MS = 1e4;
6544
7056
  EVENT_NAME_REGEX = /^(\$[a-zA-Z0-9_]+|[a-z][a-z0-9_]*)$/;
6545
- V2_BATCH_ENDPOINT = "/ingest/batch";
7057
+ V2_BATCH_ENDPOINT = "/sdk/ingest/batch";
6546
7058
  V1_BATCH_ENDPOINT = "/sdk/events/batch";
6547
7059
  V1_SINGLE_ENDPOINT = "/sdk/events";
6548
7060
  EventBatcher = class {
@@ -6906,7 +7418,7 @@ var init_ApiClient = __esm({
6906
7418
  dispose() {
6907
7419
  this.batcher.dispose();
6908
7420
  }
6909
- async identify(distinctId, properties, email, deviceId) {
7421
+ async identify(distinctId, properties, email, deviceId, pii) {
6910
7422
  await postWithQueue(
6911
7423
  {
6912
7424
  isOfflineQueueEnabled: () => this.offlineQueueEnabled,
@@ -6914,8 +7426,19 @@ var init_ApiClient = __esm({
6914
7426
  isDebug: () => this.debug,
6915
7427
  post: (path, body, skipRetry) => this.post(path, body, skipRetry)
6916
7428
  },
6917
- "/identify",
6918
- { distinctId, properties: properties ?? {}, email, platform: Platform14.OS, ...deviceId && { deviceId } },
7429
+ "/sdk/identify",
7430
+ {
7431
+ distinctId,
7432
+ properties: properties ?? {},
7433
+ email,
7434
+ platform: Platform14.OS,
7435
+ ...deviceId && { deviceId },
7436
+ ...pii?.phone && { phone: pii.phone },
7437
+ ...pii?.firstName && { firstName: pii.firstName },
7438
+ ...pii?.lastName && { lastName: pii.lastName },
7439
+ ...pii?.dateOfBirth && { dateOfBirth: pii.dateOfBirth },
7440
+ ...pii?.gender && { gender: pii.gender }
7441
+ },
6919
7442
  "identify"
6920
7443
  );
6921
7444
  }
@@ -6946,242 +7469,81 @@ var init_ApiClient = __esm({
6946
7469
  return stale !== void 0 ? stale : null;
6947
7470
  }
6948
7471
  }
6949
- async getEmergencyPaywall() {
6950
- return getEmergencyPaywall(this);
6951
- }
6952
- async getCampaign(placement, distinctId, context) {
6953
- const key = `${placement}:${distinctId}`;
6954
- const hit = this.cache.getCampaign(key);
6955
- if (hit !== void 0) return hit;
6956
- try {
6957
- const result = await getCampaignData(this, this.baseUrl, placement, distinctId, context);
6958
- if (!result) {
6959
- this.cache.setCampaign(key, null, this.cache.nullTTL);
6960
- return null;
6961
- }
6962
- this.cache.setCampaign(key, result);
6963
- return result;
6964
- } catch {
6965
- const stale = this.cache.getStaleCampaign(key);
6966
- return stale !== void 0 ? stale : null;
6967
- }
6968
- }
6969
- async getPrimaryCampaign(distinctId) {
6970
- const key = `primary:${distinctId}`;
6971
- const hit = this.cache.getCampaign(key);
6972
- if (hit !== void 0) return hit;
6973
- try {
6974
- const res = await this.get(`/campaigns/public/primary?distinctId=${encodeURIComponent(distinctId)}`);
6975
- if (res.status === 404) {
6976
- this.cache.setCampaign(key, null, this.cache.nullTTL);
6977
- return null;
6978
- }
6979
- if (!res.ok) {
6980
- const stale = this.cache.getStaleCampaign(key);
6981
- return stale !== void 0 ? stale : null;
6982
- }
6983
- this.cache.setCampaign(key, res.data);
6984
- return res.data;
6985
- } catch {
6986
- const stale = this.cache.getStaleCampaign(key);
6987
- return stale !== void 0 ? stale : null;
6988
- }
6989
- }
6990
- async validatePurchase(platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
6991
- return validatePurchase(this, this.appKey, platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey);
6992
- }
6993
- async getSubscriptionStatus(distinctId) {
6994
- return getSubscriptionStatus(this, this.baseUrl, this.appKey, distinctId);
6995
- }
6996
- async evaluateFlags(keys, distinctId) {
6997
- return evaluateFlags(this.flagDeps(), keys, distinctId);
6998
- }
6999
- async getConditionalFlag(flagKey, context) {
7000
- return getConditionalFlag(this.flagDeps(), flagKey, context);
7001
- }
7002
- async getVariantFromCache(flagKey, distinctId) {
7003
- return getVariantFromCache(this.flagDeps(), flagKey, distinctId);
7004
- }
7005
- flagDeps() {
7006
- return {
7007
- get: (path) => this.get(path),
7008
- httpClient: this.httpClient,
7009
- cache: this.cache,
7010
- appKey: this.appKey,
7011
- baseUrl: this.baseUrl
7012
- };
7013
- }
7014
- };
7015
- }
7016
- });
7017
-
7018
- // src/domains/onboarding/OnboardingError.ts
7019
- var OnboardingError, ONBOARDING_ERROR_CODES;
7020
- var init_OnboardingError = __esm({
7021
- "src/domains/onboarding/OnboardingError.ts"() {
7022
- "use strict";
7023
- init_PaywalloError();
7024
- OnboardingError = class extends PaywalloError {
7025
- constructor(code, message) {
7026
- super("onboarding", code, message);
7027
- this.name = "OnboardingError";
7028
- }
7029
- };
7030
- ONBOARDING_ERROR_CODES = {
7031
- NOT_INITIALIZED: "ONBOARDING_NOT_INITIALIZED",
7032
- INVALID_STEP_NAME: "ONBOARDING_INVALID_STEP_NAME"
7033
- };
7034
- }
7035
- });
7036
-
7037
- // src/domains/onboarding/OnboardingManager.ts
7038
- var OnboardingManager, onboardingManager;
7039
- var init_OnboardingManager = __esm({
7040
- "src/domains/onboarding/OnboardingManager.ts"() {
7041
- "use strict";
7042
- init_OnboardingError();
7043
- OnboardingManager = class {
7044
- constructor() {
7045
- this.pipeline = null;
7046
- this.debug = false;
7047
- this.lastStep = null;
7048
- this.finished = false;
7049
- }
7050
- /**
7051
- * Injects runtime dependencies. Called by PaywalloClient during init.
7052
- */
7053
- injectDeps(config) {
7054
- this.pipeline = config.pipeline;
7055
- this.debug = config.debug ?? false;
7056
- }
7057
- /**
7058
- * Tracks an onboarding step view.
7059
- * Saves the step name internally for implicit drop detection on the backend.
7060
- *
7061
- * @param stepName - Unique name for this onboarding step.
7062
- */
7063
- async step(stepName) {
7064
- this.assertConfig();
7065
- this.validateStepName(stepName, "step");
7066
- this.lastStep = stepName;
7067
- const payload = {
7068
- family: "onboarding",
7069
- type: "step",
7070
- ...{ step_name: stepName }
7071
- };
7072
- return this.emit("onboarding", payload, "step");
7073
- }
7074
- /**
7075
- * Tracks successful completion of the onboarding flow.
7076
- * Marks the flow as finished.
7077
- */
7078
- async complete() {
7079
- this.assertConfig();
7080
- this.finished = true;
7081
- return this.emit("onboarding", { family: "onboarding", type: "complete" }, "complete");
7082
- }
7083
- /**
7084
- * Tracks an explicit drop (abandonment) at the given step.
7085
- * Marks the flow as finished.
7086
- *
7087
- * @param stepName - The step name where the user dropped off.
7088
- */
7089
- async drop(stepName) {
7090
- this.assertConfig();
7091
- this.validateStepName(stepName, "drop");
7092
- this.finished = true;
7093
- const payload = {
7094
- family: "onboarding",
7095
- type: "drop",
7096
- ...{ step_name: stepName }
7097
- };
7098
- return this.emit("onboarding", payload, "drop");
7099
- }
7100
- /** Returns the last step name seen, or null if no step was tracked yet. */
7101
- getLastStep() {
7102
- return this.lastStep;
7103
- }
7104
- /** Returns whether the flow has been marked as finished (complete or drop). */
7105
- isFinished() {
7106
- return this.finished;
7472
+ async getEmergencyPaywall() {
7473
+ return getEmergencyPaywall(this);
7107
7474
  }
7108
- async emit(eventName, properties, logLabel) {
7109
- const pipeline = this.pipeline;
7110
- const distinctId = pipeline.getDistinctId();
7111
- if (!distinctId) {
7112
- if (this.debug) console.log(`[Paywallo:Onboarding] skipped (no distinctId): ${logLabel}`);
7113
- return;
7114
- }
7115
- await pipeline.trackEvent(eventName, distinctId, properties);
7116
- if (this.debug) {
7117
- console.log(`[Paywallo:Onboarding] event emitted: ${eventName}.${logLabel}`, properties);
7475
+ async getCampaign(placement, distinctId, context) {
7476
+ const key = `${placement}:${distinctId}`;
7477
+ const hit = this.cache.getCampaign(key);
7478
+ if (hit !== void 0) return hit;
7479
+ try {
7480
+ const result = await getCampaignData(this, this.baseUrl, placement, distinctId, context);
7481
+ if (!result) {
7482
+ this.cache.setCampaign(key, null, this.cache.nullTTL);
7483
+ return null;
7484
+ }
7485
+ this.cache.setCampaign(key, result);
7486
+ return result;
7487
+ } catch {
7488
+ const stale = this.cache.getStaleCampaign(key);
7489
+ return stale !== void 0 ? stale : null;
7118
7490
  }
7119
7491
  }
7120
- assertConfig() {
7121
- if (!this.pipeline) {
7122
- throw new OnboardingError(
7123
- ONBOARDING_ERROR_CODES.NOT_INITIALIZED,
7124
- "OnboardingManager not initialized. Call Paywallo.init() first."
7125
- );
7492
+ async getActivePlacements() {
7493
+ try {
7494
+ const res = await this.get("/sdk/campaigns/placements");
7495
+ if (!res.ok || !Array.isArray(res.data?.data)) return [];
7496
+ return res.data.data;
7497
+ } catch {
7498
+ return [];
7126
7499
  }
7127
7500
  }
7128
- validateStepName(stepName, method) {
7129
- if (typeof stepName !== "string" || stepName.trim().length === 0) {
7130
- throw new OnboardingError(
7131
- ONBOARDING_ERROR_CODES.INVALID_STEP_NAME,
7132
- `OnboardingManager.${method}(): stepName must be a non-empty string.`
7133
- );
7501
+ async getPrimaryCampaign(distinctId) {
7502
+ const key = `primary:${distinctId}`;
7503
+ const hit = this.cache.getCampaign(key);
7504
+ if (hit !== void 0) return hit;
7505
+ try {
7506
+ const res = await this.get(`/sdk/campaigns/public/primary?distinctId=${encodeURIComponent(distinctId)}`);
7507
+ if (res.status === 404) {
7508
+ this.cache.setCampaign(key, null, this.cache.nullTTL);
7509
+ return null;
7510
+ }
7511
+ if (!res.ok) {
7512
+ const stale = this.cache.getStaleCampaign(key);
7513
+ return stale !== void 0 ? stale : null;
7514
+ }
7515
+ this.cache.setCampaign(key, res.data);
7516
+ return res.data;
7517
+ } catch {
7518
+ const stale = this.cache.getStaleCampaign(key);
7519
+ return stale !== void 0 ? stale : null;
7134
7520
  }
7135
7521
  }
7522
+ async validatePurchase(platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
7523
+ return validatePurchase(this, this.appKey, platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey);
7524
+ }
7525
+ async getSubscriptionStatus(distinctId) {
7526
+ return getSubscriptionStatus(this, this.baseUrl, this.appKey, distinctId);
7527
+ }
7528
+ async evaluateFlags(keys, distinctId) {
7529
+ return evaluateFlags(this.flagDeps(), keys, distinctId);
7530
+ }
7531
+ async getConditionalFlag(flagKey, context) {
7532
+ return getConditionalFlag(this.flagDeps(), flagKey, context);
7533
+ }
7534
+ async getVariantFromCache(flagKey, distinctId) {
7535
+ return getVariantFromCache(this.flagDeps(), flagKey, distinctId);
7536
+ }
7537
+ flagDeps() {
7538
+ return {
7539
+ get: (path) => this.get(path),
7540
+ httpClient: this.httpClient,
7541
+ cache: this.cache,
7542
+ appKey: this.appKey,
7543
+ baseUrl: this.baseUrl
7544
+ };
7545
+ }
7136
7546
  };
7137
- onboardingManager = new OnboardingManager();
7138
- }
7139
- });
7140
-
7141
- // src/domains/onboarding/index.ts
7142
- var init_onboarding = __esm({
7143
- "src/domains/onboarding/index.ts"() {
7144
- "use strict";
7145
- init_OnboardingManager();
7146
- init_OnboardingError();
7147
- }
7148
- });
7149
-
7150
- // src/domains/paywall/paywallHeartbeatRecovery.ts
7151
- async function recoverPaywallHeartbeat(apiClient, distinctId, sessionId, debug) {
7152
- const snapshot = await readPersistedHeartbeat();
7153
- if (!snapshot) return false;
7154
- try {
7155
- const closedAt = new Date(snapshot.lastSeen);
7156
- const durationS = Math.max(0, Math.round((snapshot.lastSeen - snapshot.presentedAt) / 1e3));
7157
- await apiClient.trackEvent("paywall", distinctId, {
7158
- type: "closed",
7159
- paywall_id: snapshot.paywallId,
7160
- placement: snapshot.placement,
7161
- closed_at: closedAt.toISOString(),
7162
- duration_s: durationS,
7163
- close_reason: "error",
7164
- ...snapshot.variantId && { variant_id: snapshot.variantId },
7165
- ...snapshot.variantKey && { variant_key: snapshot.variantKey },
7166
- ...snapshot.campaignId && { campaign_id: snapshot.campaignId },
7167
- ...sessionId && { sessionId }
7168
- });
7169
- if (debug) console.log("[Paywallo HEARTBEAT] recovered orphan paywall close", {
7170
- paywallId: snapshot.paywallId,
7171
- durationS
7172
- });
7173
- return true;
7174
- } catch (err) {
7175
- if (debug) console.log("[Paywallo HEARTBEAT] recovery failed", err);
7176
- return false;
7177
- } finally {
7178
- await clearPersistedHeartbeat();
7179
- }
7180
- }
7181
- var init_paywallHeartbeatRecovery = __esm({
7182
- "src/domains/paywall/paywallHeartbeatRecovery.ts"() {
7183
- "use strict";
7184
- init_paywallHeartbeat();
7185
7547
  }
7186
7548
  });
7187
7549
 
@@ -7225,6 +7587,8 @@ var init_eventPipelineBridge = __esm({
7225
7587
 
7226
7588
  // src/core/PaywalloInitHelpers.ts
7227
7589
  function setupNotifications(apiClient, config, environment, eventPipeline) {
7590
+ if (config.notifications === false) return;
7591
+ if (config.notifications === void 0 && !isNativePushAvailable()) return;
7228
7592
  try {
7229
7593
  notificationsManager.injectDeps({
7230
7594
  httpClient: apiClient.getHttpClient(),
@@ -7266,6 +7630,10 @@ function setupAutoPreload(state, apiClient, config, distinctId) {
7266
7630
  return null;
7267
7631
  }).catch(() => null);
7268
7632
  }
7633
+ function startBatchPreload(apiClient, debug) {
7634
+ void campaignGateService.preloadAllActive(apiClient, debug).catch(() => {
7635
+ });
7636
+ }
7269
7637
  var init_PaywalloInitHelpers = __esm({
7270
7638
  "src/core/PaywalloInitHelpers.ts"() {
7271
7639
  "use strict";
@@ -7273,6 +7641,7 @@ var init_PaywalloInitHelpers = __esm({
7273
7641
  init_identity();
7274
7642
  init_notifications();
7275
7643
  init_SecureStorage();
7644
+ init_NativePushBridge();
7276
7645
  init_constants();
7277
7646
  }
7278
7647
  });
@@ -7386,6 +7755,7 @@ async function doInit(state, config) {
7386
7755
  cacheTTL: config.subscriptionCacheTTL,
7387
7756
  debug: config.debug
7388
7757
  });
7758
+ planService.init({ apiClient, debug: config.debug ?? false });
7389
7759
  await identityManager.initialize(apiClient, config.debug);
7390
7760
  if (state.pendingIdentifies.length > 0) {
7391
7761
  for (const pending of state.pendingIdentifies) {
@@ -7408,6 +7778,9 @@ async function doInit(state, config) {
7408
7778
  } catch (err) {
7409
7779
  if (config.debug) console.log("[Paywallo ATTR] start error", err);
7410
7780
  }
7781
+ void nativeDeviceInfo.getDeviceInfo().catch(() => null);
7782
+ void advertisingIdManager.collect(false).catch(() => null);
7783
+ void metaBridge.getAnonymousID().catch(() => null);
7411
7784
  apiClient.setEventContextProvider(() => {
7412
7785
  const attr = attributionTracker.get();
7413
7786
  const attrEntries = attr ? [
@@ -7422,11 +7795,35 @@ async function doInit(state, config) {
7422
7795
  ["referrer", attr.referrer]
7423
7796
  ].filter((pair) => pair[1] !== void 0 && pair[1] !== "") : [];
7424
7797
  const attribution = attrEntries.length > 0 ? Object.fromEntries(attrEntries) : void 0;
7798
+ const adIds = advertisingIdManager.getCached();
7799
+ const fbAnonId = metaBridge.getCachedAnonymousID();
7800
+ const idsEntries = [
7801
+ ["idfa", adIds?.idfa ?? null],
7802
+ ["idfv", adIds?.idfv ?? null],
7803
+ ["gaid", adIds?.gaid ?? null],
7804
+ ["fb_anon_id", fbAnonId]
7805
+ ].filter((pair) => pair[1] !== null && pair[1] !== "");
7806
+ const ids = idsEntries.length > 0 ? Object.fromEntries(idsEntries) : void 0;
7807
+ const device = nativeDeviceInfo.getCachedDeviceInfo();
7808
+ let locale;
7809
+ let timezone;
7810
+ try {
7811
+ const opts = Intl.DateTimeFormat().resolvedOptions();
7812
+ locale = opts.locale || void 0;
7813
+ timezone = opts.timeZone || void 0;
7814
+ } catch {
7815
+ }
7425
7816
  return {
7426
7817
  distinct_id: identityManager.getDistinctId() || void 0,
7427
7818
  device_id: identityManager.getDeviceId() ?? void 0,
7428
7819
  session_id: sessionManager.getSessionId() ?? void 0,
7429
- ...attribution && Object.keys(attribution).length > 0 && { attribution }
7820
+ app_version: device?.appVersion && device.appVersion !== "0.0.0" ? device.appVersion : void 0,
7821
+ os_version: device?.systemVersion && device.systemVersion !== "0.0.0" ? device.systemVersion : void 0,
7822
+ device_model: device?.modelId || device?.model || void 0,
7823
+ timezone,
7824
+ locale,
7825
+ ...attribution && Object.keys(attribution).length > 0 && { attribution },
7826
+ ...ids && Object.keys(ids).length > 0 && { ids }
7430
7827
  };
7431
7828
  });
7432
7829
  await sessionManager.initialize(
@@ -7460,7 +7857,7 @@ async function doInit(state, config) {
7460
7857
  state.resolveReady();
7461
7858
  state.resolveReady = null;
7462
7859
  }
7463
- if (config.debug) console.warn("[Paywallo INIT] complete");
7860
+ console.warn("[Paywallo] initialized successfully");
7464
7861
  if (distinctId) {
7465
7862
  apiClient.getSubscriptionStatus(distinctId).then(() => {
7466
7863
  if (config.debug) console.log("[Paywallo SUB] preloaded on boot");
@@ -7471,26 +7868,30 @@ async function doInit(state, config) {
7471
7868
  });
7472
7869
  }
7473
7870
  setupAutoPreload(state, apiClient, config, distinctId);
7871
+ startBatchPreload(apiClient, config.debug);
7474
7872
  }
7475
7873
  var init_PaywalloInitializer = __esm({
7476
7874
  "src/core/PaywalloInitializer.ts"() {
7477
7875
  "use strict";
7478
- init_SubscriptionManager();
7479
- init_ApiClient();
7480
7876
  init_identity();
7877
+ init_plan();
7878
+ init_AdvertisingIdManager();
7481
7879
  init_DeepLinkAttributionCapture();
7482
7880
  init_InstallTracker();
7483
- init_network();
7484
- init_queue();
7485
- init_session();
7881
+ init_MetaBridge();
7486
7882
  init_onboarding();
7487
- init_NativeStorage();
7488
- init_NativeDeviceInfo();
7489
7883
  init_paywallHeartbeatRecovery();
7884
+ init_session();
7885
+ init_SubscriptionManager();
7886
+ init_NativeDeviceInfo();
7887
+ init_ApiClient();
7888
+ init_constants();
7490
7889
  init_eventPipelineBridge();
7491
- init_PaywalloState();
7890
+ init_network();
7492
7891
  init_PaywalloInitHelpers();
7493
- init_constants();
7892
+ init_PaywalloState();
7893
+ init_queue();
7894
+ init_NativeStorage();
7494
7895
  }
7495
7896
  });
7496
7897
 
@@ -7744,6 +8145,7 @@ var init_PaywalloClient = __esm({
7744
8145
  init_PaywalloAnalytics();
7745
8146
  init_PaywalloSubscription();
7746
8147
  init_PaywalloFlags();
8148
+ init_plan();
7747
8149
  PaywalloClientClass = class {
7748
8150
  constructor() {
7749
8151
  this.state = createPaywalloState();
@@ -7995,6 +8397,14 @@ var init_PaywalloClient = __esm({
7995
8397
  async processOfflineQueue() {
7996
8398
  return queueProcessor.processQueue();
7997
8399
  }
8400
+ async getPlans() {
8401
+ this.ensureInitialized();
8402
+ return planService.getAllPlans();
8403
+ }
8404
+ async getCurrentPlan() {
8405
+ this.ensureInitialized();
8406
+ return planService.getCurrentPlan();
8407
+ }
7998
8408
  getApiClientOrThrow() {
7999
8409
  if (!this.state.config || !this.state.apiClient) throw new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, "SDK not initialized. Call Paywallo.init() first");
8000
8410
  return this.state.apiClient;
@@ -8077,8 +8487,124 @@ function usePaywallo() {
8077
8487
  return context;
8078
8488
  }
8079
8489
 
8080
- // src/hooks/useProducts.ts
8490
+ // src/hooks/usePlans.ts
8081
8491
  import { useCallback as useCallback8, useEffect as useEffect7, useState as useState6 } from "react";
8492
+ async function getPlanService() {
8493
+ try {
8494
+ const mod = await Promise.resolve().then(() => (init_PlanService(), PlanService_exports));
8495
+ return mod.planService ?? null;
8496
+ } catch {
8497
+ return null;
8498
+ }
8499
+ }
8500
+ function usePlans() {
8501
+ const [allPlans, setAllPlans] = useState6([]);
8502
+ const [currentPlan, setCurrentPlan] = useState6(null);
8503
+ const [isLoading, setIsLoading] = useState6(true);
8504
+ const [error, setError] = useState6(null);
8505
+ const fetchPlans = useCallback8(async (forceRefresh = false) => {
8506
+ setIsLoading(true);
8507
+ setError(null);
8508
+ try {
8509
+ const service = await getPlanService();
8510
+ if (!service) {
8511
+ throw new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, "PlanService not available");
8512
+ }
8513
+ const response = await service.getAllPlans(forceRefresh);
8514
+ const { plans, currentIdentifier } = response;
8515
+ setAllPlans(plans);
8516
+ setCurrentPlan(
8517
+ currentIdentifier != null ? plans.find((p) => p.identifier === currentIdentifier) ?? null : null
8518
+ );
8519
+ } catch (err) {
8520
+ const e = err instanceof PurchaseError ? err : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, String(err));
8521
+ setError(e);
8522
+ } finally {
8523
+ setIsLoading(false);
8524
+ }
8525
+ }, []);
8526
+ const refresh = useCallback8(() => {
8527
+ return fetchPlans(true);
8528
+ }, [fetchPlans]);
8529
+ useEffect7(() => {
8530
+ void fetchPlans(false);
8531
+ }, [fetchPlans]);
8532
+ return { currentPlan, allPlans, isLoading, error, refresh };
8533
+ }
8534
+
8535
+ // src/hooks/usePlanPurchase.ts
8536
+ import { useCallback as useCallback9, useState as useState7 } from "react";
8537
+ async function invalidatePlanCache() {
8538
+ try {
8539
+ const mod = await Promise.resolve().then(() => (init_PlanService(), PlanService_exports));
8540
+ mod.planService?.invalidateCache();
8541
+ } catch {
8542
+ }
8543
+ }
8544
+ function usePlanPurchase() {
8545
+ const [state, setState] = useState7("idle");
8546
+ const [error, setError] = useState7(null);
8547
+ const purchase = useCallback9(async (productId) => {
8548
+ setState("purchasing");
8549
+ setError(null);
8550
+ try {
8551
+ const iapService = getIAPService();
8552
+ const result = await iapService.purchase(productId);
8553
+ setState("idle");
8554
+ if (result.success) {
8555
+ await invalidatePlanCache();
8556
+ }
8557
+ return result.success ? {
8558
+ success: true,
8559
+ purchase: result.purchase ? {
8560
+ productId: result.purchase.productId,
8561
+ transactionId: result.purchase.transactionId ?? "",
8562
+ transactionDate: result.purchase.transactionDate,
8563
+ receipt: result.purchase.receipt,
8564
+ platform: "ios"
8565
+ } : void 0
8566
+ } : { success: false, error: result.error };
8567
+ } catch (err) {
8568
+ const purchaseError = err instanceof PurchaseError ? err : createPurchaseError("PURCHASE_FAILED", err instanceof Error ? err.message : String(err));
8569
+ if (purchaseError.userCancelled) {
8570
+ setState("idle");
8571
+ return null;
8572
+ }
8573
+ setError(purchaseError);
8574
+ setState("error");
8575
+ return { success: false, error: purchaseError };
8576
+ }
8577
+ }, []);
8578
+ const restore = useCallback9(async () => {
8579
+ setState("restoring");
8580
+ setError(null);
8581
+ try {
8582
+ const iapService = getIAPService();
8583
+ const transactions = await iapService.restore();
8584
+ setState("idle");
8585
+ await invalidatePlanCache();
8586
+ return {
8587
+ restoredCount: transactions.length,
8588
+ productIds: transactions.map((tx) => tx.productId)
8589
+ };
8590
+ } catch (err) {
8591
+ const e = err instanceof PurchaseError ? err : createPurchaseError("RESTORE_FAILED", String(err));
8592
+ setError(e);
8593
+ setState("error");
8594
+ return null;
8595
+ }
8596
+ }, []);
8597
+ return {
8598
+ purchase,
8599
+ restore,
8600
+ isPurchasing: state === "purchasing",
8601
+ isRestoring: state === "restoring",
8602
+ error
8603
+ };
8604
+ }
8605
+
8606
+ // src/hooks/useProducts.ts
8607
+ import { useCallback as useCallback10, useEffect as useEffect8, useState as useState8 } from "react";
8082
8608
 
8083
8609
  // src/utils/productMappers.ts
8084
8610
  function mapToIAPProduct(product) {
@@ -8117,11 +8643,11 @@ function mapToFormattedProduct(product) {
8117
8643
 
8118
8644
  // src/hooks/useProducts.ts
8119
8645
  function useProducts(initialProductIds) {
8120
- const [products, setProducts] = useState6([]);
8121
- const [formattedProducts, setFormattedProducts] = useState6([]);
8122
- const [isLoading, setIsLoading] = useState6(false);
8123
- const [error, setError] = useState6(null);
8124
- const loadProducts = useCallback8(async (productIds) => {
8646
+ const [products, setProducts] = useState8([]);
8647
+ const [formattedProducts, setFormattedProducts] = useState8([]);
8648
+ const [isLoading, setIsLoading] = useState8(false);
8649
+ const [error, setError] = useState8(null);
8650
+ const loadProducts = useCallback10(async (productIds) => {
8125
8651
  setIsLoading(true);
8126
8652
  setError(null);
8127
8653
  try {
@@ -8135,24 +8661,24 @@ function useProducts(initialProductIds) {
8135
8661
  setIsLoading(false);
8136
8662
  }
8137
8663
  }, []);
8138
- useEffect7(() => {
8664
+ useEffect8(() => {
8139
8665
  if (initialProductIds && initialProductIds.length > 0) {
8140
8666
  void loadProducts(initialProductIds);
8141
8667
  }
8142
8668
  }, []);
8143
- const getProduct = useCallback8(
8669
+ const getProduct = useCallback10(
8144
8670
  (productId) => {
8145
8671
  return products.find((p) => p.productId === productId);
8146
8672
  },
8147
8673
  [products]
8148
8674
  );
8149
- const getFormattedProduct = useCallback8(
8675
+ const getFormattedProduct = useCallback10(
8150
8676
  (productId) => {
8151
8677
  return formattedProducts.find((p) => p.productId === productId);
8152
8678
  },
8153
8679
  [formattedProducts]
8154
8680
  );
8155
- const getPriceInfo = useCallback8(
8681
+ const getPriceInfo = useCallback10(
8156
8682
  (productId) => {
8157
8683
  const p = formattedProducts.find((fp) => fp.productId === productId);
8158
8684
  if (!p) return void 0;
@@ -8181,11 +8707,11 @@ function useProducts(initialProductIds) {
8181
8707
  }
8182
8708
 
8183
8709
  // src/hooks/usePurchase.ts
8184
- import { useCallback as useCallback9, useState as useState7 } from "react";
8710
+ import { useCallback as useCallback11, useState as useState9 } from "react";
8185
8711
  function usePurchase() {
8186
- const [state, setState] = useState7("idle");
8187
- const [error, setError] = useState7(null);
8188
- const purchase = useCallback9(async (productId) => {
8712
+ const [state, setState] = useState9("idle");
8713
+ const [error, setError] = useState9(null);
8714
+ const purchase = useCallback11(async (productId) => {
8189
8715
  setState("purchasing");
8190
8716
  setError(null);
8191
8717
  try {
@@ -8221,7 +8747,7 @@ function usePurchase() {
8221
8747
  return { status: "failed", error: purchaseError };
8222
8748
  }
8223
8749
  }, []);
8224
- const restore = useCallback9(async () => {
8750
+ const restore = useCallback11(async () => {
8225
8751
  setState("restoring");
8226
8752
  setError(null);
8227
8753
  try {
@@ -8252,14 +8778,14 @@ function usePurchase() {
8252
8778
  }
8253
8779
 
8254
8780
  // src/hooks/useSubscription.ts
8255
- import { useCallback as useCallback10, useEffect as useEffect8, useState as useState8 } from "react";
8781
+ import { useCallback as useCallback12, useEffect as useEffect9, useState as useState10 } from "react";
8256
8782
  init_SubscriptionManager();
8257
8783
  var EMPTY_ENTITLEMENTS = [];
8258
8784
  function useSubscription() {
8259
- const [status, setStatus] = useState8(null);
8260
- const [isLoading, setIsLoading] = useState8(true);
8261
- const [error, setError] = useState8(null);
8262
- const refresh = useCallback10(async () => {
8785
+ const [status, setStatus] = useState10(null);
8786
+ const [isLoading, setIsLoading] = useState10(true);
8787
+ const [error, setError] = useState10(null);
8788
+ const refresh = useCallback12(async () => {
8263
8789
  setIsLoading(true);
8264
8790
  setError(null);
8265
8791
  try {
@@ -8272,7 +8798,7 @@ function useSubscription() {
8272
8798
  setIsLoading(false);
8273
8799
  }
8274
8800
  }, []);
8275
- const restore = useCallback10(async () => {
8801
+ const restore = useCallback12(async () => {
8276
8802
  setIsLoading(true);
8277
8803
  setError(null);
8278
8804
  try {
@@ -8285,7 +8811,7 @@ function useSubscription() {
8285
8811
  setIsLoading(false);
8286
8812
  }
8287
8813
  }, []);
8288
- useEffect8(() => {
8814
+ useEffect9(() => {
8289
8815
  void subscriptionManager.getSubscriptionStatus().then((s) => {
8290
8816
  setStatus(s);
8291
8817
  setIsLoading(false);
@@ -8312,16 +8838,19 @@ function useSubscription() {
8312
8838
  };
8313
8839
  }
8314
8840
 
8841
+ // src/index.ts
8842
+ init_plan();
8843
+
8315
8844
  // src/PaywalloProvider.tsx
8316
8845
  init_paywall();
8317
8846
  init_paywall();
8318
8847
  init_paywall();
8319
- import { useCallback as useCallback15, useEffect as useEffect10, useLayoutEffect, useMemo as useMemo2, useRef as useRef10, useState as useState12 } from "react";
8848
+ import { useCallback as useCallback17, useEffect as useEffect11, useLayoutEffect, useMemo as useMemo2, useRef as useRef10, useState as useState14 } from "react";
8320
8849
  import { Animated as Animated3, Dimensions as Dimensions4, Easing as Easing2, StyleSheet as StyleSheet3 } from "react-native";
8321
8850
  init_PaywalloClient();
8322
8851
 
8323
8852
  // src/hooks/useAppAutoEvents.ts
8324
- import { useEffect as useEffect9, useRef as useRef7 } from "react";
8853
+ import { useEffect as useEffect10, useRef as useRef7 } from "react";
8325
8854
  var FIRST_SEEN_KEY = "@panel:firstSeen";
8326
8855
  var DEBOUNCE_MS = 1e3;
8327
8856
  async function detectPlatform3() {
@@ -8352,7 +8881,7 @@ function useAppAutoEvents(config) {
8352
8881
  debug
8353
8882
  } = config;
8354
8883
  const didRunRef = useRef7(false);
8355
- useEffect9(() => {
8884
+ useEffect10(() => {
8356
8885
  if (!isInitialized || didRunRef.current) return;
8357
8886
  const timer = setTimeout(() => {
8358
8887
  if (didRunRef.current) return;
@@ -8399,7 +8928,7 @@ function useAppAutoEvents(config) {
8399
8928
  // src/hooks/useCampaignPreload.ts
8400
8929
  init_PaywalloClient();
8401
8930
  init_CampaignError();
8402
- import { useCallback as useCallback11, useRef as useRef8, useState as useState9 } from "react";
8931
+ import { useCallback as useCallback13, useRef as useRef8, useState as useState11 } from "react";
8403
8932
 
8404
8933
  // src/utils/loadCampaignProducts.ts
8405
8934
  init_IAPService();
@@ -8468,17 +8997,17 @@ function getCacheEntry(cache, placement) {
8468
8997
 
8469
8998
  // src/hooks/useCampaignPreload.ts
8470
8999
  function useCampaignPreload() {
8471
- const [preloadedWebView, setPreloadedWebView] = useState9(null);
9000
+ const [preloadedWebView, setPreloadedWebView] = useState11(null);
8472
9001
  const preloadedCampaignsRef = useRef8(/* @__PURE__ */ new Map());
8473
- const isPreloadValid = useCallback11(
9002
+ const isPreloadValid = useCallback13(
8474
9003
  (placement) => isCacheEntryValid(preloadedCampaignsRef.current.get(placement)),
8475
9004
  []
8476
9005
  );
8477
- const consumePreloadedCampaign = useCallback11(
9006
+ const consumePreloadedCampaign = useCallback13(
8478
9007
  (placement) => getCacheEntry(preloadedCampaignsRef.current, placement),
8479
9008
  []
8480
9009
  );
8481
- const preloadCampaign = useCallback11(
9010
+ const preloadCampaign = useCallback13(
8482
9011
  async (placement, context) => {
8483
9012
  try {
8484
9013
  const campaign = await PaywalloClient.getCampaign(placement, context);
@@ -8552,12 +9081,12 @@ init_SecureStorage();
8552
9081
  init_paywall();
8553
9082
 
8554
9083
  // src/hooks/useProductLoader.ts
8555
- import { useCallback as useCallback12, useState as useState10 } from "react";
9084
+ import { useCallback as useCallback14, useState as useState12 } from "react";
8556
9085
  init_PaywalloClient();
8557
9086
  function useProductLoader() {
8558
- const [products, setProducts] = useState10(/* @__PURE__ */ new Map());
8559
- const [isLoadingProducts, setIsLoadingProducts] = useState10(false);
8560
- const refreshProducts = useCallback12(async (productIds) => {
9087
+ const [products, setProducts] = useState12(/* @__PURE__ */ new Map());
9088
+ const [isLoadingProducts, setIsLoadingProducts] = useState12(false);
9089
+ const refreshProducts = useCallback14(async (productIds) => {
8561
9090
  setIsLoadingProducts(true);
8562
9091
  try {
8563
9092
  const iapService = getIAPService();
@@ -8575,12 +9104,12 @@ function useProductLoader() {
8575
9104
  }
8576
9105
 
8577
9106
  // src/hooks/usePurchaseOrchestrator.ts
8578
- import { useCallback as useCallback13 } from "react";
9107
+ import { useCallback as useCallback15 } from "react";
8579
9108
  init_PaywalloClient();
8580
9109
  init_paywall();
8581
9110
  function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, invalidateSubscriptionCache) {
8582
9111
  const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
8583
- const handlePreloadedPurchase = useCallback13(
9112
+ const handlePreloadedPurchase = useCallback15(
8584
9113
  async (productId) => {
8585
9114
  try {
8586
9115
  if (preloadedWebView) {
@@ -8618,7 +9147,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
8618
9147
  },
8619
9148
  [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased, invalidateSubscriptionCache]
8620
9149
  );
8621
- const handlePreloadedRestore = useCallback13(async () => {
9150
+ const handlePreloadedRestore = useCallback15(async () => {
8622
9151
  try {
8623
9152
  const transactions = await getIAPService().restore();
8624
9153
  if (transactions.length === 0) return;
@@ -8643,7 +9172,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
8643
9172
  // src/hooks/useSubscriptionSync.ts
8644
9173
  init_PaywalloClient();
8645
9174
  init_SubscriptionCache();
8646
- import { useCallback as useCallback14, useRef as useRef9, useState as useState11 } from "react";
9175
+ import { useCallback as useCallback16, useRef as useRef9, useState as useState13 } from "react";
8647
9176
  var CACHE_TTL_MS2 = 5 * 60 * 1e3;
8648
9177
  var ACTIVE_STATUSES = ["active", "in_grace_period"];
8649
9178
  var PUBLIC_TO_DOMAIN_STATUS = {
@@ -8708,14 +9237,14 @@ function isActiveCacheEntry(entry) {
8708
9237
  return isSubscriptionStillActive(entry.data);
8709
9238
  }
8710
9239
  function useSubscriptionSync() {
8711
- const [subscription, setSubscription] = useState11(null);
9240
+ const [subscription, setSubscription] = useState13(null);
8712
9241
  const cacheRef = useRef9(null);
8713
- const invalidateCache = useCallback14(() => {
9242
+ const invalidateCache = useCallback16(() => {
8714
9243
  cacheRef.current = null;
8715
9244
  const distinctId = PaywalloClient.getDistinctId();
8716
9245
  if (distinctId) void subscriptionCache.invalidate(distinctId);
8717
9246
  }, []);
8718
- const fetchAndCache = useCallback14(async () => {
9247
+ const fetchAndCache = useCallback16(async () => {
8719
9248
  const apiClient = PaywalloClient.getApiClient();
8720
9249
  const distinctId = PaywalloClient.getDistinctId();
8721
9250
  if (!apiClient || !distinctId) return null;
@@ -8729,7 +9258,7 @@ function useSubscriptionSync() {
8729
9258
  void subscriptionCache.set(distinctId, toDomainResponse(status));
8730
9259
  return sub;
8731
9260
  }, []);
8732
- const hasActiveSubscription = useCallback14(async () => {
9261
+ const hasActiveSubscription = useCallback16(async () => {
8733
9262
  const apiClient = PaywalloClient.getApiClient();
8734
9263
  const distinctId = PaywalloClient.getDistinctId();
8735
9264
  if (!apiClient || !distinctId) return false;
@@ -8772,7 +9301,7 @@ function useSubscriptionSync() {
8772
9301
  return resolveOfflineFromCache(distinctId);
8773
9302
  }
8774
9303
  }, []);
8775
- const getSubscription = useCallback14(async () => {
9304
+ const getSubscription = useCallback16(async () => {
8776
9305
  const apiClient = PaywalloClient.getApiClient();
8777
9306
  const distinctId = PaywalloClient.getDistinctId();
8778
9307
  if (!apiClient || !distinctId) return null;
@@ -8799,20 +9328,20 @@ init_ClientError();
8799
9328
  init_localization();
8800
9329
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
8801
9330
  function PaywalloProvider({ children, config }) {
8802
- const [isInitialized, setIsInitialized] = useState12(false);
8803
- const [initError, setInitError] = useState12(null);
8804
- const [distinctId, setDistinctId] = useState12(null);
8805
- const [activePaywall, setActivePaywall] = useState12(null);
9331
+ const [isInitialized, setIsInitialized] = useState14(false);
9332
+ const [initError, setInitError] = useState14(null);
9333
+ const [distinctId, setDistinctId] = useState14(null);
9334
+ const [activePaywall, setActivePaywall] = useState14(null);
8806
9335
  const { products, isLoadingProducts, refreshProducts } = useProductLoader();
8807
9336
  const { subscription, hasActiveSubscription, getSubscription, invalidateCache } = useSubscriptionSync();
8808
9337
  const { preloadedWebView, setPreloadedWebView, preloadCampaign, consumePreloadedCampaign, isPreloadValid } = useCampaignPreload();
8809
- const clearPreloadedWebView = useCallback15(() => setPreloadedWebView(null), [setPreloadedWebView]);
9338
+ const clearPreloadedWebView = useCallback17(() => setPreloadedWebView(null), [setPreloadedWebView]);
8810
9339
  const autoPreloadTriggeredRef = useRef10(false);
8811
9340
  const preloadedWebViewRef = useRef10(preloadedWebView);
8812
9341
  const pollTimerRef = useRef10(null);
8813
9342
  const pollCancelledRef = useRef10(false);
8814
9343
  const secureStorageRef = useRef10(new SecureStorage(config.debug));
8815
- useEffect10(() => {
9344
+ useEffect11(() => {
8816
9345
  preloadedWebViewRef.current = preloadedWebView;
8817
9346
  }, [preloadedWebView]);
8818
9347
  useAppAutoEvents({
@@ -8828,20 +9357,20 @@ function PaywalloProvider({ children, config }) {
8828
9357
  storageSet: (key, value) => secureStorageRef.current.set(key, value),
8829
9358
  debug: config.debug
8830
9359
  });
8831
- const triggerRepreload = useCallback15((placement) => {
9360
+ const triggerRepreload = useCallback17((placement) => {
8832
9361
  void PaywalloClient.preloadCampaign(placement).catch(() => {
8833
9362
  });
8834
9363
  }, []);
8835
9364
  const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady: baseHandlePreloadedWebViewReady, handlePreloadedClose: baseHandlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, setPreloadedWebView);
8836
9365
  const SCREEN_HEIGHT = useMemo2(() => Dimensions4.get("window").height, []);
8837
9366
  const slideAnim = useRef10(new Animated3.Value(SCREEN_HEIGHT)).current;
8838
- const handlePreloadedWebViewReady = useCallback15(() => {
9367
+ const handlePreloadedWebViewReady = useCallback17(() => {
8839
9368
  if (PaywalloClient.getConfig()?.debug) {
8840
9369
  console.log(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
8841
9370
  }
8842
9371
  baseHandlePreloadedWebViewReady();
8843
9372
  }, [preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
8844
- const handlePreloadedClose = useCallback15(() => {
9373
+ const handlePreloadedClose = useCallback17(() => {
8845
9374
  const placement = preloadedWebView?.placement;
8846
9375
  Animated3.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 200, easing: Easing2.in(Easing2.cubic), useNativeDriver: true }).start(() => {
8847
9376
  baseHandlePreloadedClose();
@@ -8856,8 +9385,8 @@ function PaywalloProvider({ children, config }) {
8856
9385
  presentedAtRef,
8857
9386
  invalidateCache
8858
9387
  );
8859
- const [isPreloadPurchasing, setIsPreloadPurchasing] = useState12(false);
8860
- const handlePreloadedPurchase = useCallback15(async (productId) => {
9388
+ const [isPreloadPurchasing, setIsPreloadPurchasing] = useState14(false);
9389
+ const handlePreloadedPurchase = useCallback17(async (productId) => {
8861
9390
  setIsPreloadPurchasing(true);
8862
9391
  try {
8863
9392
  await rawPreloadedPurchase(productId);
@@ -8866,7 +9395,7 @@ function PaywalloProvider({ children, config }) {
8866
9395
  }
8867
9396
  }, [rawPreloadedPurchase]);
8868
9397
  const configRef = useRef10(config);
8869
- useEffect10(() => {
9398
+ useEffect11(() => {
8870
9399
  let mounted = true;
8871
9400
  initLocalization({ detectDevice: true });
8872
9401
  PaywalloClient.init(configRef.current).then(async () => {
@@ -8887,14 +9416,14 @@ function PaywalloProvider({ children, config }) {
8887
9416
  mounted = false;
8888
9417
  };
8889
9418
  }, [preloadCampaign]);
8890
- const getPaywallConfig = useCallback15(async (p) => {
9419
+ const getPaywallConfig = useCallback17(async (p) => {
8891
9420
  const api = PaywalloClient.getApiClient();
8892
9421
  return api ? api.getPaywall(p) : null;
8893
9422
  }, []);
8894
- const presentPaywall = useCallback15((placement) => {
9423
+ const presentPaywall = useCallback17((placement) => {
8895
9424
  return new Promise((resolve) => setActivePaywall({ placement, resolver: resolve }));
8896
9425
  }, []);
8897
- const trackCampaignImpression = useCallback15((placement, variantKey, campaignId) => {
9426
+ const trackCampaignImpression = useCallback17((placement, variantKey, campaignId) => {
8898
9427
  const api = PaywalloClient.getApiClient();
8899
9428
  if (!api) return;
8900
9429
  const sessionId = PaywalloClient.getSessionId();
@@ -8908,7 +9437,7 @@ function PaywalloProvider({ children, config }) {
8908
9437
  }).catch(() => {
8909
9438
  });
8910
9439
  }, []);
8911
- const presentCampaign = useCallback15(
9440
+ const presentCampaign = useCallback17(
8912
9441
  async (placement, context) => {
8913
9442
  try {
8914
9443
  if (preloadedWebView?.placement === placement && preloadedWebView.loaded && isPreloadValid(placement)) {
@@ -8982,7 +9511,7 @@ function PaywalloProvider({ children, config }) {
8982
9511
  },
8983
9512
  [preloadedWebView, isPreloadValid, consumePreloadedCampaign, presentPreloadedWebView, trackCampaignImpression]
8984
9513
  );
8985
- const restorePurchases = useCallback15(async () => {
9514
+ const restorePurchases = useCallback17(async () => {
8986
9515
  invalidateCache();
8987
9516
  try {
8988
9517
  const txs = await getIAPService().restore();
@@ -8991,13 +9520,13 @@ function PaywalloProvider({ children, config }) {
8991
9520
  return { success: false, restoredProducts: [], error: error instanceof Error ? error : new PurchaseError(PURCHASE_ERROR_CODES.RESTORE_FAILED, String(error)) };
8992
9521
  }
8993
9522
  }, [invalidateCache]);
8994
- const handlePaywallResult = useCallback15((result) => {
9523
+ const handlePaywallResult = useCallback17((result) => {
8995
9524
  if (activePaywall) {
8996
9525
  activePaywall.resolver(result);
8997
9526
  setActivePaywall(null);
8998
9527
  }
8999
9528
  }, [activePaywall]);
9000
- const bridgePaywallPresenter = useCallback15((placement) => {
9529
+ const bridgePaywallPresenter = useCallback17((placement) => {
9001
9530
  if (PaywalloClient.getConfig()?.debug) {
9002
9531
  console.log(`[Paywallo PRESENT] called \u2014 preloaded: ${preloadedWebViewRef.current?.loaded === true} placement: ${placement}`);
9003
9532
  }
@@ -9026,7 +9555,7 @@ function PaywalloProvider({ children, config }) {
9026
9555
  }
9027
9556
  return presentCampaign(placement);
9028
9557
  }, [presentPreloadedWebView, presentCampaign]);
9029
- useEffect10(() => {
9558
+ useEffect11(() => {
9030
9559
  if (!isInitialized) return;
9031
9560
  const onEmergency = async (id) => {
9032
9561
  try {
@@ -9053,7 +9582,7 @@ function PaywalloProvider({ children, config }) {
9053
9582
  };
9054
9583
  }, [isInitialized, bridgePaywallPresenter, getSubscription, hasActiveSubscription, restorePurchases]);
9055
9584
  const isReady = isInitialized && !isLoadingProducts;
9056
- const noOp = useCallback15(() => {
9585
+ const noOp = useCallback17(() => {
9057
9586
  }, []);
9058
9587
  const contextValue = useMemo2(() => ({
9059
9588
  config: PaywalloClient.getConfig(),
@@ -9295,6 +9824,8 @@ export {
9295
9824
  PaywalloContext,
9296
9825
  PaywalloError,
9297
9826
  PaywalloProvider,
9827
+ PlanCache,
9828
+ PlanService,
9298
9829
  PurchaseError,
9299
9830
  SESSION_ERROR_CODES,
9300
9831
  SessionError,
@@ -9317,6 +9848,8 @@ export {
9317
9848
  offlineQueue,
9318
9849
  onboardingManager,
9319
9850
  parseVariables,
9851
+ planCache,
9852
+ planService,
9320
9853
  queueProcessor,
9321
9854
  resolveText,
9322
9855
  sessionManager,
@@ -9326,6 +9859,8 @@ export {
9326
9859
  useOnboarding,
9327
9860
  usePaywallContext,
9328
9861
  usePaywallo,
9862
+ usePlanPurchase,
9863
+ usePlans,
9329
9864
  useProducts,
9330
9865
  usePurchase,
9331
9866
  useSubscription