@virex-tech/paywallo-sdk 1.1.3 → 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);
@@ -971,7 +964,6 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
971
964
  }
972
965
  } catch (err) {
973
966
  const e = err instanceof Error ? err : new PaywallError(PAYWALL_ERROR_CODES.LOAD_FAILED, String(err));
974
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Error loading paywall:", e.message);
975
967
  setError(e);
976
968
  }
977
969
  };
@@ -980,11 +972,96 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
980
972
  return { paywallConfig, products, error };
981
973
  }
982
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
+
983
1055
  // src/domains/paywall/PaywallWebView.tsx
984
1056
  var import_react4 = require("react");
985
- var import_react_native4 = require("react-native");
1057
+ var import_react_native5 = require("react-native");
986
1058
 
987
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
+ }
988
1065
  function parseWebViewMessage(raw) {
989
1066
  try {
990
1067
  const parsed = JSON.parse(raw);
@@ -1008,7 +1085,7 @@ function buildPaywallInjectionScript(data) {
1008
1085
  secondaryProductId: data.secondaryProductId
1009
1086
  }
1010
1087
  };
1011
- 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;`;
1012
1089
  }
1013
1090
  function buildPurchaseStateScript(isPurchasing) {
1014
1091
  return `if(window.receiveNativeMessage){window.receiveNativeMessage({type:"purchaseState",data:{isPurchasing:${isPurchasing}}});}true;`;
@@ -1019,14 +1096,14 @@ var import_jsx_runtime = require("react/jsx-runtime");
1019
1096
  var NativeWebViewComponent = null;
1020
1097
  function getNativeWebView() {
1021
1098
  if (!NativeWebViewComponent) {
1022
- NativeWebViewComponent = (0, import_react_native4.requireNativeComponent)("PaywalloWebView");
1099
+ NativeWebViewComponent = (0, import_react_native5.requireNativeComponent)("PaywalloWebView");
1023
1100
  }
1024
1101
  return NativeWebViewComponent;
1025
1102
  }
1026
1103
  var webViewEmitter = null;
1027
1104
  function getWebViewEmitter() {
1028
1105
  if (!webViewEmitter) {
1029
- webViewEmitter = new import_react_native4.NativeEventEmitter(import_react_native4.NativeModules.PaywalloWebViewModule);
1106
+ webViewEmitter = new import_react_native5.NativeEventEmitter(import_react_native5.NativeModules.PaywalloWebViewModule);
1030
1107
  }
1031
1108
  return webViewEmitter;
1032
1109
  }
@@ -1044,6 +1121,7 @@ function PaywallWebView({
1044
1121
  primaryProductId,
1045
1122
  secondaryProductId,
1046
1123
  isPurchasing,
1124
+ webUrl: webUrlProp,
1047
1125
  onPurchase,
1048
1126
  onClose,
1049
1127
  onRestore,
@@ -1055,8 +1133,39 @@ function PaywallWebView({
1055
1133
  const [scriptToInject, setScriptToInject] = (0, import_react4.useState)(void 0);
1056
1134
  const onErrorRef = (0, import_react4.useRef)(onError);
1057
1135
  onErrorRef.current = onError;
1136
+ const onReadyRef = (0, import_react4.useRef)(onReady);
1137
+ onReadyRef.current = onReady;
1058
1138
  const NativeWebView = (0, import_react4.useMemo)(() => getNativeWebView(), []);
1059
- 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
+ }, []);
1060
1169
  const buildAndSetInjectionScript = (0, import_react4.useCallback)(() => {
1061
1170
  if (!craftData || paywallDataSent.current) return;
1062
1171
  paywallDataSent.current = true;
@@ -1067,19 +1176,21 @@ function PaywallWebView({
1067
1176
  secondaryProductId
1068
1177
  });
1069
1178
  setScriptToInject(script);
1070
- onReady?.();
1071
- }, [craftData, products, primaryProductId, secondaryProductId, onReady]);
1072
- const lastProcessedRef = (0, import_react4.useRef)({ type: "", time: 0 });
1179
+ }, [craftData, products, primaryProductId, secondaryProductId]);
1180
+ const processedIdsRef = (0, import_react4.useRef)(/* @__PURE__ */ new Set());
1073
1181
  const handleParsedMessage = (0, import_react4.useCallback)(
1074
1182
  (message) => {
1075
- const now = Date.now();
1076
- if (message.type === lastProcessedRef.current.type && now - lastProcessedRef.current.time < 500) {
1077
- return;
1078
- }
1079
- 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);
1080
1187
  switch (message.type) {
1081
1188
  case "ready":
1082
- if (!paywallDataSent.current) buildAndSetInjectionScript();
1189
+ if (!paywallDataSent.current) {
1190
+ buildAndSetInjectionScript();
1191
+ } else {
1192
+ onReadyRef.current?.();
1193
+ }
1083
1194
  break;
1084
1195
  case "purchase":
1085
1196
  if (message.payload?.productId) onPurchase(message.payload.productId);
@@ -1094,7 +1205,7 @@ function PaywallWebView({
1094
1205
  if (message.payload?.url) {
1095
1206
  const url = message.payload.url;
1096
1207
  if (/^https?:\/\//i.test(url) || /^mailto:/i.test(url)) {
1097
- void import_react_native4.Linking.openURL(url);
1208
+ void import_react_native5.Linking.openURL(url);
1098
1209
  }
1099
1210
  }
1100
1211
  break;
@@ -1170,8 +1281,9 @@ function PaywallWebView({
1170
1281
  if (!paywallDataSent.current) return;
1171
1282
  setScriptToInject(buildPurchaseStateScript(isPurchasing));
1172
1283
  }, [isPurchasing]);
1173
- const paywallUrl = `${webUrl}/paywall/${paywallId}`;
1174
- 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)(
1175
1287
  NativeWebView,
1176
1288
  {
1177
1289
  sourceUrl: paywallUrl,
@@ -1183,14 +1295,26 @@ function PaywallWebView({
1183
1295
  }
1184
1296
  ) });
1185
1297
  }
1186
- var styles = import_react_native4.StyleSheet.create({
1298
+ var styles = import_react_native5.StyleSheet.create({
1187
1299
  container: { flex: 1 },
1188
1300
  webview: { flex: 1, backgroundColor: "transparent" }
1189
1301
  });
1190
1302
 
1191
1303
  // src/domains/paywall/PaywallModal.tsx
1192
1304
  var import_jsx_runtime2 = require("react/jsx-runtime");
1193
- 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
+ }
1194
1318
  function PaywallModal({
1195
1319
  placement,
1196
1320
  visible,
@@ -1208,7 +1332,13 @@ function PaywallModal({
1208
1332
  variantKey,
1209
1333
  campaignId
1210
1334
  );
1211
- const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId);
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);
1212
1342
  const [webViewError, setWebViewError] = (0, import_react5.useState)(null);
1213
1343
  const [retryCount, setRetryCount] = (0, import_react5.useState)(0);
1214
1344
  const retryCountRef = (0, import_react5.useRef)(0);
@@ -1228,36 +1358,38 @@ function PaywallModal({
1228
1358
  setWebViewError(null);
1229
1359
  setRetryCount(0);
1230
1360
  }, []);
1231
- const openAnim = (0, import_react5.useRef)(new import_react_native5.Animated.Value(SCREEN_HEIGHT2)).current;
1232
1361
  (0, import_react5.useEffect)(() => {
1233
1362
  if (visible) {
1363
+ setModalVisible(true);
1234
1364
  openAnim.setValue(SCREEN_HEIGHT2);
1235
- import_react_native5.Animated.timing(openAnim, {
1365
+ import_react_native6.Animated.timing(openAnim, {
1236
1366
  toValue: 0,
1237
1367
  duration: 550,
1238
- easing: import_react_native5.Easing.out(import_react_native5.Easing.cubic),
1368
+ easing: import_react_native6.Easing.out(import_react_native6.Easing.cubic),
1239
1369
  useNativeDriver: true
1240
1370
  }).start();
1241
1371
  }
1242
1372
  }, [visible, openAnim]);
1243
- if (!visible) return null;
1373
+ if (!modalVisible && !visible) return null;
1244
1374
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1245
- import_react_native5.Modal,
1375
+ import_react_native6.Modal,
1246
1376
  {
1247
- visible,
1377
+ visible: modalVisible,
1248
1378
  animationType: "none",
1249
1379
  presentationStyle: "overFullScreen",
1250
1380
  onRequestClose: handleClose,
1251
1381
  transparent: true,
1252
1382
  statusBarTranslucent: true,
1253
- 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: [
1254
- 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 }) }),
1255
- !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: webViewError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.View, { style: styles2.fallbackContainer, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native5.View, { style: styles2.fallbackCard, children: [
1256
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Text, { style: styles2.fallbackTitle, children: "Algo deu errado" }),
1257
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Text, { style: styles2.fallbackDescription, children: webViewError.description }),
1258
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Pressable, { style: styles2.retryButton, onPress: handleRetry, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Text, { style: styles2.retryButtonText, children: "Tentar novamente" }) }),
1259
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Pressable, { style: styles2.closeButton, onPress: handleClose, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Text, { style: styles2.closeButtonText, children: "Fechar" }) })
1260
- ] }) }) : /* @__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)(
1261
1393
  PaywallWebView,
1262
1394
  {
1263
1395
  paywallId: paywallConfig.id,
@@ -1277,7 +1409,7 @@ function PaywallModal({
1277
1409
  }
1278
1410
  );
1279
1411
  }
1280
- var styles2 = import_react_native5.StyleSheet.create({
1412
+ var styles2 = import_react_native6.StyleSheet.create({
1281
1413
  overlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.3)" },
1282
1414
  animatedContainer: { flex: 1, backgroundColor: "#fff" },
1283
1415
  container: { flex: 1, backgroundColor: "#fff" },
@@ -1341,8 +1473,7 @@ var PaywallErrorBoundary = class extends import_react6.Component {
1341
1473
  static getDerivedStateFromError() {
1342
1474
  return { hasError: true };
1343
1475
  }
1344
- componentDidCatch(error) {
1345
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Paywall render error:", error.message);
1476
+ componentDidCatch(_error) {
1346
1477
  }
1347
1478
  render() {
1348
1479
  return this.state.hasError ? null : this.props.children;
@@ -1365,7 +1496,7 @@ function usePaywallContext() {
1365
1496
 
1366
1497
  // src/domains/paywall/usePaywallPresenter.ts
1367
1498
  var import_react8 = require("react");
1368
- function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView) {
1499
+ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
1369
1500
  const [showingPreloadedWebView, setShowingPreloadedWebView] = (0, import_react8.useState)(false);
1370
1501
  const resolverRef = (0, import_react8.useRef)(null);
1371
1502
  const presentedAtRef = (0, import_react8.useRef)(Date.now());
@@ -1414,9 +1545,8 @@ function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreload
1414
1545
  };
1415
1546
  void track();
1416
1547
  setShowingPreloadedWebView(false);
1417
- clearPreloadedWebView();
1418
1548
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: true, restored: false });
1419
- }, [preloadedWebView, clearPreloadedWebView, resolvePreloadedPaywall]);
1549
+ }, [preloadedWebView, resolvePreloadedPaywall]);
1420
1550
  return { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef };
1421
1551
  }
1422
1552
 
@@ -1623,10 +1753,7 @@ var AffiliateManager = class {
1623
1753
  );
1624
1754
  }
1625
1755
  }
1626
- log(...args) {
1627
- if (this.debug) {
1628
- console.warn("[AffiliateManager]", ...args);
1629
- }
1756
+ log(..._args) {
1630
1757
  }
1631
1758
  };
1632
1759
  var affiliateManager = new AffiliateManager();
@@ -1644,26 +1771,42 @@ var PaywalloAffiliate = {
1644
1771
  };
1645
1772
 
1646
1773
  // src/domains/subscription/SubscriptionCache.ts
1647
- var CACHE_KEY = "subscription_cache";
1774
+ var LEGACY_CACHE_KEY = "subscription_cache";
1775
+ var CACHE_KEY_PREFIX = "subscription_cache:";
1648
1776
  var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
1777
+ function keyFor(distinctId) {
1778
+ return `${CACHE_KEY_PREFIX}${distinctId}`;
1779
+ }
1649
1780
  var SubscriptionCache = class {
1650
1781
  constructor(ttl = DEFAULT_TTL) {
1651
- this.memoryCache = null;
1782
+ this.memoryCache = /* @__PURE__ */ new Map();
1783
+ this.legacyCleanupDone = false;
1652
1784
  this.ttl = ttl;
1653
1785
  this.storage = new SecureStorage();
1654
1786
  }
1655
- async get() {
1656
- if (this.memoryCache && !this.isExpired(this.memoryCache)) {
1657
- return this.memoryCache;
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;
1658
1801
  }
1659
1802
  try {
1660
- const stored = await this.storage.get(CACHE_KEY);
1803
+ const stored = await this.storage.get(keyFor(distinctId));
1661
1804
  if (!stored) {
1662
1805
  return null;
1663
1806
  }
1664
1807
  const parsed = JSON.parse(stored);
1665
1808
  if (typeof parsed !== "object" || parsed === null || !("data" in parsed) || typeof parsed.data !== "object" || parsed.data === null || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") {
1666
- await this.storage.remove(CACHE_KEY);
1809
+ await this.storage.remove(keyFor(distinctId));
1667
1810
  return null;
1668
1811
  }
1669
1812
  const cached = parsed;
@@ -1692,32 +1835,39 @@ var SubscriptionCache = class {
1692
1835
  }
1693
1836
  cached.isStale = this.isExpired(cached);
1694
1837
  if (!cached.isStale) {
1695
- this.memoryCache = cached;
1838
+ this.memoryCache.set(distinctId, cached);
1696
1839
  }
1697
1840
  return cached;
1698
1841
  } catch {
1699
1842
  return null;
1700
1843
  }
1701
1844
  }
1702
- async set(data) {
1845
+ async set(distinctId, data) {
1846
+ if (!distinctId) return;
1703
1847
  const cached = {
1704
1848
  data,
1705
1849
  cachedAt: Date.now(),
1706
1850
  isStale: false
1707
1851
  };
1708
- this.memoryCache = cached;
1852
+ this.memoryCache.set(distinctId, cached);
1709
1853
  try {
1710
- await this.storage.set(CACHE_KEY, JSON.stringify(cached));
1711
- } catch (error) {
1712
- if (__DEV__) console.warn("[SubscriptionCache] set() failed:", error instanceof Error ? error.message : String(error));
1854
+ await this.storage.set(keyFor(distinctId), JSON.stringify(cached));
1855
+ } catch {
1713
1856
  }
1714
1857
  }
1715
- async invalidate() {
1716
- this.memoryCache = null;
1858
+ async invalidate(distinctId) {
1859
+ if (!distinctId) return;
1860
+ this.memoryCache.delete(distinctId);
1717
1861
  try {
1718
- await this.storage.remove(CACHE_KEY);
1719
- } catch (error) {
1720
- if (__DEV__) console.warn("[SubscriptionCache] invalidate() failed:", error instanceof Error ? error.message : String(error));
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 {
1721
1871
  }
1722
1872
  }
1723
1873
  isExpired(cached) {
@@ -1729,9 +1879,156 @@ var SubscriptionCache = class {
1729
1879
  };
1730
1880
  var subscriptionCache = new SubscriptionCache();
1731
1881
 
1732
- // src/core/ApiClient.ts
1882
+ // src/domains/subscription/SubscriptionManager.ts
1733
1883
  var import_react_native7 = require("react-native");
1734
1884
 
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);
1914
+ }
1915
+ }
1916
+ setUserId(userId) {
1917
+ if (this.userId !== userId) {
1918
+ this.userId = userId;
1919
+ void this.cache.invalidateAll();
1920
+ }
1921
+ }
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
+
1735
2032
  // src/core/apiUrlUtils.ts
1736
2033
  var DEFAULT_WEB_URL = "https://paywallo.com.br";
1737
2034
  function deriveWebUrl(baseUrl) {
@@ -1962,10 +2259,7 @@ var HttpClient = class {
1962
2259
  sleep(ms) {
1963
2260
  return new Promise((resolve) => setTimeout(resolve, ms));
1964
2261
  }
1965
- log(message, data) {
1966
- if (this.config.debug) {
1967
- console.warn("[HttpClient]", message, data ?? "");
1968
- }
2262
+ log(..._args) {
1969
2263
  }
1970
2264
  setDebug(debug) {
1971
2265
  this.config.debug = debug;
@@ -1976,7 +2270,7 @@ var HttpClient = class {
1976
2270
  };
1977
2271
 
1978
2272
  // src/core/network/NetworkMonitor.ts
1979
- var import_react_native6 = require("react-native");
2273
+ var import_react_native8 = require("react-native");
1980
2274
 
1981
2275
  // src/core/network/types.ts
1982
2276
  var DEFAULT_NETWORK_CONFIG = {
@@ -2022,7 +2316,7 @@ var NetworkMonitorClass = class {
2022
2316
  }, this.config.checkInterval);
2023
2317
  }
2024
2318
  setupAppStateListener() {
2025
- this.appStateSubscription = import_react_native6.AppState.addEventListener("change", (state) => {
2319
+ this.appStateSubscription = import_react_native8.AppState.addEventListener("change", (state) => {
2026
2320
  if (state === "active") {
2027
2321
  void this.checkConnectivity();
2028
2322
  }
@@ -2109,10 +2403,7 @@ var NetworkMonitorClass = class {
2109
2403
  this.initialized = false;
2110
2404
  this.currentState = "unknown";
2111
2405
  }
2112
- log(message, data) {
2113
- if (this.config.debug) {
2114
- console.warn("[NetworkMonitor]", message, data ?? "");
2115
- }
2406
+ log(..._args) {
2116
2407
  }
2117
2408
  setDebug(debug) {
2118
2409
  this.config.debug = debug;
@@ -2142,6 +2433,8 @@ function normalizeForComparison(obj) {
2142
2433
  return "{" + pairs.join(",") + "}";
2143
2434
  }
2144
2435
  function findDuplicateIndex(queue, item) {
2436
+ const byId = queue.findIndex((existing) => existing.id === item.id);
2437
+ if (byId !== -1) return byId;
2145
2438
  const normalizedBody = normalizeForComparison(item.body);
2146
2439
  return queue.findIndex(
2147
2440
  (existing) => existing.url === item.url && normalizeForComparison(existing.body) === normalizedBody
@@ -2210,9 +2503,9 @@ var OfflineQueueClass = class {
2210
2503
  void this.saveToStorage();
2211
2504
  }
2212
2505
  }
2213
- async enqueue(method, url, body, headers) {
2506
+ async enqueue(method, url, body, headers, id) {
2214
2507
  const item = {
2215
- id: generateUUID(),
2508
+ id: id ?? generateUUID(),
2216
2509
  method,
2217
2510
  url,
2218
2511
  body,
@@ -2296,7 +2589,6 @@ var OfflineQueueClass = class {
2296
2589
  this.emit({ type: "processing:end", queueSize: this.queue.length });
2297
2590
  return { processed, failed };
2298
2591
  }
2299
- /** Returns true if the item was permanently removed (counted as failed). */
2300
2592
  async handleItemFailure(item, error, permanent = false) {
2301
2593
  if (permanent || item.attempts >= item.maxAttempts) {
2302
2594
  await this.removeItem(item.id);
@@ -2358,10 +2650,7 @@ var OfflineQueueClass = class {
2358
2650
  }
2359
2651
  }
2360
2652
  }
2361
- log(message, data) {
2362
- if (this.config.debug) {
2363
- console.warn("[OfflineQueue]", message, data ?? "");
2364
- }
2653
+ log(..._args) {
2365
2654
  }
2366
2655
  setDebug(debug) {
2367
2656
  this.config.debug = debug;
@@ -2503,10 +2792,7 @@ var QueueProcessorClass = class {
2503
2792
  this.httpClient = null;
2504
2793
  this.initialized = false;
2505
2794
  }
2506
- log(message, data) {
2507
- if (this.config.debug) {
2508
- console.warn("[QueueProcessor]", message, data ?? "");
2509
- }
2795
+ log(..._args) {
2510
2796
  }
2511
2797
  setDebug(debug) {
2512
2798
  this.config.debug = debug;
@@ -2518,10 +2804,9 @@ var queueProcessor = new QueueProcessorClass();
2518
2804
  var ApiCache = class {
2519
2805
  constructor() {
2520
2806
  this.CACHE_TTL = 3e5;
2521
- // 5 minutes
2522
2807
  this.CACHE_NULL_TTL = 3e4;
2523
- // 30 seconds — for null/404 variants
2524
2808
  this.MAX_SIZE = 200;
2809
+ this.STALE_WINDOW_MS = 864e5;
2525
2810
  this.variantCache = /* @__PURE__ */ new Map();
2526
2811
  this.paywallCache = /* @__PURE__ */ new Map();
2527
2812
  this.campaignCache = /* @__PURE__ */ new Map();
@@ -2559,7 +2844,7 @@ var ApiCache = class {
2559
2844
  }
2560
2845
  getStaleVariant(key) {
2561
2846
  const entry = this.variantCache.get(key);
2562
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2847
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2563
2848
  return void 0;
2564
2849
  }
2565
2850
  getPaywall(key) {
@@ -2571,19 +2856,19 @@ var ApiCache = class {
2571
2856
  }
2572
2857
  getStalePaywall(key) {
2573
2858
  const entry = this.paywallCache.get(key);
2574
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2859
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2575
2860
  return void 0;
2576
2861
  }
2577
2862
  getCampaign(key) {
2578
2863
  return this.getCached(this.campaignCache, key);
2579
2864
  }
2580
- setCampaign(key, value) {
2581
- this.setCached(this.campaignCache, key, value);
2865
+ setCampaign(key, value, ttlOverride) {
2866
+ this.setCached(this.campaignCache, key, value, ttlOverride);
2582
2867
  this.evictExpiredEntries(this.campaignCache);
2583
2868
  }
2584
2869
  getStaleCampaign(key) {
2585
2870
  const entry = this.campaignCache.get(key);
2586
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2871
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2587
2872
  return void 0;
2588
2873
  }
2589
2874
  };
@@ -2602,11 +2887,12 @@ var _ApiClient = class _ApiClient {
2602
2887
  this.offlineQueueEnabled = offlineQueueEnabled;
2603
2888
  this.httpClient = new HttpClient(this.baseUrl, { timeout, debug });
2604
2889
  }
2605
- // ─── Infrastructure ──────────────────────────────────────────────────────────
2606
- /** Exposed only for QueueProcessor initialization. Prefer get()/post() for requests. */
2607
2890
  getHttpClient() {
2608
2891
  return this.httpClient;
2609
2892
  }
2893
+ getBaseUrl() {
2894
+ return this.baseUrl;
2895
+ }
2610
2896
  getWebUrl() {
2611
2897
  return this.webUrl;
2612
2898
  }
@@ -2632,7 +2918,6 @@ var _ApiClient = class _ApiClient {
2632
2918
  async clearQueue() {
2633
2919
  await offlineQueue.clear();
2634
2920
  }
2635
- // ─── Public HTTP facade (headers injected automatically) ─────────────────────
2636
2921
  async get(path) {
2637
2922
  return this.httpClient.get(path, { headers: { "X-App-Key": this.appKey } });
2638
2923
  }
@@ -2646,29 +2931,28 @@ var _ApiClient = class _ApiClient {
2646
2931
  message,
2647
2932
  context,
2648
2933
  sdkVersion: SDK_VERSION,
2649
- platform: import_react_native7.Platform.OS
2934
+ platform: import_react_native9.Platform.OS
2650
2935
  }, true);
2651
2936
  } catch {
2652
2937
  }
2653
2938
  }
2654
- // ─── Business methods ────────────────────────────────────────────────────────
2655
2939
  async trackEvent(eventName, distinctId, properties, timestamp) {
2656
2940
  await this.postWithQueue("/api/v1/events", {
2657
2941
  eventName,
2658
2942
  distinctId,
2659
- properties: { ...properties, platform: import_react_native7.Platform.OS },
2943
+ properties: { ...properties, platform: import_react_native9.Platform.OS },
2660
2944
  timestamp: timestamp ?? Date.now(),
2661
2945
  environment: this.environment.toLowerCase()
2662
2946
  }, `event:${eventName}`);
2663
2947
  }
2664
- async identify(distinctId, properties, email) {
2665
- 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");
2666
2950
  }
2667
2951
  async startSession(distinctId, sessionId, platform) {
2668
2952
  await this.postWithQueue("/api/v1/sessions/start", {
2669
2953
  distinctId,
2670
2954
  sessionId,
2671
- platform: platform ?? import_react_native7.Platform.OS,
2955
+ platform: platform ?? import_react_native9.Platform.OS,
2672
2956
  environment: this.environment.toLowerCase()
2673
2957
  }, `session.start:${sessionId}`);
2674
2958
  return { success: true };
@@ -2746,7 +3030,7 @@ var _ApiClient = class _ApiClient {
2746
3030
  const result = await getCampaignData(this, this.baseUrl, placement, distinctId, context);
2747
3031
  if (!result) {
2748
3032
  this.log("No campaign found for placement:", placement);
2749
- this.cache.setCampaign(key, null);
3033
+ this.cache.setCampaign(key, null, this.cache.nullTTL);
2750
3034
  return null;
2751
3035
  }
2752
3036
  this.log("Got campaign:", placement, result.variantKey);
@@ -2759,12 +3043,38 @@ var _ApiClient = class _ApiClient {
2759
3043
  return null;
2760
3044
  }
2761
3045
  }
2762
- async validatePurchase(platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
2763
- const result = await validatePurchase(
2764
- this,
2765
- this.appKey,
2766
- platform,
2767
- receipt,
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
+ }
3072
+ async validatePurchase(platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
3073
+ const result = await validatePurchase(
3074
+ this,
3075
+ this.appKey,
3076
+ platform,
3077
+ receipt,
2768
3078
  productId,
2769
3079
  transactionId,
2770
3080
  priceLocal,
@@ -2827,12 +3137,6 @@ var _ApiClient = class _ApiClient {
2827
3137
  return { value: false, flagKey };
2828
3138
  }
2829
3139
  }
2830
- // ─── Cache-only reads ────────────────────────────────────────────────────────
2831
- /**
2832
- * Reads a flag variant from cache layers only — no network call.
2833
- * Checks in-memory cache first, then SecureStorage.
2834
- * Returns null when both layers miss.
2835
- */
2836
3140
  async getVariantFromCache(flagKey, distinctId) {
2837
3141
  const key = `${flagKey}:${distinctId}`;
2838
3142
  const hit = this.cache.getVariant(key);
@@ -2864,10 +3168,6 @@ var _ApiClient = class _ApiClient {
2864
3168
  } catch {
2865
3169
  }
2866
3170
  }
2867
- /**
2868
- * POST with offline queue fallback. Enqueues when offline or on network failure.
2869
- * Throws only when offlineQueueEnabled is false and the request fails.
2870
- */
2871
3171
  async postWithQueue(url, payload, label) {
2872
3172
  if (this.offlineQueueEnabled && !networkMonitor.isOnline()) {
2873
3173
  await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
@@ -2886,11 +3186,9 @@ var _ApiClient = class _ApiClient {
2886
3186
  throw error;
2887
3187
  }
2888
3188
  }
2889
- log(...args) {
2890
- if (this.debug) console.warn("[Paywallo API]", ...args);
3189
+ log(..._args) {
2891
3190
  }
2892
3191
  };
2893
- // ─── Private helpers ─────────────────────────────────────────────────────────
2894
3192
  _ApiClient.FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2895
3193
  var ApiClient = _ApiClient;
2896
3194
 
@@ -2911,42 +3209,72 @@ var CAMPAIGN_ERROR_CODES = {
2911
3209
 
2912
3210
  // src/domains/campaign/CampaignGateService.ts
2913
3211
  var PRELOAD_CACHE_TTL_MS = 5 * 60 * 1e3;
3212
+ var PRELOAD_WAIT_MAX_MS = 2e3;
3213
+ var PRELOAD_WAIT_INTERVAL_MS = 100;
2914
3214
  var CampaignGateService = class {
2915
3215
  constructor() {
2916
3216
  this.preloadedCampaigns = /* @__PURE__ */ new Map();
3217
+ this.pendingPreloads = /* @__PURE__ */ new Set();
2917
3218
  }
2918
3219
  async getCampaign(apiClient, placement, context) {
2919
3220
  const distinctId = identityManager.getDistinctId();
2920
3221
  if (!distinctId) return null;
2921
3222
  return apiClient.getCampaign(placement, distinctId, context);
2922
3223
  }
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
+ }
2923
3238
  async presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2924
- const campaign = await this.getCampaign(apiClient, placement, context);
3239
+ await this.waitForPreload(placement);
3240
+ const preloaded = this.getPreloadedCampaign(placement);
3241
+ const campaign = preloaded ?? await this.getCampaign(apiClient, placement, context);
2925
3242
  if (!campaign) {
2926
3243
  return { presented: false, purchased: false, cancelled: false, restored: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, "No campaign found") };
2927
3244
  }
2928
- if (!context?.forceShow && await hasActive()) {
2929
- return { presented: false, purchased: false, cancelled: false, restored: false, skippedReason: "subscriber" };
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
+ }
2930
3250
  }
2931
3251
  if (campaignPresenter) return campaignPresenter(campaign);
2932
3252
  if (!paywallPresenter) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
2933
- return { ...await paywallPresenter(campaign.paywall.placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
3253
+ return { ...await paywallPresenter(placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
2934
3254
  }
2935
3255
  async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2936
- if (await hasActive()) return true;
3256
+ const isActive = await hasActive();
3257
+ if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo] requireSubscription \u2014 hasActive: ${isActive}`);
3258
+ if (isActive) return true;
2937
3259
  const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context);
2938
3260
  return r.purchased || r.restored;
2939
3261
  }
2940
3262
  async preloadCampaign(apiClient, placement, context) {
3263
+ this.pendingPreloads.add(placement);
2941
3264
  try {
2942
3265
  const campaign = await this.getCampaign(apiClient, placement, context);
2943
3266
  if (!campaign?.paywall) {
2944
3267
  return { success: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, `Campaign or paywall not found: ${placement}`) };
2945
3268
  }
2946
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
+ }
2947
3273
  return { success: true };
2948
3274
  } catch (error) {
2949
3275
  return { success: false, error: error instanceof Error ? error : new PaywalloError("panel", "PANEL_UNKNOWN", String(error)) };
3276
+ } finally {
3277
+ this.pendingPreloads.delete(placement);
2950
3278
  }
2951
3279
  }
2952
3280
  getPreloadedCampaign(placement) {
@@ -2960,12 +3288,13 @@ var CampaignGateService = class {
2960
3288
  }
2961
3289
  clearPreloaded() {
2962
3290
  this.preloadedCampaigns.clear();
3291
+ this.pendingPreloads.clear();
2963
3292
  }
2964
3293
  };
2965
3294
  var campaignGateService = new CampaignGateService();
2966
3295
 
2967
3296
  // src/domains/identity/AdvertisingIdManager.ts
2968
- var import_react_native8 = require("react-native");
3297
+ var import_react_native10 = require("react-native");
2969
3298
  var ZERO_IDFA = "00000000-0000-0000-0000-000000000000";
2970
3299
  var AdvertisingIdManager = class {
2971
3300
  constructor() {
@@ -2976,7 +3305,7 @@ var AdvertisingIdManager = class {
2976
3305
  const fallback = { idfv: null, idfa: null, gaid: null, attStatus: "unavailable" };
2977
3306
  try {
2978
3307
  const tracking = await import("expo-tracking-transparency");
2979
- if (import_react_native8.Platform.OS === "ios") {
3308
+ if (import_react_native10.Platform.OS === "ios") {
2980
3309
  if (!tracking.isAvailable()) {
2981
3310
  const idfv2 = await this.collectIdfv();
2982
3311
  this.cached = { ...fallback, idfv: idfv2 };
@@ -2995,7 +3324,7 @@ var AdvertisingIdManager = class {
2995
3324
  this.cached = { idfv, idfa, gaid: null, attStatus: status };
2996
3325
  return this.cached;
2997
3326
  }
2998
- if (import_react_native8.Platform.OS === "android") {
3327
+ if (import_react_native10.Platform.OS === "android") {
2999
3328
  const androidStatus = await tracking.getTrackingPermissionsAsync();
3000
3329
  const optedIn = androidStatus.granted;
3001
3330
  const rawGaid = optedIn ? tracking.getAdvertisingId() : null;
@@ -3054,14 +3383,14 @@ var FacebookIdManager = class {
3054
3383
  var facebookIdManager = new FacebookIdManager();
3055
3384
 
3056
3385
  // src/domains/identity/InstallTracker.ts
3057
- var import_react_native11 = require("react-native");
3386
+ var import_react_native13 = require("react-native");
3058
3387
 
3059
3388
  // src/domains/identity/InstallReferrerManager.ts
3060
- var import_react_native9 = require("react-native");
3389
+ var import_react_native11 = require("react-native");
3061
3390
  var REFERRER_TIMEOUT_MS = 5e3;
3062
3391
  var InstallReferrerManager = class {
3063
3392
  async getReferrer() {
3064
- if (import_react_native9.Platform.OS !== "android") {
3393
+ if (import_react_native11.Platform.OS !== "android") {
3065
3394
  return this.emptyResult();
3066
3395
  }
3067
3396
  let timerId;
@@ -3121,21 +3450,7 @@ var InstallReferrerManager = class {
3121
3450
  var installReferrerManager = new InstallReferrerManager();
3122
3451
 
3123
3452
  // src/domains/session/SessionManager.ts
3124
- var import_react_native10 = require("react-native");
3125
-
3126
- // src/domains/session/SessionError.ts
3127
- var SessionError = class extends PaywalloError {
3128
- constructor(code, message) {
3129
- super("session", code, message);
3130
- this.name = "SessionError";
3131
- }
3132
- };
3133
- var SESSION_ERROR_CODES = {
3134
- NOT_INITIALIZED: "SESSION_NOT_INITIALIZED",
3135
- START_FAILED: "SESSION_START_FAILED",
3136
- END_FAILED: "SESSION_END_FAILED",
3137
- RESTORE_FAILED: "SESSION_RESTORE_FAILED"
3138
- };
3453
+ var import_react_native12 = require("react-native");
3139
3454
 
3140
3455
  // src/domains/session/sessionTracking.ts
3141
3456
  async function trackSessionStart(apiClient, sessionId, sessionStartedAt) {
@@ -3303,7 +3618,7 @@ var SessionManager = class {
3303
3618
  if (this.appStateSubscription) {
3304
3619
  this.appStateSubscription.remove();
3305
3620
  }
3306
- this.appStateSubscription = import_react_native10.AppState.addEventListener("change", this.handleAppStateChange);
3621
+ this.appStateSubscription = import_react_native12.AppState.addEventListener("change", this.handleAppStateChange);
3307
3622
  }
3308
3623
  async handleForeground() {
3309
3624
  if (this.backgroundTimestamp) {
@@ -3364,10 +3679,7 @@ var SessionManager = class {
3364
3679
  );
3365
3680
  }
3366
3681
  }
3367
- log(...args) {
3368
- if (this.debug) {
3369
- console.warn("[Paywallo Session]", ...args);
3370
- }
3682
+ log(..._args) {
3371
3683
  }
3372
3684
  };
3373
3685
  var sessionManager = new SessionManager();
@@ -3402,7 +3714,7 @@ var InstallTracker = class {
3402
3714
  }
3403
3715
  let deviceData = {};
3404
3716
  try {
3405
- const screen = import_react_native11.Dimensions.get("screen");
3717
+ const screen = import_react_native13.Dimensions.get("screen");
3406
3718
  const screenWidth = Math.round(screen.width ?? 0);
3407
3719
  const screenHeight = Math.round(screen.height ?? 0);
3408
3720
  const locale = Intl.DateTimeFormat().resolvedOptions().locale ?? "unknown";
@@ -3422,9 +3734,6 @@ var InstallTracker = class {
3422
3734
  locale
3423
3735
  };
3424
3736
  } catch {
3425
- if (this.debug) {
3426
- console.warn("[Paywallo] Failed to collect device data for install event");
3427
- }
3428
3737
  }
3429
3738
  const [fbAnonId, referrer, adIds] = await Promise.all([
3430
3739
  facebookIdManager.getAnonId(),
@@ -3434,7 +3743,7 @@ var InstallTracker = class {
3434
3743
  try {
3435
3744
  await apiClient.trackEvent("$app_installed", distinctId, {
3436
3745
  installedAt,
3437
- platform: import_react_native11.Platform.OS,
3746
+ platform: import_react_native13.Platform.OS,
3438
3747
  ...sessionId && { sessionId },
3439
3748
  ...deviceData,
3440
3749
  ...fbAnonId && { fbAnonId },
@@ -3455,13 +3764,7 @@ var InstallTracker = class {
3455
3764
  await AsyncStorage3.setItem("@paywallo:install_tracked", String(installedAt));
3456
3765
  } catch {
3457
3766
  }
3458
- if (this.debug && !stored) {
3459
- console.warn("[Paywallo] Install event tracked but flag storage failed \u2014 may re-track on next launch");
3460
- }
3461
- } catch (error) {
3462
- if (this.debug) {
3463
- console.warn("[Paywallo] Failed to track install:", error);
3464
- }
3767
+ } catch {
3465
3768
  }
3466
3769
  }
3467
3770
  };
@@ -3471,25 +3774,13 @@ function isValidEventName(name) {
3471
3774
  if (name.startsWith("$")) return /^\$[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3472
3775
  return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3473
3776
  }
3474
- var OFFLINE_ACTIVE_STATUSES = ["active", "grace_period"];
3475
- async function resolveClientOfflineFromCache() {
3476
- try {
3477
- const cached = await subscriptionCache.get();
3478
- if (!cached) return false;
3479
- const sub = cached.data.subscription;
3480
- if (!sub) return false;
3481
- if (!OFFLINE_ACTIVE_STATUSES.includes(sub.status)) return false;
3482
- if (!sub.expiresAt) return true;
3483
- return sub.expiresAt > /* @__PURE__ */ new Date();
3484
- } catch {
3485
- return false;
3486
- }
3487
- }
3488
3777
  var _PaywalloClientClass = class _PaywalloClientClass {
3489
3778
  constructor() {
3490
3779
  this.config = null;
3491
3780
  this.apiClient = null;
3492
3781
  this.initPromise = null;
3782
+ this.readyPromise = null;
3783
+ this.resolveReady = null;
3493
3784
  this.paywallPresenter = null;
3494
3785
  this.campaignPresenter = null;
3495
3786
  this.subscriptionGetter = null;
@@ -3499,29 +3790,30 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3499
3790
  this.pendingConfig = null;
3500
3791
  this.networkCleanup = null;
3501
3792
  this.initAttempts = 0;
3793
+ this.autoPreloadedPlacement = null;
3794
+ this.autoPreloadPlacementPromise = null;
3795
+ this.sessionFlagsMap = /* @__PURE__ */ new Map();
3502
3796
  }
3503
3797
  async init(config) {
3504
- if (config.debug) 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
+ }
3505
3803
  if (this.config && this.apiClient) {
3506
- if (config.debug) console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3507
3804
  return;
3508
3805
  }
3509
3806
  if (this.initPromise) {
3510
- if (config.debug) console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3511
3807
  await this.initPromise;
3512
- if (config.debug) console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3513
3808
  return;
3514
3809
  }
3515
3810
  if (!config.appKey) throw new ClientError(CLIENT_ERROR_CODES.MISSING_APP_KEY, "appKey is required");
3516
- if (config.debug) console.warn("[Paywallo INIT]", "Starting fresh init...");
3517
3811
  this.pendingConfig = config;
3518
3812
  this.initAttempts = 0;
3519
3813
  this.initPromise = this.initWithRetry(config);
3520
3814
  try {
3521
3815
  await this.initPromise;
3522
- if (config.debug) console.warn("[Paywallo INIT]", "init() SUCCESS");
3523
3816
  } catch (error) {
3524
- if (config.debug) console.warn("[Paywallo INIT]", "init() FAILED:", error instanceof Error ? error.message : String(error));
3525
3817
  this.initPromise = null;
3526
3818
  this.config = null;
3527
3819
  this.apiClient = null;
@@ -3530,6 +3822,18 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3530
3822
  throw error;
3531
3823
  }
3532
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
+ }
3533
3837
  async initWithRetry(config) {
3534
3838
  while (this.initAttempts <= _PaywalloClientClass.MAX_INIT_RETRIES) {
3535
3839
  try {
@@ -3544,7 +3848,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3544
3848
  this.initAttempts++;
3545
3849
  if (this.initAttempts > _PaywalloClientClass.MAX_INIT_RETRIES) throw error;
3546
3850
  const delay = _PaywalloClientClass.RETRY_DELAYS[this.initAttempts - 1] ?? 1e4;
3547
- if (config.debug) console.warn("[Paywallo]", `Init attempt ${this.initAttempts} failed, retrying in ${delay}ms`, error);
3548
3851
  await new Promise((resolve) => setTimeout(resolve, delay));
3549
3852
  }
3550
3853
  }
@@ -3555,11 +3858,8 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3555
3858
  this.networkCleanup = networkMonitor.addListener((state) => {
3556
3859
  if (state !== "online") return;
3557
3860
  if (this.config && this.apiClient) return;
3558
- if (config.debug) console.warn("[Paywallo]", "Network recovered \u2014 retrying init");
3559
3861
  this.initPromise = this.initWithRetry(config);
3560
- this.initPromise.then(() => {
3561
- if (config.debug) console.warn("[Paywallo]", "Init succeeded after network recovery");
3562
- }).catch(() => {
3862
+ this.initPromise.catch(() => {
3563
3863
  this.reportInitFailure(config, null, "NETWORK_RECOVERY_FAILED");
3564
3864
  });
3565
3865
  });
@@ -3588,38 +3888,79 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3588
3888
  } catch {
3589
3889
  }
3590
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
+ }
3591
3904
  async doInit(config) {
3592
3905
  const environment = config.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "Sandbox" : "Production");
3593
3906
  const offlineQueueEnabled = config.offlineQueueEnabled !== false;
3594
- if (config.debug) console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3595
3907
  await Promise.all([
3596
3908
  networkMonitor.initialize({ debug: config.debug }),
3597
3909
  offlineQueue.initialize({ debug: config.debug }).then(
3598
3910
  () => offlineQueue.clearItemsWithInvalidAppKey(config.appKey)
3599
3911
  )
3600
3912
  ]);
3601
- if (config.debug) console.warn("[Paywallo INIT]", "Step 1 DONE");
3602
- if (config.debug) console.warn("[Paywallo INIT]", "Step 2: ApiClient + queueProcessor + affiliate...");
3603
3913
  this.apiClient = new ApiClient(config.appKey, config.debug, environment, offlineQueueEnabled, config.timeout);
3604
3914
  queueProcessor.initialize(this.apiClient.getHttpClient(), { debug: config.debug });
3605
3915
  affiliateManager.initialize(this.apiClient, config.debug);
3606
- if (config.debug) console.warn("[Paywallo INIT]", "Step 2 DONE");
3607
- if (config.debug) 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
+ });
3608
3922
  await Promise.all([
3609
3923
  identityManager.initialize(this.apiClient, config.debug),
3610
3924
  sessionManager.initialize(this.apiClient, config.sessionConfig, config.debug)
3611
3925
  ]);
3612
3926
  const distinctId = identityManager.getDistinctId();
3613
- if (config.debug) console.warn("[Paywallo INIT]", "Step 3 DONE \u2014 distinctId:", distinctId ? distinctId.substring(0, 8) + "..." : "EMPTY");
3614
3927
  if (config.autoStartSession !== false) {
3615
3928
  sessionManager.startSession().catch(() => {
3616
- if (config.debug) console.warn("[Paywallo]", "Session start failed during init \u2014 continuing");
3617
3929
  });
3618
3930
  }
3619
3931
  new InstallTracker(config.debug).trackIfNeeded(this.apiClient, config.requestATT === true).catch(() => {
3620
3932
  });
3933
+ if (config.sessionFlags && config.sessionFlags.length > 0) {
3934
+ await this.resolveSessionFlags(config.sessionFlags, this.apiClient, distinctId);
3935
+ }
3621
3936
  this.config = config;
3622
- if (config.debug) 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
+ }
3623
3964
  }
3624
3965
  async identify(options) {
3625
3966
  this.ensureInitialized();
@@ -3629,7 +3970,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3629
3970
  const apiClient = this.getApiClientOrThrow();
3630
3971
  const distinctId = identityManager.getDistinctId();
3631
3972
  if (!distinctId) {
3632
- if (this.config?.debug) console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3633
3973
  return;
3634
3974
  }
3635
3975
  if (!isValidEventName(eventName)) {
@@ -3637,25 +3977,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3637
3977
  throw new ClientError(CLIENT_ERROR_CODES.INVALID_EVENT_NAME, msg);
3638
3978
  }
3639
3979
  const sessionId = sessionManager.getSessionId();
3640
- if (eventName === "$purchase_completed") {
3641
- if (this.config?.debug) console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3642
- distinctId: distinctId.substring(0, 8) + "...",
3643
- productId: options?.properties?.productId,
3644
- priceUsd: options?.properties?.priceUsd,
3645
- price: options?.properties?.price,
3646
- currency: options?.properties?.currency,
3647
- placement: options?.properties?.placement
3648
- }));
3649
- } else if (eventName === "$paywall_viewed" || eventName === "$paywall_closed") {
3650
- if (this.config?.debug) console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3651
- distinctId: distinctId.substring(0, 8) + "...",
3652
- placement: options?.properties?.placement,
3653
- variantKey: options?.properties?.variantKey,
3654
- timeOnPaywall: options?.properties?.timeOnPaywall
3655
- }));
3656
- } else {
3657
- if (this.config?.debug) console.warn("[Paywallo TRACK]", eventName, "distinctId:", distinctId.substring(0, 8) + "...");
3658
- }
3659
3980
  await apiClient.trackEvent(eventName, distinctId, {
3660
3981
  ...identityManager.getProperties(),
3661
3982
  ...options?.properties,
@@ -3666,7 +3987,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3666
3987
  const apiClient = this.getApiClientOrThrow();
3667
3988
  const distinctId = identityManager.getDistinctId();
3668
3989
  if (!distinctId) {
3669
- if (this.config?.debug) console.warn("[Paywallo] Skipping event \u2014 no distinctId available");
3670
3990
  return;
3671
3991
  }
3672
3992
  if (!Number.isInteger(index) || index < 0) {
@@ -3690,7 +4010,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3690
4010
  const apiClient = this.getApiClientOrThrow();
3691
4011
  const distinctId = identityManager.getDistinctId();
3692
4012
  if (!distinctId) {
3693
- if (this.config?.debug) console.warn("[Paywallo] Skipping event \u2014 no distinctId available");
3694
4013
  return;
3695
4014
  }
3696
4015
  if (!actionName || typeof actionName !== "string") {
@@ -3719,14 +4038,10 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3719
4038
  try {
3720
4039
  const distinctId = identityManager.getDistinctId();
3721
4040
  if (!distinctId) {
3722
- if (this.config?.debug) console.warn("[Paywallo AB]", "getVariant SKIPPED \u2014 no distinctId", flagKey);
3723
4041
  return { variant: null };
3724
4042
  }
3725
- const result = await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3726
- if (this.config?.debug) console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3727
- return result;
4043
+ return await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3728
4044
  } catch (error) {
3729
- if (this.config?.debug) console.warn("[Paywallo]", "getVariant failed \u2014 returning null", error);
3730
4045
  if (this.apiClient) void this.apiClient.reportError("GET_VARIANT_FAILED", error instanceof Error ? error.message : String(error));
3731
4046
  return { variant: null };
3732
4047
  }
@@ -3742,7 +4057,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3742
4057
  const result = await this.getApiClientOrThrow().getConditionalFlag(flagKey, { distinctId, ...context });
3743
4058
  return result.value;
3744
4059
  } catch (error) {
3745
- if (this.config?.debug) console.warn("[Paywallo]", "getConditionalFlag failed \u2014 returning false", error);
3746
4060
  if (this.apiClient) void this.apiClient.reportError("GET_CONDITIONAL_FLAG_FAILED", error instanceof Error ? error.message : String(error));
3747
4061
  return false;
3748
4062
  }
@@ -3765,15 +4079,18 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3765
4079
  async hasActiveSubscription() {
3766
4080
  this.ensureInitialized();
3767
4081
  try {
3768
- if (this.activeChecker) return await this.activeChecker();
4082
+ if (this.activeChecker) {
4083
+ return await this.activeChecker();
4084
+ }
3769
4085
  const distinctId = identityManager.getDistinctId();
3770
- if (!distinctId || !this.apiClient) return false;
4086
+ if (!distinctId || !this.apiClient) {
4087
+ return false;
4088
+ }
3771
4089
  const status = await this.apiClient.getSubscriptionStatus(distinctId);
3772
4090
  return status.hasActiveSubscription;
3773
- } catch {
3774
- if (this.config?.debug) console.warn("[Paywallo]", "hasActiveSubscription failed \u2014 using persistent cache");
3775
- if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", "hasActiveSubscription failed");
3776
- return resolveClientOfflineFromCache();
4091
+ } catch (error) {
4092
+ if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", error instanceof Error ? error.message : "hasActiveSubscription failed");
4093
+ return false;
3777
4094
  }
3778
4095
  }
3779
4096
  async getSubscription() {
@@ -3786,7 +4103,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3786
4103
  if (!status.subscription) return null;
3787
4104
  return { ...status.subscription, expiresAt: status.subscription.expiresAt ? new Date(status.subscription.expiresAt) : null };
3788
4105
  } catch {
3789
- if (this.config?.debug) console.warn("[Paywallo]", "getSubscription failed \u2014 returning null");
3790
4106
  return null;
3791
4107
  }
3792
4108
  }
@@ -3797,20 +4113,24 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3797
4113
  }
3798
4114
  async reset() {
3799
4115
  await identityManager.reset();
4116
+ await subscriptionCache.invalidateAll();
3800
4117
  }
3801
4118
  async fullReset() {
3802
4119
  const wasDebug = this.config?.debug ?? false;
3803
4120
  await sessionManager.endSession();
3804
4121
  await identityManager.reset();
3805
- await subscriptionCache.invalidate();
4122
+ await subscriptionCache.invalidateAll();
3806
4123
  queueProcessor.dispose();
3807
4124
  offlineQueue.dispose();
3808
4125
  networkMonitor.dispose();
3809
4126
  campaignGateService.clearPreloaded();
3810
4127
  this.cleanupNetworkRecovery();
4128
+ this.sessionFlagsMap.clear();
3811
4129
  this.apiClient = null;
3812
4130
  this.config = null;
3813
4131
  this.initPromise = null;
4132
+ this.readyPromise = null;
4133
+ this.resolveReady = null;
3814
4134
  this.pendingConfig = null;
3815
4135
  this.initAttempts = 0;
3816
4136
  this.paywallPresenter = null;
@@ -3819,7 +4139,13 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3819
4139
  this.activeChecker = null;
3820
4140
  this.restoreHandler = null;
3821
4141
  this.lastOnboardingStepTimestamp = null;
3822
- 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;
3823
4149
  }
3824
4150
  getConfig() {
3825
4151
  return this.config;
@@ -3908,6 +4234,15 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3908
4234
  getPreloadedCampaign(placement) {
3909
4235
  return campaignGateService.getPreloadedCampaign(placement);
3910
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
+ }
3911
4246
  isOnline() {
3912
4247
  return networkMonitor.isOnline();
3913
4248
  }
@@ -3937,48 +4272,6 @@ var PaywalloClient = new PaywalloClientClass();
3937
4272
  var import_react9 = require("react");
3938
4273
  var PaywalloContext = (0, import_react9.createContext)(null);
3939
4274
 
3940
- // src/utils/localization.ts
3941
- var import_react_native12 = require("react-native");
3942
- var currentLanguage = "pt-BR";
3943
- var defaultLanguage = "pt-BR";
3944
- function setCurrentLanguage(language) {
3945
- currentLanguage = language;
3946
- }
3947
- function setDefaultLanguage(language) {
3948
- defaultLanguage = language;
3949
- }
3950
- function getCurrentLanguage() {
3951
- return currentLanguage;
3952
- }
3953
- function getDefaultLanguage() {
3954
- return defaultLanguage;
3955
- }
3956
- function getLocalizedString(text) {
3957
- if (!text) return "";
3958
- if (typeof text === "string") return text;
3959
- return text[currentLanguage] ?? text[defaultLanguage] ?? Object.values(text)[0] ?? "";
3960
- }
3961
- function detectDeviceLanguage() {
3962
- try {
3963
- if (import_react_native12.Platform.OS === "ios") {
3964
- const settings = import_react_native12.NativeModules.SettingsManager?.settings;
3965
- return settings?.AppleLocale ?? settings?.AppleLanguages?.[0] ?? "en-US";
3966
- }
3967
- return import_react_native12.NativeModules.I18nManager?.localeIdentifier ?? "en-US";
3968
- } catch {
3969
- return "en-US";
3970
- }
3971
- }
3972
- function initLocalization(options) {
3973
- if (options?.defaultLanguage) {
3974
- defaultLanguage = options.defaultLanguage;
3975
- }
3976
- if (options?.detectDevice !== false) {
3977
- const deviceLanguage = detectDeviceLanguage();
3978
- currentLanguage = deviceLanguage;
3979
- }
3980
- }
3981
-
3982
4275
  // src/hooks/usePaywallo.ts
3983
4276
  var import_react10 = require("react");
3984
4277
  function usePaywallo() {
@@ -4004,7 +4297,8 @@ function mapToIAPProduct(product) {
4004
4297
  type: product.type === "subscription" ? "subs" : "inapp",
4005
4298
  subscriptionPeriodAndroid: product.subscriptionPeriod,
4006
4299
  introductoryPrice: product.introductoryPrice,
4007
- freeTrialPeriodAndroid: product.freeTrialPeriod
4300
+ freeTrialPeriodAndroid: product.freeTrialPeriod,
4301
+ trialDays: product.trialDays ?? 0
4008
4302
  };
4009
4303
  }
4010
4304
  function mapToFormattedProduct(product) {
@@ -4019,6 +4313,7 @@ function mapToFormattedProduct(product) {
4019
4313
  subscriptionPeriod: product.subscriptionPeriod,
4020
4314
  introductoryPrice: product.introductoryPrice,
4021
4315
  freeTrialPeriod: product.freeTrialPeriod,
4316
+ trialDays: product.trialDays ?? 0,
4022
4317
  pricePerMonth: product.pricePerMonth,
4023
4318
  pricePerWeek: product.pricePerWeek,
4024
4319
  pricePerDay: product.pricePerDay
@@ -4100,27 +4395,37 @@ function usePurchase() {
4100
4395
  setError(null);
4101
4396
  try {
4102
4397
  const iapService = getIAPService();
4103
- const result = await iapService.purchase(productId);
4398
+ const result = await iapService.purchase(productId, {
4399
+ onValidating: () => setState("validating")
4400
+ });
4104
4401
  if (result.success && result.purchase) {
4105
4402
  setState("idle");
4106
4403
  return {
4107
- productId: result.purchase.productId,
4108
- transactionId: result.purchase.transactionId,
4109
- transactionDate: result.purchase.transactionDate,
4110
- 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
+ }
4111
4411
  };
4112
4412
  }
4113
4413
  if (result.error) {
4114
- const purchaseError = createPurchaseError("PURCHASE_FAILED", result.error.message);
4115
- setError(purchaseError);
4414
+ setError(result.error);
4116
4415
  setState("error");
4117
- throw result.error;
4416
+ return { status: "failed", error: result.error };
4118
4417
  }
4119
4418
  setState("idle");
4120
- throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, "Purchase cancelled by user", true);
4419
+ return { status: "cancelled" };
4121
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);
4122
4427
  setState("error");
4123
- throw err;
4428
+ return { status: "failed", error: purchaseError };
4124
4429
  }
4125
4430
  }, []);
4126
4431
  const restore = (0, import_react12.useCallback)(async () => {
@@ -4149,146 +4454,14 @@ function usePurchase() {
4149
4454
  restore,
4150
4455
  error,
4151
4456
  isLoading: state === "loading",
4152
- isPurchasing: state === "purchasing",
4457
+ isPurchasing: state === "purchasing" || state === "validating",
4458
+ isValidating: state === "validating",
4153
4459
  isRestoring: state === "restoring"
4154
4460
  };
4155
4461
  }
4156
4462
 
4157
4463
  // src/hooks/useSubscription.ts
4158
4464
  var import_react13 = require("react");
4159
-
4160
- // src/domains/subscription/SubscriptionManager.ts
4161
- var import_react_native13 = require("react-native");
4162
- var SubscriptionManagerClass = class {
4163
- constructor() {
4164
- this.config = null;
4165
- this.listeners = /* @__PURE__ */ new Set();
4166
- this.userId = null;
4167
- this.cache = new SubscriptionCache();
4168
- }
4169
- init(config) {
4170
- this.config = config;
4171
- if (config.cacheTTL) {
4172
- this.cache.setTTL(config.cacheTTL);
4173
- }
4174
- }
4175
- setUserId(userId) {
4176
- if (this.userId !== userId) {
4177
- this.userId = userId;
4178
- void this.cache.invalidate();
4179
- }
4180
- }
4181
- async hasActiveSubscription(forceRefresh = false) {
4182
- const status = await this.getSubscriptionStatus(forceRefresh);
4183
- return status.hasActiveSubscription;
4184
- }
4185
- async getSubscription(forceRefresh = false) {
4186
- const status = await this.getSubscriptionStatus(forceRefresh);
4187
- return status.subscription;
4188
- }
4189
- async getEntitlements(forceRefresh = false) {
4190
- const status = await this.getSubscriptionStatus(forceRefresh);
4191
- return status.entitlements;
4192
- }
4193
- async getSubscriptionStatus(forceRefresh = false) {
4194
- if (!this.config) {
4195
- return this.getEmptyStatus();
4196
- }
4197
- if (!forceRefresh) {
4198
- const cached = await this.cache.get();
4199
- if (cached && !cached.isStale) {
4200
- return cached.data;
4201
- }
4202
- }
4203
- try {
4204
- const status = await this.fetchSubscriptionStatus();
4205
- await this.cache.set(status);
4206
- this.notifyListeners(status);
4207
- return status;
4208
- } catch (error) {
4209
- this.log("Failed to fetch subscription status", error);
4210
- const cached = await this.cache.get();
4211
- if (cached) {
4212
- return cached.data;
4213
- }
4214
- return this.getEmptyStatus();
4215
- }
4216
- }
4217
- async restorePurchases() {
4218
- if (!this.config) {
4219
- throw new SessionError(
4220
- SESSION_ERROR_CODES.NOT_INITIALIZED,
4221
- "SubscriptionManager not initialized"
4222
- );
4223
- }
4224
- await this.cache.invalidate();
4225
- return this.getSubscriptionStatus(true);
4226
- }
4227
- async fetchSubscriptionStatus() {
4228
- if (!this.config) {
4229
- return this.getEmptyStatus();
4230
- }
4231
- const url = new URL(`${this.config.serverUrl}/subscriptions/status`);
4232
- if (this.userId) {
4233
- url.searchParams.set("userId", this.userId);
4234
- }
4235
- url.searchParams.set("platform", import_react_native13.Platform.OS);
4236
- const response = await fetch(url.toString(), {
4237
- method: "GET",
4238
- headers: {
4239
- "Content-Type": "application/json",
4240
- "X-App-Key": this.config.appKey
4241
- }
4242
- });
4243
- if (!response.ok) {
4244
- throw new SessionError(
4245
- SESSION_ERROR_CODES.RESTORE_FAILED,
4246
- `Failed to fetch subscription status: ${response.status}`
4247
- );
4248
- }
4249
- const data = await response.json();
4250
- if (data.subscription) {
4251
- data.subscription.expiresAt = new Date(data.subscription.expiresAt);
4252
- data.subscription.originalPurchaseDate = new Date(data.subscription.originalPurchaseDate);
4253
- data.subscription.latestPurchaseDate = new Date(data.subscription.latestPurchaseDate);
4254
- if (data.subscription.cancellationDate) {
4255
- data.subscription.cancellationDate = new Date(data.subscription.cancellationDate);
4256
- }
4257
- if (data.subscription.gracePeriodExpiresAt) {
4258
- data.subscription.gracePeriodExpiresAt = new Date(data.subscription.gracePeriodExpiresAt);
4259
- }
4260
- }
4261
- return data;
4262
- }
4263
- getEmptyStatus() {
4264
- return {
4265
- hasActiveSubscription: false,
4266
- subscription: null,
4267
- entitlements: []
4268
- };
4269
- }
4270
- addListener(listener) {
4271
- this.listeners.add(listener);
4272
- return () => this.listeners.delete(listener);
4273
- }
4274
- notifyListeners(status) {
4275
- for (const listener of this.listeners) {
4276
- listener(status);
4277
- }
4278
- }
4279
- async onPurchaseComplete() {
4280
- await this.cache.invalidate();
4281
- await this.getSubscriptionStatus(true);
4282
- }
4283
- log(message, data) {
4284
- if (this.config?.debug) {
4285
- console.warn(`[SubscriptionManager] ${message}`, data ?? "");
4286
- }
4287
- }
4288
- };
4289
- var subscriptionManager = new SubscriptionManagerClass();
4290
-
4291
- // src/hooks/useSubscription.ts
4292
4465
  function useSubscription() {
4293
4466
  const [status, setStatus] = (0, import_react13.useState)(null);
4294
4467
  const [isLoading, setIsLoading] = (0, import_react13.useState)(true);
@@ -4405,10 +4578,9 @@ var CACHE_TTL_MS = 5 * 60 * 1e3;
4405
4578
  function isCacheEntryValid(entry) {
4406
4579
  return !!entry && Date.now() - entry.preloadedAt < CACHE_TTL_MS;
4407
4580
  }
4408
- function consumeCacheEntry(cache, placement) {
4581
+ function getCacheEntry(cache, placement) {
4409
4582
  const entry = cache.get(placement);
4410
4583
  if (!isCacheEntryValid(entry)) return null;
4411
- cache.delete(placement);
4412
4584
  return entry ?? null;
4413
4585
  }
4414
4586
 
@@ -4421,7 +4593,7 @@ function useCampaignPreload() {
4421
4593
  []
4422
4594
  );
4423
4595
  const consumePreloadedCampaign = (0, import_react14.useCallback)(
4424
- (placement) => consumeCacheEntry(preloadedCampaignsRef.current, placement),
4596
+ (placement) => getCacheEntry(preloadedCampaignsRef.current, placement),
4425
4597
  []
4426
4598
  );
4427
4599
  const preloadCampaign = (0, import_react14.useCallback)(
@@ -4469,6 +4641,7 @@ function useCampaignPreload() {
4469
4641
  campaignId: campaign.campaignId,
4470
4642
  variantKey: campaign.variantKey
4471
4643
  });
4644
+ if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo WEBVIEW] mounting hidden \u2014 placement: ${placement}`);
4472
4645
  return { success: true };
4473
4646
  } catch (error) {
4474
4647
  return {
@@ -4503,7 +4676,6 @@ function useProductLoader() {
4503
4676
  for (const p of loaded) map.set(p.productId, p);
4504
4677
  setProducts(map);
4505
4678
  } catch (error) {
4506
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Product load failed:", error);
4507
4679
  } finally {
4508
4680
  setIsLoadingProducts(false);
4509
4681
  }
@@ -4513,7 +4685,7 @@ function useProductLoader() {
4513
4685
 
4514
4686
  // src/hooks/usePurchaseOrchestrator.ts
4515
4687
  var import_react16 = require("react");
4516
- function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef) {
4688
+ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, invalidateSubscriptionCache) {
4517
4689
  const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
4518
4690
  const handlePreloadedPurchase = (0, import_react16.useCallback)(
4519
4691
  async (productId) => {
@@ -4538,17 +4710,17 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4538
4710
  await trackDismissed({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, durationSeconds, action: "purchase", variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4539
4711
  await trackPurchased({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, productId, variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4540
4712
  }
4713
+ invalidateSubscriptionCache();
4541
4714
  setShowingPreloadedWebView(false);
4542
4715
  clearPreloadedWebView();
4543
4716
  resolvePreloadedPaywall({ presented: true, purchased: true, cancelled: false, restored: false });
4544
4717
  } catch (error) {
4545
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Preloaded purchase error:", error);
4546
4718
  setShowingPreloadedWebView(false);
4547
4719
  clearPreloadedWebView();
4548
4720
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
4549
4721
  }
4550
4722
  },
4551
- [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased]
4723
+ [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased, invalidateSubscriptionCache]
4552
4724
  );
4553
4725
  const handlePreloadedRestore = (0, import_react16.useCallback)(async () => {
4554
4726
  try {
@@ -4558,16 +4730,16 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4558
4730
  const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
4559
4731
  await trackDismissed({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, durationSeconds, action: "restore", variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4560
4732
  }
4733
+ invalidateSubscriptionCache();
4561
4734
  setShowingPreloadedWebView(false);
4562
4735
  clearPreloadedWebView();
4563
4736
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: true });
4564
4737
  } catch (error) {
4565
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Preloaded restore error:", error);
4566
4738
  setShowingPreloadedWebView(false);
4567
4739
  clearPreloadedWebView();
4568
4740
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
4569
4741
  }
4570
- }, [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed]);
4742
+ }, [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, invalidateSubscriptionCache]);
4571
4743
  return { handlePreloadedPurchase, handlePreloadedRestore };
4572
4744
  }
4573
4745
 
@@ -4579,7 +4751,6 @@ function toDomainSubscriptionInfo(sub) {
4579
4751
  return {
4580
4752
  id: "",
4581
4753
  productId: sub.productId,
4582
- // The hook's SubscriptionStatus is a subset of the domain's — cast is safe
4583
4754
  status: sub.status,
4584
4755
  expiresAt: sub.expiresAt ?? /* @__PURE__ */ new Date(0),
4585
4756
  originalPurchaseDate: /* @__PURE__ */ new Date(0),
@@ -4597,31 +4768,32 @@ function toDomainResponse(status) {
4597
4768
  entitlements: []
4598
4769
  };
4599
4770
  }
4600
- async function resolveOfflineFromCache() {
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) {
4601
4778
  try {
4602
- const cached = await subscriptionCache.get();
4779
+ const cached = await subscriptionCache.get(distinctId);
4603
4780
  if (!cached) return false;
4604
4781
  const sub = cached.data.subscription;
4605
- if (!sub) return false;
4606
- if (!ACTIVE_STATUSES.includes(sub.status)) return false;
4607
- const hasValidExpiry = sub.expiresAt.getTime() === 0 || sub.expiresAt > /* @__PURE__ */ new Date();
4608
- return hasValidExpiry;
4782
+ return isSubscriptionStillActive(sub ? { status: sub.status, expiresAt: sub.expiresAt ?? null } : null);
4609
4783
  } catch {
4610
4784
  return false;
4611
4785
  }
4612
4786
  }
4613
4787
  function isActiveCacheEntry(entry) {
4614
- if (!entry.data) return false;
4615
- if (!ACTIVE_STATUSES.includes(entry.data.status)) return false;
4616
- if (entry.data.expiresAt && entry.data.expiresAt < /* @__PURE__ */ new Date()) return false;
4617
- return true;
4788
+ return isSubscriptionStillActive(entry.data);
4618
4789
  }
4619
4790
  function useSubscriptionSync() {
4620
4791
  const [subscription, setSubscription] = (0, import_react17.useState)(null);
4621
4792
  const cacheRef = (0, import_react17.useRef)(null);
4622
4793
  const invalidateCache = (0, import_react17.useCallback)(() => {
4623
4794
  cacheRef.current = null;
4624
- void subscriptionCache.invalidate();
4795
+ const distinctId = PaywalloClient.getDistinctId();
4796
+ if (distinctId) void subscriptionCache.invalidate(distinctId);
4625
4797
  }, []);
4626
4798
  const fetchAndCache = (0, import_react17.useCallback)(async () => {
4627
4799
  const apiClient = PaywalloClient.getApiClient();
@@ -4634,7 +4806,7 @@ function useSubscriptionSync() {
4634
4806
  } : null;
4635
4807
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4636
4808
  setSubscription(sub);
4637
- void subscriptionCache.set(toDomainResponse(status));
4809
+ void subscriptionCache.set(distinctId, toDomainResponse(status));
4638
4810
  return sub;
4639
4811
  }, []);
4640
4812
  const hasActiveSubscription = (0, import_react17.useCallback)(async () => {
@@ -4646,23 +4818,23 @@ function useSubscriptionSync() {
4646
4818
  return isActiveCacheEntry(cached);
4647
4819
  }
4648
4820
  try {
4649
- const persisted = await subscriptionCache.get();
4650
- if (persisted) {
4651
- if (!persisted.isStale) {
4652
- const sub = persisted.data.subscription ? {
4653
- productId: persisted.data.subscription.productId,
4654
- status: persisted.data.subscription.status,
4655
- expiresAt: persisted.data.subscription.expiresAt ?? null,
4656
- platform: persisted.data.subscription.platform,
4657
- autoRenewEnabled: persisted.data.subscription.willRenew,
4658
- inGracePeriod: persisted.data.subscription.status === "grace_period"
4659
- } : null;
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) {
4660
4834
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4661
4835
  setSubscription(sub);
4662
- return persisted.data.hasActiveSubscription;
4836
+ return true;
4663
4837
  }
4664
- void fetchAndCache();
4665
- return persisted.data.hasActiveSubscription;
4666
4838
  }
4667
4839
  } catch {
4668
4840
  }
@@ -4674,12 +4846,12 @@ function useSubscriptionSync() {
4674
4846
  } : null;
4675
4847
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4676
4848
  setSubscription(sub);
4677
- void subscriptionCache.set(toDomainResponse(status));
4849
+ void subscriptionCache.set(distinctId, toDomainResponse(status));
4678
4850
  return status.hasActiveSubscription;
4679
4851
  } catch {
4680
- return await resolveOfflineFromCache();
4852
+ return await resolveOfflineFromCache(distinctId);
4681
4853
  }
4682
- }, [fetchAndCache]);
4854
+ }, []);
4683
4855
  const getSubscription = (0, import_react17.useCallback)(async () => {
4684
4856
  const apiClient = PaywalloClient.getApiClient();
4685
4857
  const distinctId = PaywalloClient.getDistinctId();
@@ -4690,7 +4862,7 @@ function useSubscriptionSync() {
4690
4862
  return await fetchAndCache();
4691
4863
  } catch {
4692
4864
  try {
4693
- const persisted = await subscriptionCache.get();
4865
+ const persisted = await subscriptionCache.get(distinctId);
4694
4866
  if (!persisted?.data.subscription) return null;
4695
4867
  const s = persisted.data.subscription;
4696
4868
  const normalisedStatus = s.status === "grace_period" ? "in_grace_period" : s.status;
@@ -4721,13 +4893,38 @@ function PaywalloProvider({ children, config }) {
4721
4893
  const { subscription, hasActiveSubscription, getSubscription, invalidateCache } = useSubscriptionSync();
4722
4894
  const { preloadedWebView, setPreloadedWebView, preloadCampaign, consumePreloadedCampaign, isPreloadValid } = useCampaignPreload();
4723
4895
  const clearPreloadedWebView = (0, import_react18.useCallback)(() => setPreloadedWebView(null), [setPreloadedWebView]);
4724
- 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]);
4725
4921
  const { handlePreloadedPurchase: rawPreloadedPurchase, handlePreloadedRestore } = usePurchaseOrchestrator(
4726
4922
  preloadedWebView,
4727
4923
  resolvePreloadedPaywall,
4728
4924
  clearPreloadedWebView,
4729
4925
  setShowingPreloadedWebView,
4730
- presentedAtRef
4926
+ presentedAtRef,
4927
+ invalidateCache
4731
4928
  );
4732
4929
  const [isPreloadPurchasing, setIsPreloadPurchasing] = (0, import_react18.useState)(false);
4733
4930
  const handlePreloadedPurchase = (0, import_react18.useCallback)(async (productId) => {
@@ -4740,13 +4937,19 @@ function PaywalloProvider({ children, config }) {
4740
4937
  }, [rawPreloadedPurchase]);
4741
4938
  (0, import_react18.useEffect)(() => {
4742
4939
  initLocalization({ detectDevice: true });
4743
- PaywalloClient.init(config).then(() => {
4940
+ PaywalloClient.init(config).then(async () => {
4744
4941
  setDistinctId(PaywalloClient.getDistinctId());
4745
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
+ }
4746
4949
  }).catch((err) => {
4747
4950
  setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
4748
4951
  });
4749
- }, [config]);
4952
+ }, [config, preloadCampaign]);
4750
4953
  const getPaywallConfig = (0, import_react18.useCallback)(async (p) => {
4751
4954
  const api = PaywalloClient.getApiClient();
4752
4955
  return api ? api.getPaywall(p) : null;
@@ -4845,28 +5048,54 @@ function PaywalloProvider({ children, config }) {
4845
5048
  setActivePaywall(null);
4846
5049
  }
4847
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]);
4848
5078
  (0, import_react18.useEffect)(() => {
4849
5079
  if (!isInitialized) return;
4850
5080
  const onEmergency = async (id) => {
4851
5081
  try {
4852
- await presentPaywall(id);
5082
+ await bridgePaywallPresenter(id);
4853
5083
  } catch {
4854
5084
  }
4855
5085
  };
4856
- PaywalloClient.registerPaywallPresenter(presentPaywall);
5086
+ PaywalloClient.registerPaywallPresenter(bridgePaywallPresenter);
4857
5087
  PaywalloClient.registerSubscriptionGetter(getSubscription);
4858
5088
  PaywalloClient.registerActiveChecker(hasActiveSubscription);
4859
5089
  PaywalloClient.registerRestoreHandler(restorePurchases);
4860
5090
  PaywalloClient.registerEmergencyPaywallHandler(onEmergency);
4861
- }, [isInitialized, presentPaywall, getSubscription, hasActiveSubscription, restorePurchases]);
4862
- (0, import_react18.useEffect)(() => {
4863
- if (activePaywall) setPreloadedWebView(null);
4864
- }, [activePaywall, setPreloadedWebView]);
5091
+ }, [isInitialized, bridgePaywallPresenter, getSubscription, hasActiveSubscription, restorePurchases]);
5092
+ const isReady = isInitialized && !isLoadingProducts;
4865
5093
  const noOp = (0, import_react18.useCallback)(() => {
4866
5094
  }, []);
4867
5095
  const contextValue = (0, import_react18.useMemo)(() => ({
4868
5096
  config: PaywalloClient.getConfig(),
4869
5097
  isInitialized,
5098
+ isReady,
4870
5099
  initError,
4871
5100
  distinctId,
4872
5101
  subscription,
@@ -4882,6 +5111,7 @@ function PaywalloProvider({ children, config }) {
4882
5111
  refreshProducts
4883
5112
  }), [
4884
5113
  isInitialized,
5114
+ isReady,
4885
5115
  initError,
4886
5116
  distinctId,
4887
5117
  subscription,
@@ -4896,20 +5126,16 @@ function PaywalloProvider({ children, config }) {
4896
5126
  getPaywallConfig,
4897
5127
  refreshProducts
4898
5128
  ]);
4899
- const SCREEN_HEIGHT3 = (0, import_react18.useMemo)(() => import_react_native14.Dimensions.get("window").height, []);
4900
- const slideAnim = (0, import_react18.useRef)(new import_react_native14.Animated.Value(SCREEN_HEIGHT3)).current;
4901
- (0, import_react18.useEffect)(() => {
5129
+ (0, import_react18.useLayoutEffect)(() => {
4902
5130
  if (showingPreloadedWebView) {
4903
5131
  slideAnim.setValue(SCREEN_HEIGHT3);
4904
- import_react_native14.Animated.timing(slideAnim, { toValue: 0, duration: 320, easing: import_react_native14.Easing.out(import_react_native14.Easing.cubic), useNativeDriver: true }).start();
4905
- } else {
4906
- 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();
4907
5133
  }
4908
5134
  }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT3]);
4909
5135
  const preloadStyle = [
4910
5136
  styles3.preloadOverlay,
4911
5137
  showingPreloadedWebView ? styles3.visible : styles3.hidden,
4912
- { transform: [{ translateY: showingPreloadedWebView ? slideAnim : SCREEN_HEIGHT3 }] }
5138
+ showingPreloadedWebView ? { transform: [{ translateY: slideAnim }] } : void 0
4913
5139
  ];
4914
5140
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PaywalloContext.Provider, { value: contextValue, children: [
4915
5141
  children,
@@ -4922,6 +5148,7 @@ function PaywalloProvider({ children, config }) {
4922
5148
  primaryProductId: preloadedWebView.primaryProductId,
4923
5149
  secondaryProductId: preloadedWebView.secondaryProductId,
4924
5150
  isPurchasing: isPreloadPurchasing,
5151
+ webUrl: isInitialized ? PaywalloClient.getWebUrl() : null,
4925
5152
  onReady: handlePreloadedWebViewReady,
4926
5153
  onClose: showingPreloadedWebView ? handlePreloadedClose : noOp,
4927
5154
  onPurchase: showingPreloadedWebView ? handlePreloadedPurchase : noOp,
@@ -4944,8 +5171,8 @@ function PaywalloProvider({ children, config }) {
4944
5171
  }
4945
5172
  var styles3 = import_react_native14.StyleSheet.create({
4946
5173
  preloadOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#ffffff" },
4947
- hidden: { zIndex: -1 },
4948
- visible: { zIndex: 9999 }
5174
+ hidden: { zIndex: -1, opacity: 0, width: 1, height: 1, overflow: "hidden" },
5175
+ visible: { zIndex: 9999, opacity: 1 }
4949
5176
  });
4950
5177
 
4951
5178
  // src/utils/productVariableResolver.ts