@virex-tech/paywallo-sdk 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -130,7 +130,7 @@ var CLIENT_ERROR_CODES = {
130
130
 
131
131
  // src/domains/paywall/PaywallModal.tsx
132
132
  var import_react5 = require("react");
133
- var import_react_native5 = require("react-native");
133
+ var import_react_native6 = require("react-native");
134
134
 
135
135
  // src/domains/paywall/usePaywallActions.ts
136
136
  var import_react2 = require("react");
@@ -227,9 +227,6 @@ var SecureStorage = class {
227
227
  // eslint-disable-next-line @typescript-eslint/no-useless-constructor
228
228
  constructor(_debug = false) {
229
229
  }
230
- /**
231
- * Get a value, trying Keychain first (survives reinstalls on iOS), then AsyncStorage.
232
- */
233
230
  async get(key) {
234
231
  const keychainValue = await this.getFromKeychain(key);
235
232
  if (keychainValue) {
@@ -245,9 +242,6 @@ var SecureStorage = class {
245
242
  return null;
246
243
  }
247
244
  }
248
- /**
249
- * Set a value in both Keychain (if available) and AsyncStorage.
250
- */
251
245
  async set(key, value) {
252
246
  try {
253
247
  await Promise.all([
@@ -311,11 +305,13 @@ var secureStorage = new SecureStorage();
311
305
 
312
306
  // src/domains/identity/IdentityManager.ts
313
307
  var DEVICE_ID_KEY = "device_id";
308
+ var ANON_ID_KEY = "anon_id";
314
309
  var USER_EMAIL_KEY = "user_email";
315
310
  var USER_PROPERTIES_KEY = "user_properties";
316
311
  var IdentityManager = class {
317
312
  constructor() {
318
313
  this.deviceId = null;
314
+ this.anonId = null;
319
315
  this.email = null;
320
316
  this.properties = {};
321
317
  this.apiClient = null;
@@ -342,23 +338,23 @@ var IdentityManager = class {
342
338
  try {
343
339
  if (DeviceInfo) {
344
340
  this.deviceId = await DeviceInfo.getUniqueId();
345
- this.log("Got device ID from DeviceInfo:", this.deviceId);
346
341
  } else {
347
342
  this.deviceId = generateUUID();
348
- this.log("DeviceInfo not installed, using generated UUID:", this.deviceId);
349
343
  }
350
344
  } catch {
351
345
  this.deviceId = generateUUID();
352
- this.log("DeviceInfo unavailable, using generated UUID:", this.deviceId);
353
346
  }
354
347
  const stored = await this.secureStorage.set(DEVICE_ID_KEY, this.deviceId);
355
348
  if (!stored) {
356
- this.log("Failed to persist device ID to secure storage, using fallback");
357
349
  await this.secureStorage.set("device_id_fallback", this.deviceId).catch(() => {
358
- this.log("Fallback also failed \u2014 device ID only in memory");
359
350
  });
360
351
  }
361
352
  }
353
+ if (!this.anonId) {
354
+ this.anonId = "$paywallo_anon:" + generateUUID();
355
+ await this.secureStorage.set(ANON_ID_KEY, this.anonId).catch(() => {
356
+ });
357
+ }
362
358
  this.initialized = true;
363
359
  return this.deviceId ?? "";
364
360
  }
@@ -382,21 +378,18 @@ var IdentityManager = class {
382
378
  this.properties = { ...this.properties, ...properties };
383
379
  await this.secureStorage.set(USER_PROPERTIES_KEY, JSON.stringify(this.properties));
384
380
  }
385
- if (this.apiClient && this.deviceId) {
381
+ if (this.apiClient && this.anonId) {
386
382
  try {
387
- await this.apiClient.identify(this.deviceId, properties, email);
388
- this.log("Identity updated on server");
389
- } catch (error) {
390
- this.log("Failed to update identity on server:", error);
383
+ await this.apiClient.identify(this.anonId, properties, email, this.deviceId ?? void 0);
384
+ } catch {
391
385
  }
392
386
  }
393
387
  }
394
388
  getDistinctId() {
395
- if (!this.initialized || !this.deviceId) {
396
- if (this.debug) console.warn("[Paywallo Identity] getDistinctId called before initialization");
389
+ if (!this.initialized || !this.anonId) {
397
390
  return "";
398
391
  }
399
- return this.deviceId;
392
+ return this.anonId;
400
393
  }
401
394
  getDeviceId() {
402
395
  return this.deviceId;
@@ -411,24 +404,22 @@ var IdentityManager = class {
411
404
  this.ensureInitialized();
412
405
  this.properties = { ...this.properties, ...properties };
413
406
  await this.secureStorage.set(USER_PROPERTIES_KEY, JSON.stringify(this.properties));
414
- if (this.apiClient && this.deviceId) {
407
+ if (this.apiClient && this.anonId) {
415
408
  try {
416
- await this.apiClient.identify(this.deviceId, this.properties, this.email ?? void 0);
417
- } catch (error) {
418
- this.log("Failed to update properties on server:", error);
409
+ await this.apiClient.identify(this.anonId, this.properties, this.email ?? void 0, this.deviceId ?? void 0);
410
+ } catch {
419
411
  }
420
412
  }
421
413
  }
422
414
  async reset() {
415
+ const newAnonId = "$paywallo_anon:" + generateUUID();
416
+ this.anonId = newAnonId;
423
417
  this.email = null;
424
418
  this.properties = {};
425
- this.initialized = false;
426
- this.apiClient = null;
427
- this.deviceId = null;
428
419
  await Promise.all([
420
+ this.secureStorage?.set(ANON_ID_KEY, newAnonId) ?? Promise.resolve(false),
429
421
  this.secureStorage?.remove(USER_EMAIL_KEY) ?? Promise.resolve(false),
430
- this.secureStorage?.remove(USER_PROPERTIES_KEY) ?? Promise.resolve(false),
431
- this.secureStorage?.remove("device_id_fallback") ?? Promise.resolve(false)
422
+ this.secureStorage?.remove(USER_PROPERTIES_KEY) ?? Promise.resolve(false)
432
423
  ]);
433
424
  }
434
425
  getState() {
@@ -445,21 +436,23 @@ var IdentityManager = class {
445
436
  if (!this.secureStorage) {
446
437
  return;
447
438
  }
448
- const [storedDeviceId, storedEmail, storedPropertiesJson] = await Promise.all([
439
+ const [storedDeviceId, storedAnonId, storedEmail, storedPropertiesJson] = await Promise.all([
449
440
  this.secureStorage.get(DEVICE_ID_KEY),
441
+ this.secureStorage.get(ANON_ID_KEY),
450
442
  this.secureStorage.get(USER_EMAIL_KEY),
451
443
  this.secureStorage.get(USER_PROPERTIES_KEY)
452
444
  ]);
453
445
  if (storedDeviceId) {
454
446
  this.deviceId = storedDeviceId;
455
- this.log("Loaded device ID from storage:", this.deviceId);
456
447
  } else {
457
448
  const fallbackDeviceId = await this.secureStorage.get("device_id_fallback");
458
449
  if (fallbackDeviceId) {
459
450
  this.deviceId = fallbackDeviceId;
460
- this.log("Loaded device ID from fallback storage:", this.deviceId);
461
451
  }
462
452
  }
453
+ if (storedAnonId) {
454
+ this.anonId = storedAnonId;
455
+ }
463
456
  if (storedEmail) {
464
457
  this.email = storedEmail;
465
458
  }
@@ -479,11 +472,6 @@ var IdentityManager = class {
479
472
  );
480
473
  }
481
474
  }
482
- log(...args) {
483
- if (this.debug) {
484
- console.warn("[Paywallo Identity]", ...args);
485
- }
486
- }
487
475
  };
488
476
  var identityManager = new IdentityManager();
489
477
 
@@ -568,7 +556,6 @@ var nativeStoreKit = {
568
556
  isAvailable,
569
557
  async getProducts(productIds) {
570
558
  if (!PaywalloStoreKitNative) {
571
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] StoreKit native module not available");
572
559
  return [];
573
560
  }
574
561
  const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
@@ -639,7 +626,6 @@ var IAPService = class {
639
626
  }
640
627
  return products;
641
628
  } catch (error) {
642
- if (this.debug) console.warn("[Paywallo][Products] StoreKit error:", error);
643
629
  return [];
644
630
  }
645
631
  }
@@ -649,35 +635,46 @@ var IAPService = class {
649
635
  getCachedProducts() {
650
636
  return new Map(this.productsCache);
651
637
  }
638
+ async validateWithRetry(attempt, maxRetries = 2, delayMs = 3e3) {
639
+ let lastError;
640
+ for (let i = 0; i <= maxRetries; i++) {
641
+ try {
642
+ return await attempt();
643
+ } catch (error) {
644
+ lastError = error;
645
+ if (i < maxRetries) await new Promise((resolve) => setTimeout(resolve, delayMs));
646
+ }
647
+ }
648
+ throw lastError;
649
+ }
652
650
  async validateWithServer(purchase, productId, options) {
653
651
  const apiClient = this.getApiClient();
654
652
  const product = this.productsCache.get(productId);
655
653
  const platform = import_react_native2.Platform.OS;
656
654
  const distinctId = PaywalloClient.getDistinctId();
657
- const validation = await apiClient.validatePurchase(
658
- platform,
659
- purchase.receipt,
660
- purchase.productId,
661
- purchase.transactionId,
662
- product?.priceValue ?? 0,
663
- product?.currency ?? "USD",
664
- this.getDeviceCountry(),
665
- distinctId,
666
- options?.paywallPlacement,
667
- options?.variantKey
655
+ return this.validateWithRetry(
656
+ () => apiClient.validatePurchase(
657
+ platform,
658
+ purchase.receipt,
659
+ purchase.productId,
660
+ purchase.transactionId,
661
+ product?.priceValue ?? 0,
662
+ product?.currency ?? "USD",
663
+ this.getDeviceCountry(),
664
+ distinctId,
665
+ options?.paywallPlacement,
666
+ options?.variantKey
667
+ )
668
668
  );
669
- return validation;
670
669
  }
671
670
  async finishTransaction(transactionId) {
672
671
  try {
673
672
  await nativeStoreKit.finishTransaction(transactionId);
674
673
  } catch (finishError) {
675
- if (this.debug) console.warn("[Paywallo][Purchase] Failed to finish transaction:", finishError instanceof Error ? finishError.message : finishError);
676
674
  }
677
675
  }
678
676
  async purchase(productId, options) {
679
677
  if (!nativeStoreKit.isAvailable()) {
680
- if (this.debug) console.warn("[Paywallo][Purchase] StoreKit not available");
681
678
  return {
682
679
  success: false,
683
680
  error: new PurchaseError(PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE, "In-app purchases are not available on this device")
@@ -688,11 +685,11 @@ var IAPService = class {
688
685
  if (!purchase) {
689
686
  return { success: false };
690
687
  }
688
+ options?.onValidating?.();
691
689
  let validation = null;
692
690
  try {
693
691
  validation = await this.validateWithServer(purchase, productId, options);
694
692
  } catch (validationError) {
695
- if (this.debug) console.warn("[Paywallo][Purchase] Server validation FAILED:", validationError instanceof Error ? validationError.message : validationError);
696
693
  return {
697
694
  success: false,
698
695
  error: new PurchaseError(PURCHASE_ERROR_CODES.VALIDATION_FAILED, validationError instanceof Error ? validationError.message : "Server validation failed")
@@ -708,7 +705,6 @@ var IAPService = class {
708
705
  return { success: true, purchase };
709
706
  } catch (error) {
710
707
  const e = error instanceof PurchaseError ? error : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, error instanceof Error ? error.message : String(error));
711
- if (this.debug) console.warn("[Paywallo][Purchase] FAILED:", e.message);
712
708
  return { success: false, error: e };
713
709
  }
714
710
  }
@@ -744,14 +740,12 @@ var IAPService = class {
744
740
  try {
745
741
  await nativeStoreKit.finishTransaction(tx.transactionId);
746
742
  } catch (finishError) {
747
- if (this.debug) console.warn("[Paywallo][Restore] Failed to finish tx:", tx.transactionId, finishError instanceof Error ? finishError.message : finishError);
748
743
  }
749
744
  validated.push(tx);
750
745
  }
751
746
  }
752
747
  return validated;
753
748
  } catch (error) {
754
- if (this.debug) console.warn("[Paywallo][Restore] \u274C FAILED:", error instanceof Error ? error.message : error);
755
749
  return [];
756
750
  }
757
751
  }
@@ -819,7 +813,7 @@ function usePaywallTracking() {
819
813
 
820
814
  // src/domains/paywall/usePaywallActions.ts
821
815
  var SCREEN_HEIGHT = import_react_native3.Dimensions.get("window").height;
822
- function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId) {
816
+ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId, openAnim) {
823
817
  const [isPurchasing, setIsPurchasing] = (0, import_react2.useState)(false);
824
818
  const [isClosing, setIsClosing] = (0, import_react2.useState)(false);
825
819
  const slideAnim = (0, import_react2.useRef)(new import_react_native3.Animated.Value(0)).current;
@@ -828,7 +822,8 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
828
822
  const handleClose = (0, import_react2.useCallback)(() => {
829
823
  if (isClosing) return;
830
824
  setIsClosing(true);
831
- import_react_native3.Animated.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 300, useNativeDriver: true }).start(() => {
825
+ const animTarget = openAnim ?? slideAnim;
826
+ import_react_native3.Animated.timing(animTarget, { toValue: SCREEN_HEIGHT, duration: 350, useNativeDriver: true }).start(() => {
832
827
  if (paywallConfig) {
833
828
  const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
834
829
  void trackDismissed({ placement, paywallId: paywallConfig.id, durationSeconds, action: "close", variantKey, campaignId });
@@ -837,7 +832,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
837
832
  setIsClosing(false);
838
833
  onResult({ presented: true, purchased: false, cancelled: true, restored: false });
839
834
  });
840
- }, [isClosing, onResult, paywallConfig, placement, slideAnim, trackDismissed, variantKey, campaignId]);
835
+ }, [isClosing, openAnim, onResult, paywallConfig, placement, slideAnim, trackDismissed, variantKey, campaignId]);
841
836
  const handlePurchase = (0, import_react2.useCallback)(async (productId) => {
842
837
  if (isPurchasing) return;
843
838
  setIsPurchasing(true);
@@ -855,7 +850,6 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
855
850
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
856
851
  }
857
852
  } catch (error) {
858
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Purchase error:", error);
859
853
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
860
854
  } finally {
861
855
  setIsPurchasing(false);
@@ -876,7 +870,6 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
876
870
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
877
871
  }
878
872
  } catch (error) {
879
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Restore error:", error);
880
873
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
881
874
  } finally {
882
875
  setIsPurchasing(false);
@@ -951,11 +944,6 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
951
944
  const storeProducts = await getIAPService().loadProducts(productIds);
952
945
  const map = /* @__PURE__ */ new Map();
953
946
  for (const p of storeProducts) map.set(p.productId, p);
954
- if (PaywalloClient.getConfig()?.debug) {
955
- for (const [id, p] of map) {
956
- console.warn(`[Paywallo][CURRENCY-CHECK] product=${id} currency=${p.currency} price="${p.price}" localizedPrice="${p.localizedPrice}" priceValue=${p.priceValue} fallbackUSD=${!p.currency || p.currency === "USD" ? "POSSIBLE" : "NO"}`);
957
- }
958
- }
959
947
  setProducts(map);
960
948
  } catch {
961
949
  setProducts(/* @__PURE__ */ new Map());
@@ -976,7 +964,6 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
976
964
  }
977
965
  } catch (err) {
978
966
  const e = err instanceof Error ? err : new PaywallError(PAYWALL_ERROR_CODES.LOAD_FAILED, String(err));
979
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Error loading paywall:", e.message);
980
967
  setError(e);
981
968
  }
982
969
  };
@@ -985,11 +972,96 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
985
972
  return { paywallConfig, products, error };
986
973
  }
987
974
 
975
+ // src/utils/localization.ts
976
+ var import_react_native4 = require("react-native");
977
+ var currentLanguage = "pt-BR";
978
+ var defaultLanguage = "pt-BR";
979
+ function setCurrentLanguage(language) {
980
+ currentLanguage = language;
981
+ }
982
+ function setDefaultLanguage(language) {
983
+ defaultLanguage = language;
984
+ }
985
+ function getCurrentLanguage() {
986
+ return currentLanguage;
987
+ }
988
+ function getDefaultLanguage() {
989
+ return defaultLanguage;
990
+ }
991
+ function getLocalizedString(text) {
992
+ if (!text) return "";
993
+ if (typeof text === "string") return text;
994
+ return text[currentLanguage] ?? text[defaultLanguage] ?? Object.values(text)[0] ?? "";
995
+ }
996
+ function detectDeviceLanguage() {
997
+ try {
998
+ if (import_react_native4.Platform.OS === "ios") {
999
+ const settings = import_react_native4.NativeModules.SettingsManager?.settings;
1000
+ return settings?.AppleLocale ?? settings?.AppleLanguages?.[0] ?? "en-US";
1001
+ }
1002
+ return import_react_native4.NativeModules.I18nManager?.localeIdentifier ?? "en-US";
1003
+ } catch {
1004
+ return "en-US";
1005
+ }
1006
+ }
1007
+ var SDK_STRINGS = {
1008
+ paywallErrorTitle: {
1009
+ "pt-BR": "Algo deu errado",
1010
+ "pt": "Algo deu errado",
1011
+ "en": "Something went wrong",
1012
+ "en-US": "Something went wrong",
1013
+ "en-GB": "Something went wrong",
1014
+ "es": "Algo sali\xF3 mal",
1015
+ "es-419": "Algo sali\xF3 mal"
1016
+ },
1017
+ paywallRetryButton: {
1018
+ "pt-BR": "Tentar novamente",
1019
+ "pt": "Tentar novamente",
1020
+ "en": "Try again",
1021
+ "en-US": "Try again",
1022
+ "en-GB": "Try again",
1023
+ "es": "Reintentar",
1024
+ "es-419": "Reintentar"
1025
+ },
1026
+ paywallCloseButton: {
1027
+ "pt-BR": "Fechar",
1028
+ "pt": "Fechar",
1029
+ "en": "Close",
1030
+ "en-US": "Close",
1031
+ "en-GB": "Close",
1032
+ "es": "Cerrar",
1033
+ "es-419": "Cerrar"
1034
+ }
1035
+ };
1036
+ function getSdkString(key) {
1037
+ const map = SDK_STRINGS[key];
1038
+ const lang = currentLanguage;
1039
+ if (lang in map) return map[lang];
1040
+ const base = lang.split("-")[0];
1041
+ if (base && base in map) return map[base];
1042
+ if (defaultLanguage in map) return map[defaultLanguage];
1043
+ return Object.values(map)[0];
1044
+ }
1045
+ function initLocalization(options) {
1046
+ if (options?.defaultLanguage) {
1047
+ defaultLanguage = options.defaultLanguage;
1048
+ }
1049
+ if (options?.detectDevice !== false) {
1050
+ const deviceLanguage = detectDeviceLanguage();
1051
+ currentLanguage = deviceLanguage;
1052
+ }
1053
+ }
1054
+
988
1055
  // src/domains/paywall/PaywallWebView.tsx
989
1056
  var import_react4 = require("react");
990
- var import_react_native4 = require("react-native");
1057
+ var import_react_native5 = require("react-native");
991
1058
 
992
1059
  // src/domains/paywall/parseWebViewMessage.ts
1060
+ function deriveMessageId(message) {
1061
+ if (message.id) return message.id;
1062
+ const payloadKey = message.payload?.productId ?? message.payload?.url ?? (message.payload?.timestamp != null ? String(message.payload.timestamp) : "");
1063
+ return `${message.type}:${payloadKey}`;
1064
+ }
993
1065
  function parseWebViewMessage(raw) {
994
1066
  try {
995
1067
  const parsed = JSON.parse(raw);
@@ -1013,7 +1085,7 @@ function buildPaywallInjectionScript(data) {
1013
1085
  secondaryProductId: data.secondaryProductId
1014
1086
  }
1015
1087
  };
1016
- return `(function(){var d=${JSON.stringify(paywallData)},a=0,m=50;function t(){a++;if(window.receiveNativeMessage){window.receiveNativeMessage(d);return;}if(a<m){setTimeout(t,100);}else{console.warn('[Paywallo WV] FAILED to inject after '+m+' attempts');}}t();})();true;`;
1088
+ return `(function(){var d=${JSON.stringify(paywallData)},a=0,m=50;function t(){a++;if(window.receiveNativeMessage){window.receiveNativeMessage(d);return;}if(a<m){setTimeout(t,100);}}t();})();true;`;
1017
1089
  }
1018
1090
  function buildPurchaseStateScript(isPurchasing) {
1019
1091
  return `if(window.receiveNativeMessage){window.receiveNativeMessage({type:"purchaseState",data:{isPurchasing:${isPurchasing}}});}true;`;
@@ -1024,17 +1096,24 @@ var import_jsx_runtime = require("react/jsx-runtime");
1024
1096
  var NativeWebViewComponent = null;
1025
1097
  function getNativeWebView() {
1026
1098
  if (!NativeWebViewComponent) {
1027
- NativeWebViewComponent = (0, import_react_native4.requireNativeComponent)("PaywalloWebView");
1099
+ NativeWebViewComponent = (0, import_react_native5.requireNativeComponent)("PaywalloWebView");
1028
1100
  }
1029
1101
  return NativeWebViewComponent;
1030
1102
  }
1031
1103
  var webViewEmitter = null;
1032
1104
  function getWebViewEmitter() {
1033
1105
  if (!webViewEmitter) {
1034
- webViewEmitter = new import_react_native4.NativeEventEmitter(import_react_native4.NativeModules.PaywalloWebViewModule);
1106
+ webViewEmitter = new import_react_native5.NativeEventEmitter(import_react_native5.NativeModules.PaywalloWebViewModule);
1035
1107
  }
1036
1108
  return webViewEmitter;
1037
1109
  }
1110
+ function parseErrorEvent(raw) {
1111
+ return {
1112
+ code: typeof raw["code"] === "number" ? raw["code"] : -1,
1113
+ description: typeof raw["description"] === "string" ? raw["description"] : "WebView error",
1114
+ url: typeof raw["url"] === "string" ? raw["url"] : void 0
1115
+ };
1116
+ }
1038
1117
  function PaywallWebView({
1039
1118
  paywallId,
1040
1119
  craftData,
@@ -1042,16 +1121,51 @@ function PaywallWebView({
1042
1121
  primaryProductId,
1043
1122
  secondaryProductId,
1044
1123
  isPurchasing,
1124
+ webUrl: webUrlProp,
1045
1125
  onPurchase,
1046
1126
  onClose,
1047
1127
  onRestore,
1048
- onReady
1128
+ onReady,
1129
+ onError
1049
1130
  }) {
1050
1131
  const paywallDataSent = (0, import_react4.useRef)(false);
1051
1132
  const loadEndReceived = (0, import_react4.useRef)(false);
1052
1133
  const [scriptToInject, setScriptToInject] = (0, import_react4.useState)(void 0);
1134
+ const onErrorRef = (0, import_react4.useRef)(onError);
1135
+ onErrorRef.current = onError;
1136
+ const onReadyRef = (0, import_react4.useRef)(onReady);
1137
+ onReadyRef.current = onReady;
1053
1138
  const NativeWebView = (0, import_react4.useMemo)(() => getNativeWebView(), []);
1054
- const webUrl = (0, import_react4.useMemo)(() => PaywalloClient.getWebUrl() ?? "http://localhost:3000", []);
1139
+ const [resolvedWebUrl, setResolvedWebUrl] = (0, import_react4.useState)(
1140
+ () => webUrlProp ?? PaywalloClient.getWebUrl()
1141
+ );
1142
+ (0, import_react4.useEffect)(() => {
1143
+ if (webUrlProp !== void 0) {
1144
+ setResolvedWebUrl(webUrlProp ?? null);
1145
+ return;
1146
+ }
1147
+ const url = PaywalloClient.getWebUrl();
1148
+ if (url) {
1149
+ setResolvedWebUrl(url);
1150
+ return;
1151
+ }
1152
+ const interval = setInterval(() => {
1153
+ const u = PaywalloClient.getWebUrl();
1154
+ if (u) {
1155
+ setResolvedWebUrl(u);
1156
+ clearInterval(interval);
1157
+ }
1158
+ }, 200);
1159
+ return () => clearInterval(interval);
1160
+ }, [webUrlProp]);
1161
+ (0, import_react4.useEffect)(() => {
1162
+ if (!resolvedWebUrl) {
1163
+ onErrorRef.current?.({
1164
+ code: -1,
1165
+ description: "Paywall URL not configured. SDK may not be initialized."
1166
+ });
1167
+ }
1168
+ }, []);
1055
1169
  const buildAndSetInjectionScript = (0, import_react4.useCallback)(() => {
1056
1170
  if (!craftData || paywallDataSent.current) return;
1057
1171
  paywallDataSent.current = true;
@@ -1062,19 +1176,21 @@ function PaywallWebView({
1062
1176
  secondaryProductId
1063
1177
  });
1064
1178
  setScriptToInject(script);
1065
- onReady?.();
1066
- }, [craftData, products, primaryProductId, secondaryProductId, onReady]);
1067
- const lastProcessedRef = (0, import_react4.useRef)({ type: "", time: 0 });
1179
+ }, [craftData, products, primaryProductId, secondaryProductId]);
1180
+ const processedIdsRef = (0, import_react4.useRef)(/* @__PURE__ */ new Set());
1068
1181
  const handleParsedMessage = (0, import_react4.useCallback)(
1069
1182
  (message) => {
1070
- const now = Date.now();
1071
- if (message.type === lastProcessedRef.current.type && now - lastProcessedRef.current.time < 500) {
1072
- return;
1073
- }
1074
- lastProcessedRef.current = { type: message.type, time: now };
1183
+ const msgId = deriveMessageId(message);
1184
+ if (processedIdsRef.current.has(msgId)) return;
1185
+ processedIdsRef.current.add(msgId);
1186
+ setTimeout(() => processedIdsRef.current.delete(msgId), 5e3);
1075
1187
  switch (message.type) {
1076
1188
  case "ready":
1077
- if (!paywallDataSent.current) buildAndSetInjectionScript();
1189
+ if (!paywallDataSent.current) {
1190
+ buildAndSetInjectionScript();
1191
+ } else {
1192
+ onReadyRef.current?.();
1193
+ }
1078
1194
  break;
1079
1195
  case "purchase":
1080
1196
  if (message.payload?.productId) onPurchase(message.payload.productId);
@@ -1089,7 +1205,7 @@ function PaywallWebView({
1089
1205
  if (message.payload?.url) {
1090
1206
  const url = message.payload.url;
1091
1207
  if (/^https?:\/\//i.test(url) || /^mailto:/i.test(url)) {
1092
- void import_react_native4.Linking.openURL(url);
1208
+ void import_react_native5.Linking.openURL(url);
1093
1209
  }
1094
1210
  }
1095
1211
  break;
@@ -1109,17 +1225,51 @@ function PaywallWebView({
1109
1225
  const message = parseWebViewMessage(event.data);
1110
1226
  if (message) handleParsedMessage(message);
1111
1227
  });
1228
+ const errorSub = emitter.addListener(
1229
+ "PaywalloWebViewError",
1230
+ (event) => {
1231
+ onErrorRef.current?.(parseErrorEvent(event));
1232
+ }
1233
+ );
1112
1234
  return () => {
1113
1235
  loadEndSub.remove();
1114
1236
  messageSub.remove();
1237
+ errorSub.remove();
1115
1238
  };
1116
1239
  }, [buildAndSetInjectionScript, handleParsedMessage]);
1240
+ (0, import_react4.useEffect)(() => {
1241
+ const timer = setTimeout(() => {
1242
+ if (!paywallDataSent.current) {
1243
+ onErrorRef.current?.({
1244
+ code: -1001,
1245
+ description: "Paywall load timed out after 10 seconds"
1246
+ });
1247
+ }
1248
+ }, 1e4);
1249
+ return () => clearTimeout(timer);
1250
+ }, []);
1117
1251
  const handleLoadEnd = (0, import_react4.useCallback)(() => {
1118
1252
  if (!loadEndReceived.current) {
1119
1253
  loadEndReceived.current = true;
1120
1254
  buildAndSetInjectionScript();
1121
1255
  }
1122
1256
  }, [buildAndSetInjectionScript]);
1257
+ const handleNativeError = (0, import_react4.useCallback)((event) => {
1258
+ const ne = event.nativeEvent;
1259
+ let errorData;
1260
+ if (ne["payload"] && typeof ne["payload"] === "object") {
1261
+ errorData = ne["payload"];
1262
+ } else if (typeof ne["data"] === "string") {
1263
+ try {
1264
+ errorData = JSON.parse(ne["data"]);
1265
+ } catch {
1266
+ errorData = {};
1267
+ }
1268
+ } else {
1269
+ errorData = {};
1270
+ }
1271
+ onErrorRef.current?.(parseErrorEvent(errorData));
1272
+ }, []);
1123
1273
  const handleMessage = (0, import_react4.useCallback)(
1124
1274
  (event) => {
1125
1275
  const message = parseWebViewMessage(event.nativeEvent.data);
@@ -1131,26 +1281,40 @@ function PaywallWebView({
1131
1281
  if (!paywallDataSent.current) return;
1132
1282
  setScriptToInject(buildPurchaseStateScript(isPurchasing));
1133
1283
  }, [isPurchasing]);
1134
- const paywallUrl = `${webUrl}/paywall/${paywallId}`;
1135
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native4.View, { style: styles.container, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1284
+ if (!resolvedWebUrl) return null;
1285
+ const paywallUrl = `${resolvedWebUrl}/paywall/${paywallId}`;
1286
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native5.View, { style: styles.container, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1136
1287
  NativeWebView,
1137
1288
  {
1138
1289
  sourceUrl: paywallUrl,
1139
1290
  injectedJavaScript: scriptToInject,
1140
1291
  onMessage: handleMessage,
1141
1292
  onLoadEnd: handleLoadEnd,
1293
+ onError: handleNativeError,
1142
1294
  style: styles.webview
1143
1295
  }
1144
1296
  ) });
1145
1297
  }
1146
- var styles = import_react_native4.StyleSheet.create({
1298
+ var styles = import_react_native5.StyleSheet.create({
1147
1299
  container: { flex: 1 },
1148
1300
  webview: { flex: 1, backgroundColor: "transparent" }
1149
1301
  });
1150
1302
 
1151
1303
  // src/domains/paywall/PaywallModal.tsx
1152
1304
  var import_jsx_runtime2 = require("react/jsx-runtime");
1153
- var SCREEN_HEIGHT2 = import_react_native5.Dimensions.get("window").height;
1305
+ var SCREEN_HEIGHT2 = import_react_native6.Dimensions.get("window").height;
1306
+ function ErrorFallback({ description, onRetry, onClose }) {
1307
+ const overrides = PaywalloClient.getConfig()?.errorStrings;
1308
+ const title = overrides?.title ?? getSdkString("paywallErrorTitle");
1309
+ const retryLabel = overrides?.retry ?? getSdkString("paywallRetryButton");
1310
+ const closeLabel = overrides?.close ?? getSdkString("paywallCloseButton");
1311
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.View, { style: styles2.fallbackContainer, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native6.View, { style: styles2.fallbackCard, children: [
1312
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Text, { style: styles2.fallbackTitle, children: title }),
1313
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Text, { style: styles2.fallbackDescription, children: description }),
1314
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Pressable, { style: styles2.retryButton, onPress: onRetry, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Text, { style: styles2.retryButtonText, children: retryLabel }) }),
1315
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Pressable, { style: styles2.closeButton, onPress: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Text, { style: styles2.closeButtonText, children: closeLabel }) })
1316
+ ] }) });
1317
+ }
1154
1318
  function PaywallModal({
1155
1319
  placement,
1156
1320
  visible,
@@ -1168,32 +1332,64 @@ function PaywallModal({
1168
1332
  variantKey,
1169
1333
  campaignId
1170
1334
  );
1171
- const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId);
1172
- const openAnim = (0, import_react5.useRef)(new import_react_native5.Animated.Value(SCREEN_HEIGHT2)).current;
1335
+ const openAnim = (0, import_react5.useRef)(new import_react_native6.Animated.Value(SCREEN_HEIGHT2)).current;
1336
+ const [modalVisible, setModalVisible] = (0, import_react5.useState)(false);
1337
+ const handleResult = (0, import_react5.useCallback)((result) => {
1338
+ setModalVisible(false);
1339
+ onResult(result);
1340
+ }, [onResult]);
1341
+ const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, handleResult, variantKey, campaignId, openAnim);
1342
+ const [webViewError, setWebViewError] = (0, import_react5.useState)(null);
1343
+ const [retryCount, setRetryCount] = (0, import_react5.useState)(0);
1344
+ const retryCountRef = (0, import_react5.useRef)(0);
1345
+ const handleWebViewError = (0, import_react5.useCallback)(
1346
+ (err) => {
1347
+ if (retryCountRef.current >= 1) {
1348
+ setWebViewError({ code: err.code, description: err.description });
1349
+ } else {
1350
+ retryCountRef.current += 1;
1351
+ setRetryCount(retryCountRef.current);
1352
+ }
1353
+ },
1354
+ []
1355
+ );
1356
+ const handleRetry = (0, import_react5.useCallback)(() => {
1357
+ retryCountRef.current = 0;
1358
+ setWebViewError(null);
1359
+ setRetryCount(0);
1360
+ }, []);
1173
1361
  (0, import_react5.useEffect)(() => {
1174
1362
  if (visible) {
1363
+ setModalVisible(true);
1175
1364
  openAnim.setValue(SCREEN_HEIGHT2);
1176
- import_react_native5.Animated.timing(openAnim, {
1365
+ import_react_native6.Animated.timing(openAnim, {
1177
1366
  toValue: 0,
1178
1367
  duration: 550,
1179
- easing: import_react_native5.Easing.out(import_react_native5.Easing.cubic),
1368
+ easing: import_react_native6.Easing.out(import_react_native6.Easing.cubic),
1180
1369
  useNativeDriver: true
1181
1370
  }).start();
1182
1371
  }
1183
1372
  }, [visible, openAnim]);
1184
- if (!visible) return null;
1373
+ if (!modalVisible && !visible) return null;
1185
1374
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1186
- import_react_native5.Modal,
1375
+ import_react_native6.Modal,
1187
1376
  {
1188
- visible,
1377
+ visible: modalVisible,
1189
1378
  animationType: "none",
1190
1379
  presentationStyle: "overFullScreen",
1191
1380
  onRequestClose: handleClose,
1192
1381
  transparent: true,
1193
1382
  statusBarTranslucent: true,
1194
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.View, { style: styles2.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Animated.View, { style: [styles2.animatedContainer, { transform: [{ translateY: openAnim }] }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native5.View, { style: styles2.container, children: [
1195
- error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.View, { style: styles2.errorContainer, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Text, { style: styles2.errorText, children: error.message }) }),
1196
- !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1383
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.View, { style: styles2.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Animated.View, { style: [styles2.animatedContainer, { transform: [{ translateY: openAnim }] }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native6.View, { style: styles2.container, children: [
1384
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.View, { style: styles2.errorContainer, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Text, { style: styles2.errorText, children: error.message }) }),
1385
+ !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: webViewError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1386
+ ErrorFallback,
1387
+ {
1388
+ description: webViewError.description,
1389
+ onRetry: handleRetry,
1390
+ onClose: handleClose
1391
+ }
1392
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1197
1393
  PaywallWebView,
1198
1394
  {
1199
1395
  paywallId: paywallConfig.id,
@@ -1204,39 +1400,67 @@ function PaywallModal({
1204
1400
  isPurchasing,
1205
1401
  onClose: handleClose,
1206
1402
  onPurchase: handlePurchase,
1207
- onRestore: handleRestore
1208
- }
1209
- )
1403
+ onRestore: handleRestore,
1404
+ onError: handleWebViewError
1405
+ },
1406
+ retryCount
1407
+ ) })
1210
1408
  ] }) }) })
1211
1409
  }
1212
1410
  );
1213
1411
  }
1214
- var styles2 = import_react_native5.StyleSheet.create({
1215
- overlay: {
1412
+ var styles2 = import_react_native6.StyleSheet.create({
1413
+ overlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.3)" },
1414
+ animatedContainer: { flex: 1, backgroundColor: "#fff" },
1415
+ container: { flex: 1, backgroundColor: "#fff" },
1416
+ errorContainer: { flex: 1, justifyContent: "center", alignItems: "center", padding: 20 },
1417
+ errorText: { fontSize: 16, color: "#ef4444", textAlign: "center" },
1418
+ fallbackContainer: {
1216
1419
  flex: 1,
1217
- backgroundColor: "rgba(0,0,0,0.3)"
1420
+ justifyContent: "center",
1421
+ alignItems: "center",
1422
+ backgroundColor: "#111010",
1423
+ padding: 24
1218
1424
  },
1219
- animatedContainer: {
1220
- flex: 1,
1221
- backgroundColor: "#fff",
1222
- borderTopLeftRadius: 0,
1223
- borderTopRightRadius: 0
1425
+ fallbackCard: {
1426
+ width: "100%",
1427
+ maxWidth: 360,
1428
+ alignItems: "center",
1429
+ backgroundColor: "rgba(255,255,255,0.07)",
1430
+ borderRadius: 16,
1431
+ padding: 28
1224
1432
  },
1225
- container: {
1226
- flex: 1,
1227
- backgroundColor: "#fff"
1433
+ fallbackTitle: {
1434
+ fontSize: 18,
1435
+ fontWeight: "600",
1436
+ color: "#f5f5f5",
1437
+ marginBottom: 10,
1438
+ textAlign: "center"
1228
1439
  },
1229
- errorContainer: {
1230
- flex: 1,
1231
- justifyContent: "center",
1440
+ fallbackDescription: {
1441
+ fontSize: 14,
1442
+ color: "rgba(255,255,255,0.55)",
1443
+ textAlign: "center",
1444
+ marginBottom: 28,
1445
+ lineHeight: 20
1446
+ },
1447
+ retryButton: {
1448
+ width: "100%",
1449
+ backgroundColor: "#ffffff",
1450
+ borderRadius: 10,
1451
+ paddingVertical: 14,
1232
1452
  alignItems: "center",
1233
- padding: 20
1453
+ marginBottom: 12
1234
1454
  },
1235
- errorText: {
1236
- fontSize: 16,
1237
- color: "#ef4444",
1238
- textAlign: "center"
1239
- }
1455
+ retryButtonText: { fontSize: 15, fontWeight: "600", color: "#111010" },
1456
+ closeButton: {
1457
+ width: "100%",
1458
+ borderRadius: 10,
1459
+ paddingVertical: 14,
1460
+ alignItems: "center",
1461
+ backgroundColor: "rgba(255,255,255,0.1)"
1462
+ },
1463
+ closeButtonText: { fontSize: 15, fontWeight: "500", color: "rgba(255,255,255,0.7)" }
1240
1464
  });
1241
1465
 
1242
1466
  // src/domains/paywall/PaywallErrorBoundary.tsx
@@ -1249,8 +1473,7 @@ var PaywallErrorBoundary = class extends import_react6.Component {
1249
1473
  static getDerivedStateFromError() {
1250
1474
  return { hasError: true };
1251
1475
  }
1252
- componentDidCatch(error) {
1253
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Paywall render error:", error.message);
1476
+ componentDidCatch(_error) {
1254
1477
  }
1255
1478
  render() {
1256
1479
  return this.state.hasError ? null : this.props.children;
@@ -1273,7 +1496,7 @@ function usePaywallContext() {
1273
1496
 
1274
1497
  // src/domains/paywall/usePaywallPresenter.ts
1275
1498
  var import_react8 = require("react");
1276
- function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView) {
1499
+ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
1277
1500
  const [showingPreloadedWebView, setShowingPreloadedWebView] = (0, import_react8.useState)(false);
1278
1501
  const resolverRef = (0, import_react8.useRef)(null);
1279
1502
  const presentedAtRef = (0, import_react8.useRef)(Date.now());
@@ -1322,9 +1545,8 @@ function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreload
1322
1545
  };
1323
1546
  void track();
1324
1547
  setShowingPreloadedWebView(false);
1325
- clearPreloadedWebView();
1326
1548
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: true, restored: false });
1327
- }, [preloadedWebView, clearPreloadedWebView, resolvePreloadedPaywall]);
1549
+ }, [preloadedWebView, resolvePreloadedPaywall]);
1328
1550
  return { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef };
1329
1551
  }
1330
1552
 
@@ -1531,10 +1753,7 @@ var AffiliateManager = class {
1531
1753
  );
1532
1754
  }
1533
1755
  }
1534
- log(...args) {
1535
- if (this.debug) {
1536
- console.warn("[AffiliateManager]", ...args);
1537
- }
1756
+ log(..._args) {
1538
1757
  }
1539
1758
  };
1540
1759
  var affiliateManager = new AffiliateManager();
@@ -1551,33 +1770,289 @@ var PaywalloAffiliate = {
1551
1770
  getWithdrawals: () => affiliateManager.getWithdrawals()
1552
1771
  };
1553
1772
 
1554
- // src/core/ApiClient.ts
1773
+ // src/domains/subscription/SubscriptionCache.ts
1774
+ var LEGACY_CACHE_KEY = "subscription_cache";
1775
+ var CACHE_KEY_PREFIX = "subscription_cache:";
1776
+ var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
1777
+ function keyFor(distinctId) {
1778
+ return `${CACHE_KEY_PREFIX}${distinctId}`;
1779
+ }
1780
+ var SubscriptionCache = class {
1781
+ constructor(ttl = DEFAULT_TTL) {
1782
+ this.memoryCache = /* @__PURE__ */ new Map();
1783
+ this.legacyCleanupDone = false;
1784
+ this.ttl = ttl;
1785
+ this.storage = new SecureStorage();
1786
+ }
1787
+ async cleanupLegacyCache() {
1788
+ if (this.legacyCleanupDone) return;
1789
+ this.legacyCleanupDone = true;
1790
+ try {
1791
+ await this.storage.remove(LEGACY_CACHE_KEY);
1792
+ } catch {
1793
+ }
1794
+ }
1795
+ async get(distinctId) {
1796
+ await this.cleanupLegacyCache();
1797
+ if (!distinctId) return null;
1798
+ const memEntry = this.memoryCache.get(distinctId);
1799
+ if (memEntry && !this.isExpired(memEntry)) {
1800
+ return memEntry;
1801
+ }
1802
+ try {
1803
+ const stored = await this.storage.get(keyFor(distinctId));
1804
+ if (!stored) {
1805
+ return null;
1806
+ }
1807
+ const parsed = JSON.parse(stored);
1808
+ if (typeof parsed !== "object" || parsed === null || !("data" in parsed) || typeof parsed.data !== "object" || parsed.data === null || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") {
1809
+ await this.storage.remove(keyFor(distinctId));
1810
+ return null;
1811
+ }
1812
+ const cached = parsed;
1813
+ if (cached.data.subscription?.expiresAt) {
1814
+ cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
1815
+ }
1816
+ if (cached.data.subscription?.originalPurchaseDate) {
1817
+ cached.data.subscription.originalPurchaseDate = new Date(
1818
+ cached.data.subscription.originalPurchaseDate
1819
+ );
1820
+ }
1821
+ if (cached.data.subscription?.latestPurchaseDate) {
1822
+ cached.data.subscription.latestPurchaseDate = new Date(
1823
+ cached.data.subscription.latestPurchaseDate
1824
+ );
1825
+ }
1826
+ if (cached.data.subscription?.cancellationDate) {
1827
+ cached.data.subscription.cancellationDate = new Date(
1828
+ cached.data.subscription.cancellationDate
1829
+ );
1830
+ }
1831
+ if (cached.data.subscription?.gracePeriodExpiresAt) {
1832
+ cached.data.subscription.gracePeriodExpiresAt = new Date(
1833
+ cached.data.subscription.gracePeriodExpiresAt
1834
+ );
1835
+ }
1836
+ cached.isStale = this.isExpired(cached);
1837
+ if (!cached.isStale) {
1838
+ this.memoryCache.set(distinctId, cached);
1839
+ }
1840
+ return cached;
1841
+ } catch {
1842
+ return null;
1843
+ }
1844
+ }
1845
+ async set(distinctId, data) {
1846
+ if (!distinctId) return;
1847
+ const cached = {
1848
+ data,
1849
+ cachedAt: Date.now(),
1850
+ isStale: false
1851
+ };
1852
+ this.memoryCache.set(distinctId, cached);
1853
+ try {
1854
+ await this.storage.set(keyFor(distinctId), JSON.stringify(cached));
1855
+ } catch {
1856
+ }
1857
+ }
1858
+ async invalidate(distinctId) {
1859
+ if (!distinctId) return;
1860
+ this.memoryCache.delete(distinctId);
1861
+ try {
1862
+ await this.storage.remove(keyFor(distinctId));
1863
+ } catch {
1864
+ }
1865
+ }
1866
+ async invalidateAll() {
1867
+ this.memoryCache.clear();
1868
+ try {
1869
+ await this.storage.remove(LEGACY_CACHE_KEY);
1870
+ } catch {
1871
+ }
1872
+ }
1873
+ isExpired(cached) {
1874
+ return Date.now() - cached.cachedAt > this.ttl;
1875
+ }
1876
+ setTTL(ttl) {
1877
+ this.ttl = ttl;
1878
+ }
1879
+ };
1880
+ var subscriptionCache = new SubscriptionCache();
1881
+
1882
+ // src/domains/subscription/SubscriptionManager.ts
1555
1883
  var import_react_native7 = require("react-native");
1556
1884
 
1557
- // src/core/apiUrlUtils.ts
1558
- var DEFAULT_WEB_URL = "https://paywallo.com.br";
1559
- function deriveWebUrl(baseUrl) {
1560
- try {
1561
- const url = new URL(baseUrl);
1562
- if (url.hostname.startsWith("192.168") || url.hostname === "localhost") {
1563
- url.port = "3000";
1564
- return url.toString().replace(/\/$/, "");
1885
+ // src/domains/session/SessionError.ts
1886
+ var SessionError = class extends PaywalloError {
1887
+ constructor(code, message) {
1888
+ super("session", code, message);
1889
+ this.name = "SessionError";
1890
+ }
1891
+ };
1892
+ var SESSION_ERROR_CODES = {
1893
+ NOT_INITIALIZED: "SESSION_NOT_INITIALIZED",
1894
+ START_FAILED: "SESSION_START_FAILED",
1895
+ END_FAILED: "SESSION_END_FAILED",
1896
+ RESTORE_FAILED: "SESSION_RESTORE_FAILED"
1897
+ };
1898
+
1899
+ // src/domains/subscription/SubscriptionManager.ts
1900
+ var SubscriptionManagerClass = class {
1901
+ constructor() {
1902
+ this.config = null;
1903
+ this.listeners = /* @__PURE__ */ new Set();
1904
+ this.userId = null;
1905
+ this.cache = new SubscriptionCache();
1906
+ }
1907
+ cacheKey() {
1908
+ return this.userId ?? "__anonymous__";
1909
+ }
1910
+ init(config) {
1911
+ this.config = config;
1912
+ if (config.cacheTTL) {
1913
+ this.cache.setTTL(config.cacheTTL);
1565
1914
  }
1566
- if (url.hostname.startsWith("api.")) {
1567
- url.hostname = url.hostname.replace("api.", "app.");
1568
- return url.toString().replace(/\/$/, "");
1915
+ }
1916
+ setUserId(userId) {
1917
+ if (this.userId !== userId) {
1918
+ this.userId = userId;
1919
+ void this.cache.invalidateAll();
1569
1920
  }
1570
- return DEFAULT_WEB_URL;
1571
- } catch {
1572
- return DEFAULT_WEB_URL;
1573
1921
  }
1574
- }
1575
- function buildConditionalFlagUrl(baseUrl, flagKey, context) {
1576
- const url = new URL(`${baseUrl}/api/v1/conditional-flags/${flagKey}`);
1577
- if (context.platform) url.searchParams.set("platform", context.platform);
1578
- if (context.appVersion) url.searchParams.set("appVersion", context.appVersion);
1579
- if (context.country) url.searchParams.set("country", context.country);
1580
- if (context.distinctId) url.searchParams.set("distinctId", context.distinctId);
1922
+ async hasActiveSubscription(forceRefresh = false) {
1923
+ const status = await this.getSubscriptionStatus(forceRefresh);
1924
+ return status.hasActiveSubscription;
1925
+ }
1926
+ async getSubscription(forceRefresh = false) {
1927
+ const status = await this.getSubscriptionStatus(forceRefresh);
1928
+ return status.subscription;
1929
+ }
1930
+ async getEntitlements(forceRefresh = false) {
1931
+ const status = await this.getSubscriptionStatus(forceRefresh);
1932
+ return status.entitlements;
1933
+ }
1934
+ async getSubscriptionStatus(forceRefresh = false) {
1935
+ if (!this.config) {
1936
+ return this.getEmptyStatus();
1937
+ }
1938
+ if (!forceRefresh) {
1939
+ const cached = await this.cache.get(this.cacheKey());
1940
+ if (cached && !cached.isStale) {
1941
+ return cached.data;
1942
+ }
1943
+ }
1944
+ try {
1945
+ const status = await this.fetchSubscriptionStatus();
1946
+ await this.cache.set(this.cacheKey(), status);
1947
+ this.notifyListeners(status);
1948
+ return status;
1949
+ } catch (error) {
1950
+ this.log("Failed to fetch subscription status", error);
1951
+ const cached = await this.cache.get(this.cacheKey());
1952
+ if (cached) {
1953
+ return cached.data;
1954
+ }
1955
+ return this.getEmptyStatus();
1956
+ }
1957
+ }
1958
+ async restorePurchases() {
1959
+ if (!this.config) {
1960
+ throw new SessionError(
1961
+ SESSION_ERROR_CODES.NOT_INITIALIZED,
1962
+ "SubscriptionManager not initialized"
1963
+ );
1964
+ }
1965
+ await this.cache.invalidate(this.cacheKey());
1966
+ return this.getSubscriptionStatus(true);
1967
+ }
1968
+ async fetchSubscriptionStatus() {
1969
+ if (!this.config) {
1970
+ return this.getEmptyStatus();
1971
+ }
1972
+ const url = new URL(`${this.config.serverUrl}/subscriptions/status`);
1973
+ if (this.userId) {
1974
+ url.searchParams.set("userId", this.userId);
1975
+ }
1976
+ url.searchParams.set("platform", import_react_native7.Platform.OS);
1977
+ const response = await fetch(url.toString(), {
1978
+ method: "GET",
1979
+ headers: {
1980
+ "Content-Type": "application/json",
1981
+ "X-App-Key": this.config.appKey
1982
+ }
1983
+ });
1984
+ if (!response.ok) {
1985
+ throw new SessionError(
1986
+ SESSION_ERROR_CODES.RESTORE_FAILED,
1987
+ `Failed to fetch subscription status: ${response.status}`
1988
+ );
1989
+ }
1990
+ const data = await response.json();
1991
+ if (data.subscription) {
1992
+ data.subscription.expiresAt = new Date(data.subscription.expiresAt);
1993
+ data.subscription.originalPurchaseDate = new Date(data.subscription.originalPurchaseDate);
1994
+ data.subscription.latestPurchaseDate = new Date(data.subscription.latestPurchaseDate);
1995
+ if (data.subscription.cancellationDate) {
1996
+ data.subscription.cancellationDate = new Date(data.subscription.cancellationDate);
1997
+ }
1998
+ if (data.subscription.gracePeriodExpiresAt) {
1999
+ data.subscription.gracePeriodExpiresAt = new Date(data.subscription.gracePeriodExpiresAt);
2000
+ }
2001
+ }
2002
+ return data;
2003
+ }
2004
+ getEmptyStatus() {
2005
+ return {
2006
+ hasActiveSubscription: false,
2007
+ subscription: null,
2008
+ entitlements: []
2009
+ };
2010
+ }
2011
+ addListener(listener) {
2012
+ this.listeners.add(listener);
2013
+ return () => this.listeners.delete(listener);
2014
+ }
2015
+ notifyListeners(status) {
2016
+ for (const listener of this.listeners) {
2017
+ listener(status);
2018
+ }
2019
+ }
2020
+ async onPurchaseComplete() {
2021
+ await this.cache.invalidate(this.cacheKey());
2022
+ await this.getSubscriptionStatus(true);
2023
+ }
2024
+ log(..._args) {
2025
+ }
2026
+ };
2027
+ var subscriptionManager = new SubscriptionManagerClass();
2028
+
2029
+ // src/core/ApiClient.ts
2030
+ var import_react_native9 = require("react-native");
2031
+
2032
+ // src/core/apiUrlUtils.ts
2033
+ var DEFAULT_WEB_URL = "https://paywallo.com.br";
2034
+ function deriveWebUrl(baseUrl) {
2035
+ try {
2036
+ const url = new URL(baseUrl);
2037
+ if (url.hostname.startsWith("192.168") || url.hostname === "localhost") {
2038
+ url.port = "3000";
2039
+ return url.toString().replace(/\/$/, "");
2040
+ }
2041
+ if (url.hostname.startsWith("api.")) {
2042
+ url.hostname = url.hostname.replace("api.", "app.");
2043
+ return url.toString().replace(/\/$/, "");
2044
+ }
2045
+ return DEFAULT_WEB_URL;
2046
+ } catch {
2047
+ return DEFAULT_WEB_URL;
2048
+ }
2049
+ }
2050
+ function buildConditionalFlagUrl(baseUrl, flagKey, context) {
2051
+ const url = new URL(`${baseUrl}/api/v1/conditional-flags/${flagKey}`);
2052
+ if (context.platform) url.searchParams.set("platform", context.platform);
2053
+ if (context.appVersion) url.searchParams.set("appVersion", context.appVersion);
2054
+ if (context.country) url.searchParams.set("country", context.country);
2055
+ if (context.distinctId) url.searchParams.set("distinctId", context.distinctId);
1581
2056
  return url.toString();
1582
2057
  }
1583
2058
 
@@ -1784,10 +2259,7 @@ var HttpClient = class {
1784
2259
  sleep(ms) {
1785
2260
  return new Promise((resolve) => setTimeout(resolve, ms));
1786
2261
  }
1787
- log(message, data) {
1788
- if (this.config.debug) {
1789
- console.warn("[HttpClient]", message, data ?? "");
1790
- }
2262
+ log(..._args) {
1791
2263
  }
1792
2264
  setDebug(debug) {
1793
2265
  this.config.debug = debug;
@@ -1798,7 +2270,7 @@ var HttpClient = class {
1798
2270
  };
1799
2271
 
1800
2272
  // src/core/network/NetworkMonitor.ts
1801
- var import_react_native6 = require("react-native");
2273
+ var import_react_native8 = require("react-native");
1802
2274
 
1803
2275
  // src/core/network/types.ts
1804
2276
  var DEFAULT_NETWORK_CONFIG = {
@@ -1844,7 +2316,7 @@ var NetworkMonitorClass = class {
1844
2316
  }, this.config.checkInterval);
1845
2317
  }
1846
2318
  setupAppStateListener() {
1847
- this.appStateSubscription = import_react_native6.AppState.addEventListener("change", (state) => {
2319
+ this.appStateSubscription = import_react_native8.AppState.addEventListener("change", (state) => {
1848
2320
  if (state === "active") {
1849
2321
  void this.checkConnectivity();
1850
2322
  }
@@ -1931,10 +2403,7 @@ var NetworkMonitorClass = class {
1931
2403
  this.initialized = false;
1932
2404
  this.currentState = "unknown";
1933
2405
  }
1934
- log(message, data) {
1935
- if (this.config.debug) {
1936
- console.warn("[NetworkMonitor]", message, data ?? "");
1937
- }
2406
+ log(..._args) {
1938
2407
  }
1939
2408
  setDebug(debug) {
1940
2409
  this.config.debug = debug;
@@ -1964,6 +2433,8 @@ function normalizeForComparison(obj) {
1964
2433
  return "{" + pairs.join(",") + "}";
1965
2434
  }
1966
2435
  function findDuplicateIndex(queue, item) {
2436
+ const byId = queue.findIndex((existing) => existing.id === item.id);
2437
+ if (byId !== -1) return byId;
1967
2438
  const normalizedBody = normalizeForComparison(item.body);
1968
2439
  return queue.findIndex(
1969
2440
  (existing) => existing.url === item.url && normalizeForComparison(existing.body) === normalizedBody
@@ -2032,9 +2503,9 @@ var OfflineQueueClass = class {
2032
2503
  void this.saveToStorage();
2033
2504
  }
2034
2505
  }
2035
- async enqueue(method, url, body, headers) {
2506
+ async enqueue(method, url, body, headers, id) {
2036
2507
  const item = {
2037
- id: generateUUID(),
2508
+ id: id ?? generateUUID(),
2038
2509
  method,
2039
2510
  url,
2040
2511
  body,
@@ -2118,7 +2589,6 @@ var OfflineQueueClass = class {
2118
2589
  this.emit({ type: "processing:end", queueSize: this.queue.length });
2119
2590
  return { processed, failed };
2120
2591
  }
2121
- /** Returns true if the item was permanently removed (counted as failed). */
2122
2592
  async handleItemFailure(item, error, permanent = false) {
2123
2593
  if (permanent || item.attempts >= item.maxAttempts) {
2124
2594
  await this.removeItem(item.id);
@@ -2180,10 +2650,7 @@ var OfflineQueueClass = class {
2180
2650
  }
2181
2651
  }
2182
2652
  }
2183
- log(message, data) {
2184
- if (this.config.debug) {
2185
- console.warn("[OfflineQueue]", message, data ?? "");
2186
- }
2653
+ log(..._args) {
2187
2654
  }
2188
2655
  setDebug(debug) {
2189
2656
  this.config.debug = debug;
@@ -2325,10 +2792,7 @@ var QueueProcessorClass = class {
2325
2792
  this.httpClient = null;
2326
2793
  this.initialized = false;
2327
2794
  }
2328
- log(message, data) {
2329
- if (this.config.debug) {
2330
- console.warn("[QueueProcessor]", message, data ?? "");
2331
- }
2795
+ log(..._args) {
2332
2796
  }
2333
2797
  setDebug(debug) {
2334
2798
  this.config.debug = debug;
@@ -2340,10 +2804,9 @@ var queueProcessor = new QueueProcessorClass();
2340
2804
  var ApiCache = class {
2341
2805
  constructor() {
2342
2806
  this.CACHE_TTL = 3e5;
2343
- // 5 minutes
2344
2807
  this.CACHE_NULL_TTL = 3e4;
2345
- // 30 seconds — for null/404 variants
2346
2808
  this.MAX_SIZE = 200;
2809
+ this.STALE_WINDOW_MS = 864e5;
2347
2810
  this.variantCache = /* @__PURE__ */ new Map();
2348
2811
  this.paywallCache = /* @__PURE__ */ new Map();
2349
2812
  this.campaignCache = /* @__PURE__ */ new Map();
@@ -2381,7 +2844,7 @@ var ApiCache = class {
2381
2844
  }
2382
2845
  getStaleVariant(key) {
2383
2846
  const entry = this.variantCache.get(key);
2384
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2847
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2385
2848
  return void 0;
2386
2849
  }
2387
2850
  getPaywall(key) {
@@ -2393,19 +2856,19 @@ var ApiCache = class {
2393
2856
  }
2394
2857
  getStalePaywall(key) {
2395
2858
  const entry = this.paywallCache.get(key);
2396
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2859
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2397
2860
  return void 0;
2398
2861
  }
2399
2862
  getCampaign(key) {
2400
2863
  return this.getCached(this.campaignCache, key);
2401
2864
  }
2402
- setCampaign(key, value) {
2403
- this.setCached(this.campaignCache, key, value);
2865
+ setCampaign(key, value, ttlOverride) {
2866
+ this.setCached(this.campaignCache, key, value, ttlOverride);
2404
2867
  this.evictExpiredEntries(this.campaignCache);
2405
2868
  }
2406
2869
  getStaleCampaign(key) {
2407
2870
  const entry = this.campaignCache.get(key);
2408
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2871
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2409
2872
  return void 0;
2410
2873
  }
2411
2874
  };
@@ -2413,7 +2876,7 @@ var ApiCache = class {
2413
2876
  // src/core/ApiClient.ts
2414
2877
  var SDK_VERSION = "1.0.0";
2415
2878
  var DEFAULT_BASE_URL = "https://panel.lucasqueiroga.shop";
2416
- var ApiClient = class {
2879
+ var _ApiClient = class _ApiClient {
2417
2880
  constructor(appKey, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
2418
2881
  this.cache = new ApiCache();
2419
2882
  this.appKey = appKey;
@@ -2424,11 +2887,12 @@ var ApiClient = class {
2424
2887
  this.offlineQueueEnabled = offlineQueueEnabled;
2425
2888
  this.httpClient = new HttpClient(this.baseUrl, { timeout, debug });
2426
2889
  }
2427
- // ─── Infrastructure ──────────────────────────────────────────────────────────
2428
- /** Exposed only for QueueProcessor initialization. Prefer get()/post() for requests. */
2429
2890
  getHttpClient() {
2430
2891
  return this.httpClient;
2431
2892
  }
2893
+ getBaseUrl() {
2894
+ return this.baseUrl;
2895
+ }
2432
2896
  getWebUrl() {
2433
2897
  return this.webUrl;
2434
2898
  }
@@ -2454,7 +2918,6 @@ var ApiClient = class {
2454
2918
  async clearQueue() {
2455
2919
  await offlineQueue.clear();
2456
2920
  }
2457
- // ─── Public HTTP facade (headers injected automatically) ─────────────────────
2458
2921
  async get(path) {
2459
2922
  return this.httpClient.get(path, { headers: { "X-App-Key": this.appKey } });
2460
2923
  }
@@ -2468,29 +2931,28 @@ var ApiClient = class {
2468
2931
  message,
2469
2932
  context,
2470
2933
  sdkVersion: SDK_VERSION,
2471
- platform: import_react_native7.Platform.OS
2934
+ platform: import_react_native9.Platform.OS
2472
2935
  }, true);
2473
2936
  } catch {
2474
2937
  }
2475
2938
  }
2476
- // ─── Business methods ────────────────────────────────────────────────────────
2477
2939
  async trackEvent(eventName, distinctId, properties, timestamp) {
2478
2940
  await this.postWithQueue("/api/v1/events", {
2479
2941
  eventName,
2480
2942
  distinctId,
2481
- properties: { ...properties, platform: import_react_native7.Platform.OS },
2943
+ properties: { ...properties, platform: import_react_native9.Platform.OS },
2482
2944
  timestamp: timestamp ?? Date.now(),
2483
2945
  environment: this.environment.toLowerCase()
2484
2946
  }, `event:${eventName}`);
2485
2947
  }
2486
- async identify(distinctId, properties, email) {
2487
- await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: import_react_native7.Platform.OS }, "identify");
2948
+ async identify(distinctId, properties, email, deviceId) {
2949
+ await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: import_react_native9.Platform.OS, ...deviceId && { deviceId } }, "identify");
2488
2950
  }
2489
2951
  async startSession(distinctId, sessionId, platform) {
2490
2952
  await this.postWithQueue("/api/v1/sessions/start", {
2491
2953
  distinctId,
2492
2954
  sessionId,
2493
- platform: platform ?? import_react_native7.Platform.OS,
2955
+ platform: platform ?? import_react_native9.Platform.OS,
2494
2956
  environment: this.environment.toLowerCase()
2495
2957
  }, `session.start:${sessionId}`);
2496
2958
  return { success: true };
@@ -2503,6 +2965,11 @@ var ApiClient = class {
2503
2965
  const key = `${flagKey}:${distinctId}`;
2504
2966
  const hit = this.cache.getVariant(key);
2505
2967
  if (hit !== void 0) return hit;
2968
+ const persisted = await this.readFlagFromStorage(flagKey, distinctId);
2969
+ if (persisted !== null) {
2970
+ this.cache.setVariant(key, persisted);
2971
+ return persisted;
2972
+ }
2506
2973
  try {
2507
2974
  const res = await this.get(`/api/v1/flags/${flagKey}?distinctId=${encodeURIComponent(distinctId)}`);
2508
2975
  if (res.status === 404) {
@@ -2517,6 +2984,7 @@ var ApiClient = class {
2517
2984
  return { variant: null };
2518
2985
  }
2519
2986
  this.cache.setVariant(key, res.data);
2987
+ await this.writeFlagToStorage(flagKey, distinctId, res.data);
2520
2988
  return res.data;
2521
2989
  } catch (error) {
2522
2990
  this.log("Failed to get variant:", flagKey, error);
@@ -2562,7 +3030,7 @@ var ApiClient = class {
2562
3030
  const result = await getCampaignData(this, this.baseUrl, placement, distinctId, context);
2563
3031
  if (!result) {
2564
3032
  this.log("No campaign found for placement:", placement);
2565
- this.cache.setCampaign(key, null);
3033
+ this.cache.setCampaign(key, null, this.cache.nullTTL);
2566
3034
  return null;
2567
3035
  }
2568
3036
  this.log("Got campaign:", placement, result.variantKey);
@@ -2575,6 +3043,32 @@ var ApiClient = class {
2575
3043
  return null;
2576
3044
  }
2577
3045
  }
3046
+ async getPrimaryCampaign(distinctId) {
3047
+ const key = `primary:${distinctId}`;
3048
+ const hit = this.cache.getCampaign(key);
3049
+ if (hit !== void 0) return hit;
3050
+ try {
3051
+ const res = await this.get(`/campaigns/public/primary?distinctId=${encodeURIComponent(distinctId)}`);
3052
+ if (res.status === 404) {
3053
+ this.cache.setCampaign(key, null, this.cache.nullTTL);
3054
+ return null;
3055
+ }
3056
+ if (!res.ok) {
3057
+ this.log("Non-OK response for primary campaign:", res.status);
3058
+ const stale = this.cache.getStaleCampaign(key);
3059
+ if (stale !== void 0) return stale;
3060
+ return null;
3061
+ }
3062
+ this.log("Got primary campaign:", res.data?.campaignId);
3063
+ this.cache.setCampaign(key, res.data);
3064
+ return res.data;
3065
+ } catch (error) {
3066
+ this.log("Failed to get primary campaign:", error);
3067
+ const stale = this.cache.getStaleCampaign(key);
3068
+ if (stale !== void 0) return stale;
3069
+ return null;
3070
+ }
3071
+ }
2578
3072
  async validatePurchase(platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
2579
3073
  const result = await validatePurchase(
2580
3074
  this,
@@ -2598,6 +3092,35 @@ var ApiClient = class {
2598
3092
  this.log("Subscription status:", result.hasActiveSubscription);
2599
3093
  return result;
2600
3094
  }
3095
+ async evaluateFlags(keys, distinctId) {
3096
+ const query = keys.map(encodeURIComponent).join(",");
3097
+ const path = `/api/v1/flags/evaluate?keys=${query}`;
3098
+ try {
3099
+ const res = await this.httpClient.get(path, {
3100
+ headers: { "X-App-Key": this.appKey, "x-distinct-id": distinctId }
3101
+ });
3102
+ if (!res.ok) {
3103
+ this.log("Non-OK response for evaluateFlags:", res.status);
3104
+ return {};
3105
+ }
3106
+ const raw = res.data;
3107
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
3108
+ const record = raw;
3109
+ const result = {};
3110
+ for (const key of Object.keys(record)) {
3111
+ const value = record[key];
3112
+ const variant = typeof value === "boolean" ? String(value) : typeof value === "string" ? value : null;
3113
+ result[key] = variant;
3114
+ const flagVariant = { variant };
3115
+ this.cache.setVariant(`${key}:${distinctId}`, flagVariant);
3116
+ await this.writeFlagToStorage(key, distinctId, flagVariant);
3117
+ }
3118
+ return result;
3119
+ } catch (error) {
3120
+ this.log("Failed to evaluateFlags:", error);
3121
+ return {};
3122
+ }
3123
+ }
2601
3124
  async getConditionalFlag(flagKey, context) {
2602
3125
  try {
2603
3126
  const url = buildConditionalFlagUrl(this.baseUrl, flagKey, context);
@@ -2614,11 +3137,37 @@ var ApiClient = class {
2614
3137
  return { value: false, flagKey };
2615
3138
  }
2616
3139
  }
2617
- // ─── Private helpers ─────────────────────────────────────────────────────────
2618
- /**
2619
- * POST with offline queue fallback. Enqueues when offline or on network failure.
2620
- * Throws only when offlineQueueEnabled is false and the request fails.
2621
- */
3140
+ async getVariantFromCache(flagKey, distinctId) {
3141
+ const key = `${flagKey}:${distinctId}`;
3142
+ const hit = this.cache.getVariant(key);
3143
+ if (hit !== void 0) return hit;
3144
+ const persisted = await this.readFlagFromStorage(flagKey, distinctId);
3145
+ if (persisted !== null) {
3146
+ this.cache.setVariant(key, persisted);
3147
+ return persisted;
3148
+ }
3149
+ return null;
3150
+ }
3151
+ async readFlagFromStorage(flagKey, distinctId) {
3152
+ try {
3153
+ const raw = await secureStorage.get(`flag:${flagKey}:${distinctId}`);
3154
+ if (!raw) return null;
3155
+ const parsed = JSON.parse(raw);
3156
+ if (typeof parsed !== "object" || parsed === null || !("variant" in parsed) || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") return null;
3157
+ const { variant, payload, cachedAt } = parsed;
3158
+ if (Date.now() - cachedAt > _ApiClient.FLAG_MAX_AGE_MS) return null;
3159
+ return { variant, payload };
3160
+ } catch {
3161
+ return null;
3162
+ }
3163
+ }
3164
+ async writeFlagToStorage(flagKey, distinctId, flag) {
3165
+ try {
3166
+ const value = JSON.stringify({ variant: flag.variant, payload: flag.payload, cachedAt: Date.now() });
3167
+ await secureStorage.set(`flag:${flagKey}:${distinctId}`, value);
3168
+ } catch {
3169
+ }
3170
+ }
2622
3171
  async postWithQueue(url, payload, label) {
2623
3172
  if (this.offlineQueueEnabled && !networkMonitor.isOnline()) {
2624
3173
  await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
@@ -2637,10 +3186,11 @@ var ApiClient = class {
2637
3186
  throw error;
2638
3187
  }
2639
3188
  }
2640
- log(...args) {
2641
- if (this.debug) console.warn("[Paywallo API]", ...args);
3189
+ log(..._args) {
2642
3190
  }
2643
3191
  };
3192
+ _ApiClient.FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
3193
+ var ApiClient = _ApiClient;
2644
3194
 
2645
3195
  // src/domains/campaign/CampaignError.ts
2646
3196
  var CampaignError = class extends PaywalloError {
@@ -2659,39 +3209,72 @@ var CAMPAIGN_ERROR_CODES = {
2659
3209
 
2660
3210
  // src/domains/campaign/CampaignGateService.ts
2661
3211
  var PRELOAD_CACHE_TTL_MS = 5 * 60 * 1e3;
3212
+ var PRELOAD_WAIT_MAX_MS = 2e3;
3213
+ var PRELOAD_WAIT_INTERVAL_MS = 100;
2662
3214
  var CampaignGateService = class {
2663
3215
  constructor() {
2664
3216
  this.preloadedCampaigns = /* @__PURE__ */ new Map();
3217
+ this.pendingPreloads = /* @__PURE__ */ new Set();
2665
3218
  }
2666
3219
  async getCampaign(apiClient, placement, context) {
2667
3220
  const distinctId = identityManager.getDistinctId();
2668
3221
  if (!distinctId) return null;
2669
3222
  return apiClient.getCampaign(placement, distinctId, context);
2670
3223
  }
2671
- async presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context) {
2672
- const campaign = await this.getCampaign(apiClient, placement, context);
3224
+ async waitForPreload(placement) {
3225
+ if (!this.pendingPreloads.has(placement)) return;
3226
+ const deadline = Date.now() + PRELOAD_WAIT_MAX_MS;
3227
+ await new Promise((resolve) => {
3228
+ const check = () => {
3229
+ if (!this.pendingPreloads.has(placement) || Date.now() >= deadline) {
3230
+ resolve();
3231
+ return;
3232
+ }
3233
+ setTimeout(check, PRELOAD_WAIT_INTERVAL_MS);
3234
+ };
3235
+ check();
3236
+ });
3237
+ }
3238
+ async presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
3239
+ await this.waitForPreload(placement);
3240
+ const preloaded = this.getPreloadedCampaign(placement);
3241
+ const campaign = preloaded ?? await this.getCampaign(apiClient, placement, context);
2673
3242
  if (!campaign) {
2674
3243
  return { presented: false, purchased: false, cancelled: false, restored: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, "No campaign found") };
2675
3244
  }
3245
+ if (!context?.forceShow) {
3246
+ const isActive = await hasActive();
3247
+ if (isActive) {
3248
+ return { presented: false, purchased: false, cancelled: false, restored: false, skippedReason: "subscriber" };
3249
+ }
3250
+ }
2676
3251
  if (campaignPresenter) return campaignPresenter(campaign);
2677
3252
  if (!paywallPresenter) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
2678
- return { ...await paywallPresenter(campaign.paywall.placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
3253
+ return { ...await paywallPresenter(placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
2679
3254
  }
2680
3255
  async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2681
- if (await hasActive()) return true;
2682
- const r = await this.presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context);
3256
+ const isActive = await hasActive();
3257
+ if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo] requireSubscription \u2014 hasActive: ${isActive}`);
3258
+ if (isActive) return true;
3259
+ const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context);
2683
3260
  return r.purchased || r.restored;
2684
3261
  }
2685
3262
  async preloadCampaign(apiClient, placement, context) {
3263
+ this.pendingPreloads.add(placement);
2686
3264
  try {
2687
3265
  const campaign = await this.getCampaign(apiClient, placement, context);
2688
3266
  if (!campaign?.paywall) {
2689
3267
  return { success: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, `Campaign or paywall not found: ${placement}`) };
2690
3268
  }
2691
3269
  this.preloadedCampaigns.set(placement, { campaign, preloadedAt: Date.now() });
3270
+ if (PaywalloClient.getConfig()?.debug) {
3271
+ console.info(`[Paywallo PRELOAD] data cached \u2014 placement: ${placement}`);
3272
+ }
2692
3273
  return { success: true };
2693
3274
  } catch (error) {
2694
3275
  return { success: false, error: error instanceof Error ? error : new PaywalloError("panel", "PANEL_UNKNOWN", String(error)) };
3276
+ } finally {
3277
+ this.pendingPreloads.delete(placement);
2695
3278
  }
2696
3279
  }
2697
3280
  getPreloadedCampaign(placement) {
@@ -2705,12 +3288,13 @@ var CampaignGateService = class {
2705
3288
  }
2706
3289
  clearPreloaded() {
2707
3290
  this.preloadedCampaigns.clear();
3291
+ this.pendingPreloads.clear();
2708
3292
  }
2709
3293
  };
2710
3294
  var campaignGateService = new CampaignGateService();
2711
3295
 
2712
3296
  // src/domains/identity/AdvertisingIdManager.ts
2713
- var import_react_native8 = require("react-native");
3297
+ var import_react_native10 = require("react-native");
2714
3298
  var ZERO_IDFA = "00000000-0000-0000-0000-000000000000";
2715
3299
  var AdvertisingIdManager = class {
2716
3300
  constructor() {
@@ -2721,7 +3305,7 @@ var AdvertisingIdManager = class {
2721
3305
  const fallback = { idfv: null, idfa: null, gaid: null, attStatus: "unavailable" };
2722
3306
  try {
2723
3307
  const tracking = await import("expo-tracking-transparency");
2724
- if (import_react_native8.Platform.OS === "ios") {
3308
+ if (import_react_native10.Platform.OS === "ios") {
2725
3309
  if (!tracking.isAvailable()) {
2726
3310
  const idfv2 = await this.collectIdfv();
2727
3311
  this.cached = { ...fallback, idfv: idfv2 };
@@ -2740,7 +3324,7 @@ var AdvertisingIdManager = class {
2740
3324
  this.cached = { idfv, idfa, gaid: null, attStatus: status };
2741
3325
  return this.cached;
2742
3326
  }
2743
- if (import_react_native8.Platform.OS === "android") {
3327
+ if (import_react_native10.Platform.OS === "android") {
2744
3328
  const androidStatus = await tracking.getTrackingPermissionsAsync();
2745
3329
  const optedIn = androidStatus.granted;
2746
3330
  const rawGaid = optedIn ? tracking.getAdvertisingId() : null;
@@ -2799,14 +3383,14 @@ var FacebookIdManager = class {
2799
3383
  var facebookIdManager = new FacebookIdManager();
2800
3384
 
2801
3385
  // src/domains/identity/InstallTracker.ts
2802
- var import_react_native11 = require("react-native");
3386
+ var import_react_native13 = require("react-native");
2803
3387
 
2804
3388
  // src/domains/identity/InstallReferrerManager.ts
2805
- var import_react_native9 = require("react-native");
3389
+ var import_react_native11 = require("react-native");
2806
3390
  var REFERRER_TIMEOUT_MS = 5e3;
2807
3391
  var InstallReferrerManager = class {
2808
3392
  async getReferrer() {
2809
- if (import_react_native9.Platform.OS !== "android") {
3393
+ if (import_react_native11.Platform.OS !== "android") {
2810
3394
  return this.emptyResult();
2811
3395
  }
2812
3396
  let timerId;
@@ -2866,21 +3450,7 @@ var InstallReferrerManager = class {
2866
3450
  var installReferrerManager = new InstallReferrerManager();
2867
3451
 
2868
3452
  // src/domains/session/SessionManager.ts
2869
- var import_react_native10 = require("react-native");
2870
-
2871
- // src/domains/session/SessionError.ts
2872
- var SessionError = class extends PaywalloError {
2873
- constructor(code, message) {
2874
- super("session", code, message);
2875
- this.name = "SessionError";
2876
- }
2877
- };
2878
- var SESSION_ERROR_CODES = {
2879
- NOT_INITIALIZED: "SESSION_NOT_INITIALIZED",
2880
- START_FAILED: "SESSION_START_FAILED",
2881
- END_FAILED: "SESSION_END_FAILED",
2882
- RESTORE_FAILED: "SESSION_RESTORE_FAILED"
2883
- };
3453
+ var import_react_native12 = require("react-native");
2884
3454
 
2885
3455
  // src/domains/session/sessionTracking.ts
2886
3456
  async function trackSessionStart(apiClient, sessionId, sessionStartedAt) {
@@ -3048,7 +3618,7 @@ var SessionManager = class {
3048
3618
  if (this.appStateSubscription) {
3049
3619
  this.appStateSubscription.remove();
3050
3620
  }
3051
- this.appStateSubscription = import_react_native10.AppState.addEventListener("change", this.handleAppStateChange);
3621
+ this.appStateSubscription = import_react_native12.AppState.addEventListener("change", this.handleAppStateChange);
3052
3622
  }
3053
3623
  async handleForeground() {
3054
3624
  if (this.backgroundTimestamp) {
@@ -3109,10 +3679,7 @@ var SessionManager = class {
3109
3679
  );
3110
3680
  }
3111
3681
  }
3112
- log(...args) {
3113
- if (this.debug) {
3114
- console.warn("[Paywallo Session]", ...args);
3115
- }
3682
+ log(..._args) {
3116
3683
  }
3117
3684
  };
3118
3685
  var sessionManager = new SessionManager();
@@ -3147,7 +3714,7 @@ var InstallTracker = class {
3147
3714
  }
3148
3715
  let deviceData = {};
3149
3716
  try {
3150
- const screen = import_react_native11.Dimensions.get("screen");
3717
+ const screen = import_react_native13.Dimensions.get("screen");
3151
3718
  const screenWidth = Math.round(screen.width ?? 0);
3152
3719
  const screenHeight = Math.round(screen.height ?? 0);
3153
3720
  const locale = Intl.DateTimeFormat().resolvedOptions().locale ?? "unknown";
@@ -3167,9 +3734,6 @@ var InstallTracker = class {
3167
3734
  locale
3168
3735
  };
3169
3736
  } catch {
3170
- if (this.debug) {
3171
- console.warn("[Paywallo] Failed to collect device data for install event");
3172
- }
3173
3737
  }
3174
3738
  const [fbAnonId, referrer, adIds] = await Promise.all([
3175
3739
  facebookIdManager.getAnonId(),
@@ -3179,7 +3743,7 @@ var InstallTracker = class {
3179
3743
  try {
3180
3744
  await apiClient.trackEvent("$app_installed", distinctId, {
3181
3745
  installedAt,
3182
- platform: import_react_native11.Platform.OS,
3746
+ platform: import_react_native13.Platform.OS,
3183
3747
  ...sessionId && { sessionId },
3184
3748
  ...deviceData,
3185
3749
  ...fbAnonId && { fbAnonId },
@@ -3200,13 +3764,7 @@ var InstallTracker = class {
3200
3764
  await AsyncStorage3.setItem("@paywallo:install_tracked", String(installedAt));
3201
3765
  } catch {
3202
3766
  }
3203
- if (this.debug && !stored) {
3204
- console.warn("[Paywallo] Install event tracked but flag storage failed \u2014 may re-track on next launch");
3205
- }
3206
- } catch (error) {
3207
- if (this.debug) {
3208
- console.warn("[Paywallo] Failed to track install:", error);
3209
- }
3767
+ } catch {
3210
3768
  }
3211
3769
  }
3212
3770
  };
@@ -3221,6 +3779,8 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3221
3779
  this.config = null;
3222
3780
  this.apiClient = null;
3223
3781
  this.initPromise = null;
3782
+ this.readyPromise = null;
3783
+ this.resolveReady = null;
3224
3784
  this.paywallPresenter = null;
3225
3785
  this.campaignPresenter = null;
3226
3786
  this.subscriptionGetter = null;
@@ -3230,29 +3790,30 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3230
3790
  this.pendingConfig = null;
3231
3791
  this.networkCleanup = null;
3232
3792
  this.initAttempts = 0;
3793
+ this.autoPreloadedPlacement = null;
3794
+ this.autoPreloadPlacementPromise = null;
3795
+ this.sessionFlagsMap = /* @__PURE__ */ new Map();
3233
3796
  }
3234
3797
  async init(config) {
3235
- console.warn("[Paywallo INIT]", "init() called \u2014 appKey:", config.appKey ? config.appKey.substring(0, 8) + "..." : "MISSING");
3798
+ if (!this.readyPromise) {
3799
+ this.readyPromise = new Promise((resolve) => {
3800
+ this.resolveReady = resolve;
3801
+ });
3802
+ }
3236
3803
  if (this.config && this.apiClient) {
3237
- console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3238
3804
  return;
3239
3805
  }
3240
3806
  if (this.initPromise) {
3241
- console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3242
3807
  await this.initPromise;
3243
- console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3244
3808
  return;
3245
3809
  }
3246
3810
  if (!config.appKey) throw new ClientError(CLIENT_ERROR_CODES.MISSING_APP_KEY, "appKey is required");
3247
- console.warn("[Paywallo INIT]", "Starting fresh init...");
3248
3811
  this.pendingConfig = config;
3249
3812
  this.initAttempts = 0;
3250
3813
  this.initPromise = this.initWithRetry(config);
3251
3814
  try {
3252
3815
  await this.initPromise;
3253
- console.warn("[Paywallo INIT]", "init() SUCCESS");
3254
3816
  } catch (error) {
3255
- console.warn("[Paywallo INIT]", "init() FAILED:", error instanceof Error ? error.message : String(error));
3256
3817
  this.initPromise = null;
3257
3818
  this.config = null;
3258
3819
  this.apiClient = null;
@@ -3261,6 +3822,18 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3261
3822
  throw error;
3262
3823
  }
3263
3824
  }
3825
+ isReady() {
3826
+ return this.config !== null && this.apiClient !== null;
3827
+ }
3828
+ waitUntilReady() {
3829
+ if (this.isReady()) return Promise.resolve();
3830
+ if (!this.readyPromise) {
3831
+ this.readyPromise = new Promise((resolve) => {
3832
+ this.resolveReady = resolve;
3833
+ });
3834
+ }
3835
+ return this.readyPromise;
3836
+ }
3264
3837
  async initWithRetry(config) {
3265
3838
  while (this.initAttempts <= _PaywalloClientClass.MAX_INIT_RETRIES) {
3266
3839
  try {
@@ -3275,7 +3848,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3275
3848
  this.initAttempts++;
3276
3849
  if (this.initAttempts > _PaywalloClientClass.MAX_INIT_RETRIES) throw error;
3277
3850
  const delay = _PaywalloClientClass.RETRY_DELAYS[this.initAttempts - 1] ?? 1e4;
3278
- if (config.debug) console.warn("[Paywallo]", `Init attempt ${this.initAttempts} failed, retrying in ${delay}ms`, error);
3279
3851
  await new Promise((resolve) => setTimeout(resolve, delay));
3280
3852
  }
3281
3853
  }
@@ -3286,11 +3858,8 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3286
3858
  this.networkCleanup = networkMonitor.addListener((state) => {
3287
3859
  if (state !== "online") return;
3288
3860
  if (this.config && this.apiClient) return;
3289
- if (config.debug) console.warn("[Paywallo]", "Network recovered \u2014 retrying init");
3290
3861
  this.initPromise = this.initWithRetry(config);
3291
- this.initPromise.then(() => {
3292
- if (config.debug) console.warn("[Paywallo]", "Init succeeded after network recovery");
3293
- }).catch(() => {
3862
+ this.initPromise.catch(() => {
3294
3863
  this.reportInitFailure(config, null, "NETWORK_RECOVERY_FAILED");
3295
3864
  });
3296
3865
  });
@@ -3319,38 +3888,79 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3319
3888
  } catch {
3320
3889
  }
3321
3890
  }
3891
+ async resolveSessionFlags(keys, apiClient, distinctId) {
3892
+ try {
3893
+ const results = await apiClient.evaluateFlags(keys, distinctId);
3894
+ this.sessionFlagsMap.clear();
3895
+ for (const key of keys) {
3896
+ this.sessionFlagsMap.set(key, key in results ? results[key] : null);
3897
+ }
3898
+ } catch (error) {
3899
+ for (const key of keys) {
3900
+ this.sessionFlagsMap.set(key, null);
3901
+ }
3902
+ }
3903
+ }
3322
3904
  async doInit(config) {
3323
3905
  const environment = config.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "Sandbox" : "Production");
3324
3906
  const offlineQueueEnabled = config.offlineQueueEnabled !== false;
3325
- console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3326
3907
  await Promise.all([
3327
3908
  networkMonitor.initialize({ debug: config.debug }),
3328
3909
  offlineQueue.initialize({ debug: config.debug }).then(
3329
3910
  () => offlineQueue.clearItemsWithInvalidAppKey(config.appKey)
3330
3911
  )
3331
3912
  ]);
3332
- console.warn("[Paywallo INIT]", "Step 1 DONE");
3333
- console.warn("[Paywallo INIT]", "Step 2: ApiClient + queueProcessor + affiliate...");
3334
3913
  this.apiClient = new ApiClient(config.appKey, config.debug, environment, offlineQueueEnabled, config.timeout);
3335
3914
  queueProcessor.initialize(this.apiClient.getHttpClient(), { debug: config.debug });
3336
3915
  affiliateManager.initialize(this.apiClient, config.debug);
3337
- console.warn("[Paywallo INIT]", "Step 2 DONE");
3338
- console.warn("[Paywallo INIT]", "Step 3: identityManager + sessionManager...");
3916
+ subscriptionManager.init({
3917
+ serverUrl: this.apiClient.getBaseUrl(),
3918
+ appKey: config.appKey,
3919
+ cacheTTL: config.subscriptionCacheTTL,
3920
+ debug: config.debug
3921
+ });
3339
3922
  await Promise.all([
3340
3923
  identityManager.initialize(this.apiClient, config.debug),
3341
3924
  sessionManager.initialize(this.apiClient, config.sessionConfig, config.debug)
3342
3925
  ]);
3343
3926
  const distinctId = identityManager.getDistinctId();
3344
- console.warn("[Paywallo INIT]", "Step 3 DONE \u2014 distinctId:", distinctId ? distinctId.substring(0, 8) + "..." : "EMPTY");
3345
3927
  if (config.autoStartSession !== false) {
3346
3928
  sessionManager.startSession().catch(() => {
3347
- if (config.debug) console.warn("[Paywallo]", "Session start failed during init \u2014 continuing");
3348
3929
  });
3349
3930
  }
3350
3931
  new InstallTracker(config.debug).trackIfNeeded(this.apiClient, config.requestATT === true).catch(() => {
3351
3932
  });
3933
+ if (config.sessionFlags && config.sessionFlags.length > 0) {
3934
+ await this.resolveSessionFlags(config.sessionFlags, this.apiClient, distinctId);
3935
+ }
3352
3936
  this.config = config;
3353
- console.warn("[Paywallo INIT]", "COMPLETE \u2014 SDK ready, environment:", environment);
3937
+ if (this.resolveReady) {
3938
+ this.resolveReady();
3939
+ this.resolveReady = null;
3940
+ }
3941
+ if (config.debug) console.info("[Paywallo INIT] complete");
3942
+ if (this.apiClient) {
3943
+ const apiClientRef = this.apiClient;
3944
+ if (config.autoPreloadCampaign) {
3945
+ this.autoPreloadedPlacement = config.autoPreloadCampaign;
3946
+ this.autoPreloadPlacementPromise = Promise.resolve(config.autoPreloadCampaign);
3947
+ if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${config.autoPreloadCampaign}`);
3948
+ void campaignGateService.preloadCampaign(apiClientRef, config.autoPreloadCampaign).catch(() => {
3949
+ });
3950
+ } else {
3951
+ this.autoPreloadPlacementPromise = apiClientRef.getPrimaryCampaign(distinctId).then((primary) => {
3952
+ const placement = primary?.placement ?? primary?.paywall?.placement;
3953
+ if (placement) {
3954
+ this.autoPreloadedPlacement = placement;
3955
+ if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${placement}`);
3956
+ void campaignGateService.preloadCampaign(apiClientRef, placement).catch(() => {
3957
+ });
3958
+ return placement;
3959
+ }
3960
+ return null;
3961
+ }).catch(() => null);
3962
+ }
3963
+ }
3354
3964
  }
3355
3965
  async identify(options) {
3356
3966
  this.ensureInitialized();
@@ -3360,7 +3970,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3360
3970
  const apiClient = this.getApiClientOrThrow();
3361
3971
  const distinctId = identityManager.getDistinctId();
3362
3972
  if (!distinctId) {
3363
- console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3364
3973
  return;
3365
3974
  }
3366
3975
  if (!isValidEventName(eventName)) {
@@ -3368,25 +3977,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3368
3977
  throw new ClientError(CLIENT_ERROR_CODES.INVALID_EVENT_NAME, msg);
3369
3978
  }
3370
3979
  const sessionId = sessionManager.getSessionId();
3371
- if (eventName === "$purchase_completed") {
3372
- console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3373
- distinctId: distinctId.substring(0, 8) + "...",
3374
- productId: options?.properties?.productId,
3375
- priceUsd: options?.properties?.priceUsd,
3376
- price: options?.properties?.price,
3377
- currency: options?.properties?.currency,
3378
- placement: options?.properties?.placement
3379
- }));
3380
- } else if (eventName === "$paywall_viewed" || eventName === "$paywall_closed") {
3381
- console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3382
- distinctId: distinctId.substring(0, 8) + "...",
3383
- placement: options?.properties?.placement,
3384
- variantKey: options?.properties?.variantKey,
3385
- timeOnPaywall: options?.properties?.timeOnPaywall
3386
- }));
3387
- } else {
3388
- console.warn("[Paywallo TRACK]", eventName, "distinctId:", distinctId.substring(0, 8) + "...");
3389
- }
3390
3980
  await apiClient.trackEvent(eventName, distinctId, {
3391
3981
  ...identityManager.getProperties(),
3392
3982
  ...options?.properties,
@@ -3397,7 +3987,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3397
3987
  const apiClient = this.getApiClientOrThrow();
3398
3988
  const distinctId = identityManager.getDistinctId();
3399
3989
  if (!distinctId) {
3400
- if (this.config?.debug) console.warn("[Paywallo] Skipping event \u2014 no distinctId available");
3401
3990
  return;
3402
3991
  }
3403
3992
  if (!Number.isInteger(index) || index < 0) {
@@ -3421,7 +4010,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3421
4010
  const apiClient = this.getApiClientOrThrow();
3422
4011
  const distinctId = identityManager.getDistinctId();
3423
4012
  if (!distinctId) {
3424
- if (this.config?.debug) console.warn("[Paywallo] Skipping event \u2014 no distinctId available");
3425
4013
  return;
3426
4014
  }
3427
4015
  if (!actionName || typeof actionName !== "string") {
@@ -3436,22 +4024,32 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3436
4024
  ...sessionId && { sessionId }
3437
4025
  });
3438
4026
  }
4027
+ async getVariantCached(flagKey, defaultValue) {
4028
+ this.ensureInitialized();
4029
+ const apiClient = this.getApiClientOrThrow();
4030
+ const distinctId = identityManager.getDistinctId();
4031
+ const result = await apiClient.getVariantFromCache(flagKey, distinctId);
4032
+ if (result) return result;
4033
+ void apiClient.getVariant(flagKey, distinctId).catch(() => {
4034
+ });
4035
+ return { variant: defaultValue ?? null, payload: void 0 };
4036
+ }
3439
4037
  async getVariant(flagKey) {
3440
4038
  try {
3441
4039
  const distinctId = identityManager.getDistinctId();
3442
4040
  if (!distinctId) {
3443
- console.warn("[Paywallo AB]", "getVariant SKIPPED \u2014 no distinctId", flagKey);
3444
4041
  return { variant: null };
3445
4042
  }
3446
- const result = await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3447
- console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3448
- return result;
4043
+ return await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3449
4044
  } catch (error) {
3450
- if (this.config?.debug) console.warn("[Paywallo]", "getVariant failed \u2014 returning null", error);
3451
4045
  if (this.apiClient) void this.apiClient.reportError("GET_VARIANT_FAILED", error instanceof Error ? error.message : String(error));
3452
4046
  return { variant: null };
3453
4047
  }
3454
4048
  }
4049
+ async evaluateFlags(keys) {
4050
+ this.ensureInitialized();
4051
+ return this.getApiClientOrThrow().evaluateFlags(keys, identityManager.getDistinctId());
4052
+ }
3455
4053
  async getConditionalFlag(flagKey, context) {
3456
4054
  try {
3457
4055
  const distinctId = identityManager.getDistinctId();
@@ -3459,7 +4057,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3459
4057
  const result = await this.getApiClientOrThrow().getConditionalFlag(flagKey, { distinctId, ...context });
3460
4058
  return result.value;
3461
4059
  } catch (error) {
3462
- if (this.config?.debug) console.warn("[Paywallo]", "getConditionalFlag failed \u2014 returning false", error);
3463
4060
  if (this.apiClient) void this.apiClient.reportError("GET_CONDITIONAL_FLAG_FAILED", error instanceof Error ? error.message : String(error));
3464
4061
  return false;
3465
4062
  }
@@ -3477,18 +4074,22 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3477
4074
  }
3478
4075
  async presentCampaign(placement, context) {
3479
4076
  this.ensureInitialized();
3480
- return campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, this.paywallPresenter, this.campaignPresenter, context);
4077
+ return campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, () => this.hasActiveSubscription(), this.paywallPresenter, this.campaignPresenter, context);
3481
4078
  }
3482
4079
  async hasActiveSubscription() {
3483
4080
  this.ensureInitialized();
3484
4081
  try {
3485
- if (this.activeChecker) return await this.activeChecker();
4082
+ if (this.activeChecker) {
4083
+ return await this.activeChecker();
4084
+ }
3486
4085
  const distinctId = identityManager.getDistinctId();
3487
- if (!distinctId || !this.apiClient) return false;
3488
- return (await this.apiClient.getSubscriptionStatus(distinctId)).hasActiveSubscription;
3489
- } catch {
3490
- if (this.config?.debug) console.warn("[Paywallo]", "hasActiveSubscription failed \u2014 returning false");
3491
- if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", "hasActiveSubscription failed");
4086
+ if (!distinctId || !this.apiClient) {
4087
+ return false;
4088
+ }
4089
+ const status = await this.apiClient.getSubscriptionStatus(distinctId);
4090
+ return status.hasActiveSubscription;
4091
+ } catch (error) {
4092
+ if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", error instanceof Error ? error.message : "hasActiveSubscription failed");
3492
4093
  return false;
3493
4094
  }
3494
4095
  }
@@ -3502,7 +4103,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3502
4103
  if (!status.subscription) return null;
3503
4104
  return { ...status.subscription, expiresAt: status.subscription.expiresAt ? new Date(status.subscription.expiresAt) : null };
3504
4105
  } catch {
3505
- if (this.config?.debug) console.warn("[Paywallo]", "getSubscription failed \u2014 returning null");
3506
4106
  return null;
3507
4107
  }
3508
4108
  }
@@ -3513,19 +4113,24 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3513
4113
  }
3514
4114
  async reset() {
3515
4115
  await identityManager.reset();
4116
+ await subscriptionCache.invalidateAll();
3516
4117
  }
3517
4118
  async fullReset() {
3518
4119
  const wasDebug = this.config?.debug ?? false;
3519
4120
  await sessionManager.endSession();
3520
4121
  await identityManager.reset();
4122
+ await subscriptionCache.invalidateAll();
3521
4123
  queueProcessor.dispose();
3522
4124
  offlineQueue.dispose();
3523
4125
  networkMonitor.dispose();
3524
4126
  campaignGateService.clearPreloaded();
3525
4127
  this.cleanupNetworkRecovery();
4128
+ this.sessionFlagsMap.clear();
3526
4129
  this.apiClient = null;
3527
4130
  this.config = null;
3528
4131
  this.initPromise = null;
4132
+ this.readyPromise = null;
4133
+ this.resolveReady = null;
3529
4134
  this.pendingConfig = null;
3530
4135
  this.initAttempts = 0;
3531
4136
  this.paywallPresenter = null;
@@ -3534,7 +4139,13 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3534
4139
  this.activeChecker = null;
3535
4140
  this.restoreHandler = null;
3536
4141
  this.lastOnboardingStepTimestamp = null;
3537
- if (wasDebug) console.warn("[Paywallo]", "Full reset complete");
4142
+ this.autoPreloadedPlacement = null;
4143
+ this.autoPreloadPlacementPromise = null;
4144
+ }
4145
+ getSessionFlag(key) {
4146
+ if (!this.config || !this.apiClient) return null;
4147
+ const value = this.sessionFlagsMap.get(key);
4148
+ return value !== void 0 ? value : null;
3538
4149
  }
3539
4150
  getConfig() {
3540
4151
  return this.config;
@@ -3623,6 +4234,15 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3623
4234
  getPreloadedCampaign(placement) {
3624
4235
  return campaignGateService.getPreloadedCampaign(placement);
3625
4236
  }
4237
+ isPreloaded(placement) {
4238
+ return campaignGateService.getPreloadedCampaign(placement) !== null;
4239
+ }
4240
+ getAutoPreloadedPlacement() {
4241
+ return this.autoPreloadedPlacement;
4242
+ }
4243
+ waitForAutoPreloadedPlacement() {
4244
+ return this.autoPreloadPlacementPromise ?? Promise.resolve(null);
4245
+ }
3626
4246
  isOnline() {
3627
4247
  return networkMonitor.isOnline();
3628
4248
  }
@@ -3652,48 +4272,6 @@ var PaywalloClient = new PaywalloClientClass();
3652
4272
  var import_react9 = require("react");
3653
4273
  var PaywalloContext = (0, import_react9.createContext)(null);
3654
4274
 
3655
- // src/utils/localization.ts
3656
- var import_react_native12 = require("react-native");
3657
- var currentLanguage = "pt-BR";
3658
- var defaultLanguage = "pt-BR";
3659
- function setCurrentLanguage(language) {
3660
- currentLanguage = language;
3661
- }
3662
- function setDefaultLanguage(language) {
3663
- defaultLanguage = language;
3664
- }
3665
- function getCurrentLanguage() {
3666
- return currentLanguage;
3667
- }
3668
- function getDefaultLanguage() {
3669
- return defaultLanguage;
3670
- }
3671
- function getLocalizedString(text) {
3672
- if (!text) return "";
3673
- if (typeof text === "string") return text;
3674
- return text[currentLanguage] ?? text[defaultLanguage] ?? Object.values(text)[0] ?? "";
3675
- }
3676
- function detectDeviceLanguage() {
3677
- try {
3678
- if (import_react_native12.Platform.OS === "ios") {
3679
- const settings = import_react_native12.NativeModules.SettingsManager?.settings;
3680
- return settings?.AppleLocale ?? settings?.AppleLanguages?.[0] ?? "en-US";
3681
- }
3682
- return import_react_native12.NativeModules.I18nManager?.localeIdentifier ?? "en-US";
3683
- } catch {
3684
- return "en-US";
3685
- }
3686
- }
3687
- function initLocalization(options) {
3688
- if (options?.defaultLanguage) {
3689
- defaultLanguage = options.defaultLanguage;
3690
- }
3691
- if (options?.detectDevice !== false) {
3692
- const deviceLanguage = detectDeviceLanguage();
3693
- currentLanguage = deviceLanguage;
3694
- }
3695
- }
3696
-
3697
4275
  // src/hooks/usePaywallo.ts
3698
4276
  var import_react10 = require("react");
3699
4277
  function usePaywallo() {
@@ -3719,7 +4297,8 @@ function mapToIAPProduct(product) {
3719
4297
  type: product.type === "subscription" ? "subs" : "inapp",
3720
4298
  subscriptionPeriodAndroid: product.subscriptionPeriod,
3721
4299
  introductoryPrice: product.introductoryPrice,
3722
- freeTrialPeriodAndroid: product.freeTrialPeriod
4300
+ freeTrialPeriodAndroid: product.freeTrialPeriod,
4301
+ trialDays: product.trialDays ?? 0
3723
4302
  };
3724
4303
  }
3725
4304
  function mapToFormattedProduct(product) {
@@ -3734,6 +4313,7 @@ function mapToFormattedProduct(product) {
3734
4313
  subscriptionPeriod: product.subscriptionPeriod,
3735
4314
  introductoryPrice: product.introductoryPrice,
3736
4315
  freeTrialPeriod: product.freeTrialPeriod,
4316
+ trialDays: product.trialDays ?? 0,
3737
4317
  pricePerMonth: product.pricePerMonth,
3738
4318
  pricePerWeek: product.pricePerWeek,
3739
4319
  pricePerDay: product.pricePerDay
@@ -3815,27 +4395,37 @@ function usePurchase() {
3815
4395
  setError(null);
3816
4396
  try {
3817
4397
  const iapService = getIAPService();
3818
- const result = await iapService.purchase(productId);
4398
+ const result = await iapService.purchase(productId, {
4399
+ onValidating: () => setState("validating")
4400
+ });
3819
4401
  if (result.success && result.purchase) {
3820
4402
  setState("idle");
3821
4403
  return {
3822
- productId: result.purchase.productId,
3823
- transactionId: result.purchase.transactionId,
3824
- transactionDate: result.purchase.transactionDate,
3825
- transactionReceipt: result.purchase.receipt
4404
+ status: "success",
4405
+ purchase: {
4406
+ productId: result.purchase.productId,
4407
+ transactionId: result.purchase.transactionId,
4408
+ transactionDate: result.purchase.transactionDate,
4409
+ transactionReceipt: result.purchase.receipt
4410
+ }
3826
4411
  };
3827
4412
  }
3828
4413
  if (result.error) {
3829
- const purchaseError = createPurchaseError("PURCHASE_FAILED", result.error.message);
3830
- setError(purchaseError);
4414
+ setError(result.error);
3831
4415
  setState("error");
3832
- throw result.error;
4416
+ return { status: "failed", error: result.error };
3833
4417
  }
3834
4418
  setState("idle");
3835
- throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, "Purchase cancelled by user", true);
4419
+ return { status: "cancelled" };
3836
4420
  } catch (err) {
4421
+ const purchaseError = err instanceof PurchaseError ? err : createPurchaseError("PURCHASE_FAILED", err instanceof Error ? err.message : String(err));
4422
+ if (purchaseError.userCancelled) {
4423
+ setState("idle");
4424
+ return { status: "cancelled" };
4425
+ }
4426
+ setError(purchaseError);
3837
4427
  setState("error");
3838
- throw err;
4428
+ return { status: "failed", error: purchaseError };
3839
4429
  }
3840
4430
  }, []);
3841
4431
  const restore = (0, import_react12.useCallback)(async () => {
@@ -3864,226 +4454,14 @@ function usePurchase() {
3864
4454
  restore,
3865
4455
  error,
3866
4456
  isLoading: state === "loading",
3867
- isPurchasing: state === "purchasing",
4457
+ isPurchasing: state === "purchasing" || state === "validating",
4458
+ isValidating: state === "validating",
3868
4459
  isRestoring: state === "restoring"
3869
4460
  };
3870
4461
  }
3871
4462
 
3872
4463
  // src/hooks/useSubscription.ts
3873
4464
  var import_react13 = require("react");
3874
-
3875
- // src/domains/subscription/SubscriptionManager.ts
3876
- var import_react_native13 = require("react-native");
3877
-
3878
- // src/domains/subscription/SubscriptionCache.ts
3879
- var CACHE_KEY = "subscription_cache";
3880
- var DEFAULT_TTL = 5 * 60 * 1e3;
3881
- var SubscriptionCache = class {
3882
- constructor(ttl = DEFAULT_TTL) {
3883
- this.memoryCache = null;
3884
- this.ttl = ttl;
3885
- this.storage = new SecureStorage();
3886
- }
3887
- async get() {
3888
- if (this.memoryCache && !this.isExpired(this.memoryCache)) {
3889
- return this.memoryCache;
3890
- }
3891
- try {
3892
- const stored = await this.storage.get(CACHE_KEY);
3893
- if (!stored) {
3894
- return null;
3895
- }
3896
- const cached = JSON.parse(stored);
3897
- if (cached.data.subscription?.expiresAt) {
3898
- cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
3899
- }
3900
- if (cached.data.subscription?.originalPurchaseDate) {
3901
- cached.data.subscription.originalPurchaseDate = new Date(
3902
- cached.data.subscription.originalPurchaseDate
3903
- );
3904
- }
3905
- if (cached.data.subscription?.latestPurchaseDate) {
3906
- cached.data.subscription.latestPurchaseDate = new Date(
3907
- cached.data.subscription.latestPurchaseDate
3908
- );
3909
- }
3910
- if (cached.data.subscription?.cancellationDate) {
3911
- cached.data.subscription.cancellationDate = new Date(
3912
- cached.data.subscription.cancellationDate
3913
- );
3914
- }
3915
- if (cached.data.subscription?.gracePeriodExpiresAt) {
3916
- cached.data.subscription.gracePeriodExpiresAt = new Date(
3917
- cached.data.subscription.gracePeriodExpiresAt
3918
- );
3919
- }
3920
- cached.isStale = this.isExpired(cached);
3921
- if (!cached.isStale) {
3922
- this.memoryCache = cached;
3923
- }
3924
- return cached;
3925
- } catch {
3926
- return null;
3927
- }
3928
- }
3929
- async set(data) {
3930
- const cached = {
3931
- data,
3932
- cachedAt: Date.now(),
3933
- isStale: false
3934
- };
3935
- this.memoryCache = cached;
3936
- try {
3937
- await this.storage.set(CACHE_KEY, JSON.stringify(cached));
3938
- } catch {
3939
- }
3940
- }
3941
- async invalidate() {
3942
- this.memoryCache = null;
3943
- try {
3944
- await this.storage.remove(CACHE_KEY);
3945
- } catch {
3946
- }
3947
- }
3948
- isExpired(cached) {
3949
- return Date.now() - cached.cachedAt > this.ttl;
3950
- }
3951
- setTTL(ttl) {
3952
- this.ttl = ttl;
3953
- }
3954
- };
3955
-
3956
- // src/domains/subscription/SubscriptionManager.ts
3957
- var SubscriptionManagerClass = class {
3958
- constructor() {
3959
- this.config = null;
3960
- this.listeners = /* @__PURE__ */ new Set();
3961
- this.userId = null;
3962
- this.cache = new SubscriptionCache();
3963
- }
3964
- init(config) {
3965
- this.config = config;
3966
- if (config.cacheTTL) {
3967
- this.cache.setTTL(config.cacheTTL);
3968
- }
3969
- }
3970
- setUserId(userId) {
3971
- if (this.userId !== userId) {
3972
- this.userId = userId;
3973
- void this.cache.invalidate();
3974
- }
3975
- }
3976
- async hasActiveSubscription(forceRefresh = false) {
3977
- const status = await this.getSubscriptionStatus(forceRefresh);
3978
- return status.hasActiveSubscription;
3979
- }
3980
- async getSubscription(forceRefresh = false) {
3981
- const status = await this.getSubscriptionStatus(forceRefresh);
3982
- return status.subscription;
3983
- }
3984
- async getEntitlements(forceRefresh = false) {
3985
- const status = await this.getSubscriptionStatus(forceRefresh);
3986
- return status.entitlements;
3987
- }
3988
- async getSubscriptionStatus(forceRefresh = false) {
3989
- if (!this.config) {
3990
- return this.getEmptyStatus();
3991
- }
3992
- if (!forceRefresh) {
3993
- const cached = await this.cache.get();
3994
- if (cached && !cached.isStale) {
3995
- return cached.data;
3996
- }
3997
- }
3998
- try {
3999
- const status = await this.fetchSubscriptionStatus();
4000
- await this.cache.set(status);
4001
- this.notifyListeners(status);
4002
- return status;
4003
- } catch (error) {
4004
- this.log("Failed to fetch subscription status", error);
4005
- const cached = await this.cache.get();
4006
- if (cached) {
4007
- return cached.data;
4008
- }
4009
- return this.getEmptyStatus();
4010
- }
4011
- }
4012
- async restorePurchases() {
4013
- if (!this.config) {
4014
- throw new SessionError(
4015
- SESSION_ERROR_CODES.NOT_INITIALIZED,
4016
- "SubscriptionManager not initialized"
4017
- );
4018
- }
4019
- await this.cache.invalidate();
4020
- return this.getSubscriptionStatus(true);
4021
- }
4022
- async fetchSubscriptionStatus() {
4023
- if (!this.config) {
4024
- return this.getEmptyStatus();
4025
- }
4026
- const url = new URL(`${this.config.serverUrl}/subscriptions/status`);
4027
- if (this.userId) {
4028
- url.searchParams.set("userId", this.userId);
4029
- }
4030
- url.searchParams.set("platform", import_react_native13.Platform.OS);
4031
- const response = await fetch(url.toString(), {
4032
- method: "GET",
4033
- headers: {
4034
- "Content-Type": "application/json",
4035
- "X-App-Key": this.config.appKey
4036
- }
4037
- });
4038
- if (!response.ok) {
4039
- throw new SessionError(
4040
- SESSION_ERROR_CODES.RESTORE_FAILED,
4041
- `Failed to fetch subscription status: ${response.status}`
4042
- );
4043
- }
4044
- const data = await response.json();
4045
- if (data.subscription) {
4046
- data.subscription.expiresAt = new Date(data.subscription.expiresAt);
4047
- data.subscription.originalPurchaseDate = new Date(data.subscription.originalPurchaseDate);
4048
- data.subscription.latestPurchaseDate = new Date(data.subscription.latestPurchaseDate);
4049
- if (data.subscription.cancellationDate) {
4050
- data.subscription.cancellationDate = new Date(data.subscription.cancellationDate);
4051
- }
4052
- if (data.subscription.gracePeriodExpiresAt) {
4053
- data.subscription.gracePeriodExpiresAt = new Date(data.subscription.gracePeriodExpiresAt);
4054
- }
4055
- }
4056
- return data;
4057
- }
4058
- getEmptyStatus() {
4059
- return {
4060
- hasActiveSubscription: false,
4061
- subscription: null,
4062
- entitlements: []
4063
- };
4064
- }
4065
- addListener(listener) {
4066
- this.listeners.add(listener);
4067
- return () => this.listeners.delete(listener);
4068
- }
4069
- notifyListeners(status) {
4070
- for (const listener of this.listeners) {
4071
- listener(status);
4072
- }
4073
- }
4074
- async onPurchaseComplete() {
4075
- await this.cache.invalidate();
4076
- await this.getSubscriptionStatus(true);
4077
- }
4078
- log(message, data) {
4079
- if (this.config?.debug) {
4080
- console.warn(`[SubscriptionManager] ${message}`, data ?? "");
4081
- }
4082
- }
4083
- };
4084
- var subscriptionManager = new SubscriptionManagerClass();
4085
-
4086
- // src/hooks/useSubscription.ts
4087
4465
  function useSubscription() {
4088
4466
  const [status, setStatus] = (0, import_react13.useState)(null);
4089
4467
  const [isLoading, setIsLoading] = (0, import_react13.useState)(true);
@@ -4200,10 +4578,9 @@ var CACHE_TTL_MS = 5 * 60 * 1e3;
4200
4578
  function isCacheEntryValid(entry) {
4201
4579
  return !!entry && Date.now() - entry.preloadedAt < CACHE_TTL_MS;
4202
4580
  }
4203
- function consumeCacheEntry(cache, placement) {
4581
+ function getCacheEntry(cache, placement) {
4204
4582
  const entry = cache.get(placement);
4205
4583
  if (!isCacheEntryValid(entry)) return null;
4206
- cache.delete(placement);
4207
4584
  return entry ?? null;
4208
4585
  }
4209
4586
 
@@ -4216,7 +4593,7 @@ function useCampaignPreload() {
4216
4593
  []
4217
4594
  );
4218
4595
  const consumePreloadedCampaign = (0, import_react14.useCallback)(
4219
- (placement) => consumeCacheEntry(preloadedCampaignsRef.current, placement),
4596
+ (placement) => getCacheEntry(preloadedCampaignsRef.current, placement),
4220
4597
  []
4221
4598
  );
4222
4599
  const preloadCampaign = (0, import_react14.useCallback)(
@@ -4264,6 +4641,7 @@ function useCampaignPreload() {
4264
4641
  campaignId: campaign.campaignId,
4265
4642
  variantKey: campaign.variantKey
4266
4643
  });
4644
+ if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo WEBVIEW] mounting hidden \u2014 placement: ${placement}`);
4267
4645
  return { success: true };
4268
4646
  } catch (error) {
4269
4647
  return {
@@ -4298,7 +4676,6 @@ function useProductLoader() {
4298
4676
  for (const p of loaded) map.set(p.productId, p);
4299
4677
  setProducts(map);
4300
4678
  } catch (error) {
4301
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Product load failed:", error);
4302
4679
  } finally {
4303
4680
  setIsLoadingProducts(false);
4304
4681
  }
@@ -4308,7 +4685,7 @@ function useProductLoader() {
4308
4685
 
4309
4686
  // src/hooks/usePurchaseOrchestrator.ts
4310
4687
  var import_react16 = require("react");
4311
- function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef) {
4688
+ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, invalidateSubscriptionCache) {
4312
4689
  const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
4313
4690
  const handlePreloadedPurchase = (0, import_react16.useCallback)(
4314
4691
  async (productId) => {
@@ -4333,17 +4710,17 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4333
4710
  await trackDismissed({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, durationSeconds, action: "purchase", variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4334
4711
  await trackPurchased({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, productId, variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4335
4712
  }
4713
+ invalidateSubscriptionCache();
4336
4714
  setShowingPreloadedWebView(false);
4337
4715
  clearPreloadedWebView();
4338
4716
  resolvePreloadedPaywall({ presented: true, purchased: true, cancelled: false, restored: false });
4339
4717
  } catch (error) {
4340
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Preloaded purchase error:", error);
4341
4718
  setShowingPreloadedWebView(false);
4342
4719
  clearPreloadedWebView();
4343
4720
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
4344
4721
  }
4345
4722
  },
4346
- [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased]
4723
+ [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased, invalidateSubscriptionCache]
4347
4724
  );
4348
4725
  const handlePreloadedRestore = (0, import_react16.useCallback)(async () => {
4349
4726
  try {
@@ -4353,28 +4730,70 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4353
4730
  const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
4354
4731
  await trackDismissed({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, durationSeconds, action: "restore", variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4355
4732
  }
4733
+ invalidateSubscriptionCache();
4356
4734
  setShowingPreloadedWebView(false);
4357
4735
  clearPreloadedWebView();
4358
4736
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: true });
4359
4737
  } catch (error) {
4360
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Preloaded restore error:", error);
4361
4738
  setShowingPreloadedWebView(false);
4362
4739
  clearPreloadedWebView();
4363
4740
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
4364
4741
  }
4365
- }, [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed]);
4742
+ }, [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, invalidateSubscriptionCache]);
4366
4743
  return { handlePreloadedPurchase, handlePreloadedRestore };
4367
4744
  }
4368
4745
 
4369
4746
  // src/hooks/useSubscriptionSync.ts
4370
4747
  var import_react17 = require("react");
4371
4748
  var CACHE_TTL_MS2 = 5 * 60 * 1e3;
4372
- var ACTIVE_STATUSES = ["active", "in_grace_period"];
4749
+ var ACTIVE_STATUSES = ["active", "grace_period"];
4750
+ function toDomainSubscriptionInfo(sub) {
4751
+ return {
4752
+ id: "",
4753
+ productId: sub.productId,
4754
+ status: sub.status,
4755
+ expiresAt: sub.expiresAt ?? /* @__PURE__ */ new Date(0),
4756
+ originalPurchaseDate: /* @__PURE__ */ new Date(0),
4757
+ latestPurchaseDate: /* @__PURE__ */ new Date(0),
4758
+ willRenew: sub.autoRenewEnabled,
4759
+ isTrial: false,
4760
+ isIntroductoryPeriod: false,
4761
+ platform: sub.platform
4762
+ };
4763
+ }
4764
+ function toDomainResponse(status) {
4765
+ return {
4766
+ hasActiveSubscription: status.hasActiveSubscription,
4767
+ subscription: status.subscription ? toDomainSubscriptionInfo(status.subscription) : null,
4768
+ entitlements: []
4769
+ };
4770
+ }
4771
+ function isSubscriptionStillActive(sub) {
4772
+ if (!sub) return false;
4773
+ if (!ACTIVE_STATUSES.includes(sub.status)) return false;
4774
+ if (sub.expiresAt && sub.expiresAt.getTime() !== 0 && sub.expiresAt < /* @__PURE__ */ new Date()) return false;
4775
+ return true;
4776
+ }
4777
+ async function resolveOfflineFromCache(distinctId) {
4778
+ try {
4779
+ const cached = await subscriptionCache.get(distinctId);
4780
+ if (!cached) return false;
4781
+ const sub = cached.data.subscription;
4782
+ return isSubscriptionStillActive(sub ? { status: sub.status, expiresAt: sub.expiresAt ?? null } : null);
4783
+ } catch {
4784
+ return false;
4785
+ }
4786
+ }
4787
+ function isActiveCacheEntry(entry) {
4788
+ return isSubscriptionStillActive(entry.data);
4789
+ }
4373
4790
  function useSubscriptionSync() {
4374
4791
  const [subscription, setSubscription] = (0, import_react17.useState)(null);
4375
4792
  const cacheRef = (0, import_react17.useRef)(null);
4376
4793
  const invalidateCache = (0, import_react17.useCallback)(() => {
4377
4794
  cacheRef.current = null;
4795
+ const distinctId = PaywalloClient.getDistinctId();
4796
+ if (distinctId) void subscriptionCache.invalidate(distinctId);
4378
4797
  }, []);
4379
4798
  const fetchAndCache = (0, import_react17.useCallback)(async () => {
4380
4799
  const apiClient = PaywalloClient.getApiClient();
@@ -4387,6 +4806,7 @@ function useSubscriptionSync() {
4387
4806
  } : null;
4388
4807
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4389
4808
  setSubscription(sub);
4809
+ void subscriptionCache.set(distinctId, toDomainResponse(status));
4390
4810
  return sub;
4391
4811
  }, []);
4392
4812
  const hasActiveSubscription = (0, import_react17.useCallback)(async () => {
@@ -4395,10 +4815,28 @@ function useSubscriptionSync() {
4395
4815
  if (!apiClient || !distinctId) return false;
4396
4816
  const cached = cacheRef.current;
4397
4817
  if (cached && cached.expiresAt > Date.now()) {
4398
- if (!cached.data) return false;
4399
- if (!ACTIVE_STATUSES.includes(cached.data.status)) return false;
4400
- if (cached.data.expiresAt && cached.data.expiresAt < /* @__PURE__ */ new Date()) return false;
4401
- return true;
4818
+ return isActiveCacheEntry(cached);
4819
+ }
4820
+ try {
4821
+ const persisted = await subscriptionCache.get(distinctId);
4822
+ if (persisted && !persisted.isStale) {
4823
+ const persistedSub = persisted.data.subscription;
4824
+ const sub = persistedSub ? {
4825
+ productId: persistedSub.productId,
4826
+ status: persistedSub.status,
4827
+ expiresAt: persistedSub.expiresAt ?? null,
4828
+ platform: persistedSub.platform,
4829
+ autoRenewEnabled: persistedSub.willRenew,
4830
+ inGracePeriod: persistedSub.status === "grace_period"
4831
+ } : null;
4832
+ const stillActive = isSubscriptionStillActive(sub);
4833
+ if (stillActive) {
4834
+ cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4835
+ setSubscription(sub);
4836
+ return true;
4837
+ }
4838
+ }
4839
+ } catch {
4402
4840
  }
4403
4841
  try {
4404
4842
  const status = await apiClient.getSubscriptionStatus(distinctId);
@@ -4408,9 +4846,10 @@ function useSubscriptionSync() {
4408
4846
  } : null;
4409
4847
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4410
4848
  setSubscription(sub);
4849
+ void subscriptionCache.set(distinctId, toDomainResponse(status));
4411
4850
  return status.hasActiveSubscription;
4412
4851
  } catch {
4413
- return false;
4852
+ return await resolveOfflineFromCache(distinctId);
4414
4853
  }
4415
4854
  }, []);
4416
4855
  const getSubscription = (0, import_react17.useCallback)(async () => {
@@ -4422,7 +4861,22 @@ function useSubscriptionSync() {
4422
4861
  try {
4423
4862
  return await fetchAndCache();
4424
4863
  } catch {
4425
- return null;
4864
+ try {
4865
+ const persisted = await subscriptionCache.get(distinctId);
4866
+ if (!persisted?.data.subscription) return null;
4867
+ const s = persisted.data.subscription;
4868
+ const normalisedStatus = s.status === "grace_period" ? "in_grace_period" : s.status;
4869
+ return {
4870
+ productId: s.productId,
4871
+ status: normalisedStatus,
4872
+ expiresAt: s.expiresAt ?? null,
4873
+ platform: s.platform,
4874
+ autoRenewEnabled: s.willRenew,
4875
+ inGracePeriod: normalisedStatus === "in_grace_period"
4876
+ };
4877
+ } catch {
4878
+ return null;
4879
+ }
4426
4880
  }
4427
4881
  }, [fetchAndCache]);
4428
4882
  return { subscription, hasActiveSubscription, getSubscription, invalidateCache };
@@ -4439,13 +4893,38 @@ function PaywalloProvider({ children, config }) {
4439
4893
  const { subscription, hasActiveSubscription, getSubscription, invalidateCache } = useSubscriptionSync();
4440
4894
  const { preloadedWebView, setPreloadedWebView, preloadCampaign, consumePreloadedCampaign, isPreloadValid } = useCampaignPreload();
4441
4895
  const clearPreloadedWebView = (0, import_react18.useCallback)(() => setPreloadedWebView(null), [setPreloadedWebView]);
4442
- const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView);
4896
+ const autoPreloadTriggeredRef = (0, import_react18.useRef)(false);
4897
+ const preloadedWebViewRef = (0, import_react18.useRef)(preloadedWebView);
4898
+ (0, import_react18.useEffect)(() => {
4899
+ preloadedWebViewRef.current = preloadedWebView;
4900
+ }, [preloadedWebView]);
4901
+ const triggerRepreload = (0, import_react18.useCallback)((placement) => {
4902
+ void PaywalloClient.preloadCampaign(placement).catch(() => {
4903
+ });
4904
+ }, []);
4905
+ const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady: baseHandlePreloadedWebViewReady, handlePreloadedClose: baseHandlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, setPreloadedWebView);
4906
+ const SCREEN_HEIGHT3 = (0, import_react18.useMemo)(() => import_react_native14.Dimensions.get("window").height, []);
4907
+ const slideAnim = (0, import_react18.useRef)(new import_react_native14.Animated.Value(SCREEN_HEIGHT3)).current;
4908
+ const handlePreloadedWebViewReady = (0, import_react18.useCallback)(() => {
4909
+ if (PaywalloClient.getConfig()?.debug) {
4910
+ console.info(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
4911
+ }
4912
+ baseHandlePreloadedWebViewReady();
4913
+ }, [config?.debug, preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
4914
+ const handlePreloadedClose = (0, import_react18.useCallback)(() => {
4915
+ const placement = preloadedWebView?.placement;
4916
+ import_react_native14.Animated.timing(slideAnim, { toValue: SCREEN_HEIGHT3, duration: 200, easing: import_react_native14.Easing.in(import_react_native14.Easing.cubic), useNativeDriver: true }).start(() => {
4917
+ baseHandlePreloadedClose();
4918
+ if (placement) triggerRepreload(placement);
4919
+ });
4920
+ }, [baseHandlePreloadedClose, preloadedWebView, triggerRepreload, slideAnim, SCREEN_HEIGHT3]);
4443
4921
  const { handlePreloadedPurchase: rawPreloadedPurchase, handlePreloadedRestore } = usePurchaseOrchestrator(
4444
4922
  preloadedWebView,
4445
4923
  resolvePreloadedPaywall,
4446
4924
  clearPreloadedWebView,
4447
4925
  setShowingPreloadedWebView,
4448
- presentedAtRef
4926
+ presentedAtRef,
4927
+ invalidateCache
4449
4928
  );
4450
4929
  const [isPreloadPurchasing, setIsPreloadPurchasing] = (0, import_react18.useState)(false);
4451
4930
  const handlePreloadedPurchase = (0, import_react18.useCallback)(async (productId) => {
@@ -4458,13 +4937,19 @@ function PaywalloProvider({ children, config }) {
4458
4937
  }, [rawPreloadedPurchase]);
4459
4938
  (0, import_react18.useEffect)(() => {
4460
4939
  initLocalization({ detectDevice: true });
4461
- PaywalloClient.init(config).then(() => {
4940
+ PaywalloClient.init(config).then(async () => {
4462
4941
  setDistinctId(PaywalloClient.getDistinctId());
4463
4942
  setIsInitialized(true);
4943
+ if (!autoPreloadTriggeredRef.current) {
4944
+ autoPreloadTriggeredRef.current = true;
4945
+ const placement = await PaywalloClient.waitForAutoPreloadedPlacement();
4946
+ if (placement) void preloadCampaign(placement).catch(() => {
4947
+ });
4948
+ }
4464
4949
  }).catch((err) => {
4465
4950
  setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
4466
4951
  });
4467
- }, [config]);
4952
+ }, [config, preloadCampaign]);
4468
4953
  const getPaywallConfig = (0, import_react18.useCallback)(async (p) => {
4469
4954
  const api = PaywalloClient.getApiClient();
4470
4955
  return api ? api.getPaywall(p) : null;
@@ -4563,28 +5048,54 @@ function PaywalloProvider({ children, config }) {
4563
5048
  setActivePaywall(null);
4564
5049
  }
4565
5050
  }, [activePaywall]);
5051
+ const bridgePaywallPresenter = (0, import_react18.useCallback)((placement) => {
5052
+ if (PaywalloClient.getConfig()?.debug) {
5053
+ console.info(`[Paywallo PRESENT] called \u2014 preloaded: ${preloadedWebViewRef.current?.loaded === true} placement: ${placement}`);
5054
+ }
5055
+ if (preloadedWebViewRef.current?.placement === placement) {
5056
+ if (preloadedWebViewRef.current.loaded) {
5057
+ return presentPreloadedWebView();
5058
+ }
5059
+ return new Promise((resolve) => {
5060
+ let elapsed = 0;
5061
+ const poll = () => {
5062
+ if (preloadedWebViewRef.current?.loaded) {
5063
+ resolve(presentPreloadedWebView());
5064
+ return;
5065
+ }
5066
+ elapsed += 100;
5067
+ if (elapsed >= 500) {
5068
+ resolve(presentCampaign(placement));
5069
+ return;
5070
+ }
5071
+ setTimeout(poll, 100);
5072
+ };
5073
+ setTimeout(poll, 100);
5074
+ });
5075
+ }
5076
+ return presentCampaign(placement);
5077
+ }, [presentPreloadedWebView, presentCampaign]);
4566
5078
  (0, import_react18.useEffect)(() => {
4567
5079
  if (!isInitialized) return;
4568
5080
  const onEmergency = async (id) => {
4569
5081
  try {
4570
- await presentPaywall(id);
5082
+ await bridgePaywallPresenter(id);
4571
5083
  } catch {
4572
5084
  }
4573
5085
  };
4574
- PaywalloClient.registerPaywallPresenter(presentPaywall);
5086
+ PaywalloClient.registerPaywallPresenter(bridgePaywallPresenter);
4575
5087
  PaywalloClient.registerSubscriptionGetter(getSubscription);
4576
5088
  PaywalloClient.registerActiveChecker(hasActiveSubscription);
4577
5089
  PaywalloClient.registerRestoreHandler(restorePurchases);
4578
5090
  PaywalloClient.registerEmergencyPaywallHandler(onEmergency);
4579
- }, [isInitialized, presentPaywall, getSubscription, hasActiveSubscription, restorePurchases]);
4580
- (0, import_react18.useEffect)(() => {
4581
- if (activePaywall) setPreloadedWebView(null);
4582
- }, [activePaywall, setPreloadedWebView]);
5091
+ }, [isInitialized, bridgePaywallPresenter, getSubscription, hasActiveSubscription, restorePurchases]);
5092
+ const isReady = isInitialized && !isLoadingProducts;
4583
5093
  const noOp = (0, import_react18.useCallback)(() => {
4584
5094
  }, []);
4585
5095
  const contextValue = (0, import_react18.useMemo)(() => ({
4586
5096
  config: PaywalloClient.getConfig(),
4587
5097
  isInitialized,
5098
+ isReady,
4588
5099
  initError,
4589
5100
  distinctId,
4590
5101
  subscription,
@@ -4600,6 +5111,7 @@ function PaywalloProvider({ children, config }) {
4600
5111
  refreshProducts
4601
5112
  }), [
4602
5113
  isInitialized,
5114
+ isReady,
4603
5115
  initError,
4604
5116
  distinctId,
4605
5117
  subscription,
@@ -4614,20 +5126,16 @@ function PaywalloProvider({ children, config }) {
4614
5126
  getPaywallConfig,
4615
5127
  refreshProducts
4616
5128
  ]);
4617
- const SCREEN_HEIGHT3 = (0, import_react18.useMemo)(() => import_react_native14.Dimensions.get("window").height, []);
4618
- const slideAnim = (0, import_react18.useRef)(new import_react_native14.Animated.Value(SCREEN_HEIGHT3)).current;
4619
- (0, import_react18.useEffect)(() => {
5129
+ (0, import_react18.useLayoutEffect)(() => {
4620
5130
  if (showingPreloadedWebView) {
4621
5131
  slideAnim.setValue(SCREEN_HEIGHT3);
4622
- import_react_native14.Animated.timing(slideAnim, { toValue: 0, duration: 320, easing: import_react_native14.Easing.out(import_react_native14.Easing.cubic), useNativeDriver: true }).start();
4623
- } else {
4624
- slideAnim.setValue(SCREEN_HEIGHT3);
5132
+ import_react_native14.Animated.timing(slideAnim, { toValue: 0, duration: 200, easing: import_react_native14.Easing.out(import_react_native14.Easing.cubic), useNativeDriver: true }).start();
4625
5133
  }
4626
5134
  }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT3]);
4627
5135
  const preloadStyle = [
4628
5136
  styles3.preloadOverlay,
4629
5137
  showingPreloadedWebView ? styles3.visible : styles3.hidden,
4630
- { transform: [{ translateY: showingPreloadedWebView ? slideAnim : SCREEN_HEIGHT3 }] }
5138
+ showingPreloadedWebView ? { transform: [{ translateY: slideAnim }] } : void 0
4631
5139
  ];
4632
5140
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PaywalloContext.Provider, { value: contextValue, children: [
4633
5141
  children,
@@ -4640,6 +5148,7 @@ function PaywalloProvider({ children, config }) {
4640
5148
  primaryProductId: preloadedWebView.primaryProductId,
4641
5149
  secondaryProductId: preloadedWebView.secondaryProductId,
4642
5150
  isPurchasing: isPreloadPurchasing,
5151
+ webUrl: isInitialized ? PaywalloClient.getWebUrl() : null,
4643
5152
  onReady: handlePreloadedWebViewReady,
4644
5153
  onClose: showingPreloadedWebView ? handlePreloadedClose : noOp,
4645
5154
  onPurchase: showingPreloadedWebView ? handlePreloadedPurchase : noOp,
@@ -4662,8 +5171,8 @@ function PaywalloProvider({ children, config }) {
4662
5171
  }
4663
5172
  var styles3 = import_react_native14.StyleSheet.create({
4664
5173
  preloadOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#ffffff" },
4665
- hidden: { zIndex: -1 },
4666
- visible: { zIndex: 9999 }
5174
+ hidden: { zIndex: -1, opacity: 0, width: 1, height: 1, overflow: "hidden" },
5175
+ visible: { zIndex: 9999, opacity: 1 }
4667
5176
  });
4668
5177
 
4669
5178
  // src/utils/productVariableResolver.ts