@virex-tech/paywallo-sdk 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -38,8 +38,17 @@ var CLIENT_ERROR_CODES = {
38
38
  };
39
39
 
40
40
  // src/domains/paywall/PaywallModal.tsx
41
- import { useEffect as useEffect3, useRef as useRef4 } from "react";
42
- import { Animated as Animated2, Dimensions as Dimensions2, Easing, Modal, StyleSheet as StyleSheet2, Text, View as View2 } from "react-native";
41
+ import { useCallback as useCallback4, useEffect as useEffect3, useRef as useRef4, useState as useState4 } from "react";
42
+ import {
43
+ Animated as Animated2,
44
+ Dimensions as Dimensions2,
45
+ Easing,
46
+ Modal,
47
+ Pressable,
48
+ StyleSheet as StyleSheet2,
49
+ Text,
50
+ View as View2
51
+ } from "react-native";
43
52
 
44
53
  // src/domains/paywall/usePaywallActions.ts
45
54
  import { useCallback as useCallback2, useRef, useState } from "react";
@@ -136,9 +145,6 @@ var SecureStorage = class {
136
145
  // eslint-disable-next-line @typescript-eslint/no-useless-constructor
137
146
  constructor(_debug = false) {
138
147
  }
139
- /**
140
- * Get a value, trying Keychain first (survives reinstalls on iOS), then AsyncStorage.
141
- */
142
148
  async get(key) {
143
149
  const keychainValue = await this.getFromKeychain(key);
144
150
  if (keychainValue) {
@@ -154,9 +160,6 @@ var SecureStorage = class {
154
160
  return null;
155
161
  }
156
162
  }
157
- /**
158
- * Set a value in both Keychain (if available) and AsyncStorage.
159
- */
160
163
  async set(key, value) {
161
164
  try {
162
165
  await Promise.all([
@@ -220,11 +223,13 @@ var secureStorage = new SecureStorage();
220
223
 
221
224
  // src/domains/identity/IdentityManager.ts
222
225
  var DEVICE_ID_KEY = "device_id";
226
+ var ANON_ID_KEY = "anon_id";
223
227
  var USER_EMAIL_KEY = "user_email";
224
228
  var USER_PROPERTIES_KEY = "user_properties";
225
229
  var IdentityManager = class {
226
230
  constructor() {
227
231
  this.deviceId = null;
232
+ this.anonId = null;
228
233
  this.email = null;
229
234
  this.properties = {};
230
235
  this.apiClient = null;
@@ -251,23 +256,23 @@ var IdentityManager = class {
251
256
  try {
252
257
  if (DeviceInfo) {
253
258
  this.deviceId = await DeviceInfo.getUniqueId();
254
- this.log("Got device ID from DeviceInfo:", this.deviceId);
255
259
  } else {
256
260
  this.deviceId = generateUUID();
257
- this.log("DeviceInfo not installed, using generated UUID:", this.deviceId);
258
261
  }
259
262
  } catch {
260
263
  this.deviceId = generateUUID();
261
- this.log("DeviceInfo unavailable, using generated UUID:", this.deviceId);
262
264
  }
263
265
  const stored = await this.secureStorage.set(DEVICE_ID_KEY, this.deviceId);
264
266
  if (!stored) {
265
- this.log("Failed to persist device ID to secure storage, using fallback");
266
267
  await this.secureStorage.set("device_id_fallback", this.deviceId).catch(() => {
267
- this.log("Fallback also failed \u2014 device ID only in memory");
268
268
  });
269
269
  }
270
270
  }
271
+ if (!this.anonId) {
272
+ this.anonId = "$paywallo_anon:" + generateUUID();
273
+ await this.secureStorage.set(ANON_ID_KEY, this.anonId).catch(() => {
274
+ });
275
+ }
271
276
  this.initialized = true;
272
277
  return this.deviceId ?? "";
273
278
  }
@@ -291,21 +296,18 @@ var IdentityManager = class {
291
296
  this.properties = { ...this.properties, ...properties };
292
297
  await this.secureStorage.set(USER_PROPERTIES_KEY, JSON.stringify(this.properties));
293
298
  }
294
- if (this.apiClient && this.deviceId) {
299
+ if (this.apiClient && this.anonId) {
295
300
  try {
296
- await this.apiClient.identify(this.deviceId, properties, email);
297
- this.log("Identity updated on server");
298
- } catch (error) {
299
- this.log("Failed to update identity on server:", error);
301
+ await this.apiClient.identify(this.anonId, properties, email, this.deviceId ?? void 0);
302
+ } catch {
300
303
  }
301
304
  }
302
305
  }
303
306
  getDistinctId() {
304
- if (!this.initialized || !this.deviceId) {
305
- if (this.debug) console.warn("[Paywallo Identity] getDistinctId called before initialization");
307
+ if (!this.initialized || !this.anonId) {
306
308
  return "";
307
309
  }
308
- return this.deviceId;
310
+ return this.anonId;
309
311
  }
310
312
  getDeviceId() {
311
313
  return this.deviceId;
@@ -320,24 +322,22 @@ var IdentityManager = class {
320
322
  this.ensureInitialized();
321
323
  this.properties = { ...this.properties, ...properties };
322
324
  await this.secureStorage.set(USER_PROPERTIES_KEY, JSON.stringify(this.properties));
323
- if (this.apiClient && this.deviceId) {
325
+ if (this.apiClient && this.anonId) {
324
326
  try {
325
- await this.apiClient.identify(this.deviceId, this.properties, this.email ?? void 0);
326
- } catch (error) {
327
- this.log("Failed to update properties on server:", error);
327
+ await this.apiClient.identify(this.anonId, this.properties, this.email ?? void 0, this.deviceId ?? void 0);
328
+ } catch {
328
329
  }
329
330
  }
330
331
  }
331
332
  async reset() {
333
+ const newAnonId = "$paywallo_anon:" + generateUUID();
334
+ this.anonId = newAnonId;
332
335
  this.email = null;
333
336
  this.properties = {};
334
- this.initialized = false;
335
- this.apiClient = null;
336
- this.deviceId = null;
337
337
  await Promise.all([
338
+ this.secureStorage?.set(ANON_ID_KEY, newAnonId) ?? Promise.resolve(false),
338
339
  this.secureStorage?.remove(USER_EMAIL_KEY) ?? Promise.resolve(false),
339
- this.secureStorage?.remove(USER_PROPERTIES_KEY) ?? Promise.resolve(false),
340
- this.secureStorage?.remove("device_id_fallback") ?? Promise.resolve(false)
340
+ this.secureStorage?.remove(USER_PROPERTIES_KEY) ?? Promise.resolve(false)
341
341
  ]);
342
342
  }
343
343
  getState() {
@@ -354,21 +354,23 @@ var IdentityManager = class {
354
354
  if (!this.secureStorage) {
355
355
  return;
356
356
  }
357
- const [storedDeviceId, storedEmail, storedPropertiesJson] = await Promise.all([
357
+ const [storedDeviceId, storedAnonId, storedEmail, storedPropertiesJson] = await Promise.all([
358
358
  this.secureStorage.get(DEVICE_ID_KEY),
359
+ this.secureStorage.get(ANON_ID_KEY),
359
360
  this.secureStorage.get(USER_EMAIL_KEY),
360
361
  this.secureStorage.get(USER_PROPERTIES_KEY)
361
362
  ]);
362
363
  if (storedDeviceId) {
363
364
  this.deviceId = storedDeviceId;
364
- this.log("Loaded device ID from storage:", this.deviceId);
365
365
  } else {
366
366
  const fallbackDeviceId = await this.secureStorage.get("device_id_fallback");
367
367
  if (fallbackDeviceId) {
368
368
  this.deviceId = fallbackDeviceId;
369
- this.log("Loaded device ID from fallback storage:", this.deviceId);
370
369
  }
371
370
  }
371
+ if (storedAnonId) {
372
+ this.anonId = storedAnonId;
373
+ }
372
374
  if (storedEmail) {
373
375
  this.email = storedEmail;
374
376
  }
@@ -388,11 +390,6 @@ var IdentityManager = class {
388
390
  );
389
391
  }
390
392
  }
391
- log(...args) {
392
- if (this.debug) {
393
- console.warn("[Paywallo Identity]", ...args);
394
- }
395
- }
396
393
  };
397
394
  var identityManager = new IdentityManager();
398
395
 
@@ -477,7 +474,6 @@ var nativeStoreKit = {
477
474
  isAvailable,
478
475
  async getProducts(productIds) {
479
476
  if (!PaywalloStoreKitNative) {
480
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] StoreKit native module not available");
481
477
  return [];
482
478
  }
483
479
  const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
@@ -548,7 +544,6 @@ var IAPService = class {
548
544
  }
549
545
  return products;
550
546
  } catch (error) {
551
- if (this.debug) console.warn("[Paywallo][Products] StoreKit error:", error);
552
547
  return [];
553
548
  }
554
549
  }
@@ -558,35 +553,46 @@ var IAPService = class {
558
553
  getCachedProducts() {
559
554
  return new Map(this.productsCache);
560
555
  }
556
+ async validateWithRetry(attempt, maxRetries = 2, delayMs = 3e3) {
557
+ let lastError;
558
+ for (let i = 0; i <= maxRetries; i++) {
559
+ try {
560
+ return await attempt();
561
+ } catch (error) {
562
+ lastError = error;
563
+ if (i < maxRetries) await new Promise((resolve) => setTimeout(resolve, delayMs));
564
+ }
565
+ }
566
+ throw lastError;
567
+ }
561
568
  async validateWithServer(purchase, productId, options) {
562
569
  const apiClient = this.getApiClient();
563
570
  const product = this.productsCache.get(productId);
564
571
  const platform = Platform2.OS;
565
572
  const distinctId = PaywalloClient.getDistinctId();
566
- const validation = await apiClient.validatePurchase(
567
- platform,
568
- purchase.receipt,
569
- purchase.productId,
570
- purchase.transactionId,
571
- product?.priceValue ?? 0,
572
- product?.currency ?? "USD",
573
- this.getDeviceCountry(),
574
- distinctId,
575
- options?.paywallPlacement,
576
- options?.variantKey
573
+ return this.validateWithRetry(
574
+ () => apiClient.validatePurchase(
575
+ platform,
576
+ purchase.receipt,
577
+ purchase.productId,
578
+ purchase.transactionId,
579
+ product?.priceValue ?? 0,
580
+ product?.currency ?? "USD",
581
+ this.getDeviceCountry(),
582
+ distinctId,
583
+ options?.paywallPlacement,
584
+ options?.variantKey
585
+ )
577
586
  );
578
- return validation;
579
587
  }
580
588
  async finishTransaction(transactionId) {
581
589
  try {
582
590
  await nativeStoreKit.finishTransaction(transactionId);
583
591
  } catch (finishError) {
584
- if (this.debug) console.warn("[Paywallo][Purchase] Failed to finish transaction:", finishError instanceof Error ? finishError.message : finishError);
585
592
  }
586
593
  }
587
594
  async purchase(productId, options) {
588
595
  if (!nativeStoreKit.isAvailable()) {
589
- if (this.debug) console.warn("[Paywallo][Purchase] StoreKit not available");
590
596
  return {
591
597
  success: false,
592
598
  error: new PurchaseError(PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE, "In-app purchases are not available on this device")
@@ -597,11 +603,11 @@ var IAPService = class {
597
603
  if (!purchase) {
598
604
  return { success: false };
599
605
  }
606
+ options?.onValidating?.();
600
607
  let validation = null;
601
608
  try {
602
609
  validation = await this.validateWithServer(purchase, productId, options);
603
610
  } catch (validationError) {
604
- if (this.debug) console.warn("[Paywallo][Purchase] Server validation FAILED:", validationError instanceof Error ? validationError.message : validationError);
605
611
  return {
606
612
  success: false,
607
613
  error: new PurchaseError(PURCHASE_ERROR_CODES.VALIDATION_FAILED, validationError instanceof Error ? validationError.message : "Server validation failed")
@@ -617,7 +623,6 @@ var IAPService = class {
617
623
  return { success: true, purchase };
618
624
  } catch (error) {
619
625
  const e = error instanceof PurchaseError ? error : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, error instanceof Error ? error.message : String(error));
620
- if (this.debug) console.warn("[Paywallo][Purchase] FAILED:", e.message);
621
626
  return { success: false, error: e };
622
627
  }
623
628
  }
@@ -653,14 +658,12 @@ var IAPService = class {
653
658
  try {
654
659
  await nativeStoreKit.finishTransaction(tx.transactionId);
655
660
  } catch (finishError) {
656
- if (this.debug) console.warn("[Paywallo][Restore] Failed to finish tx:", tx.transactionId, finishError instanceof Error ? finishError.message : finishError);
657
661
  }
658
662
  validated.push(tx);
659
663
  }
660
664
  }
661
665
  return validated;
662
666
  } catch (error) {
663
- if (this.debug) console.warn("[Paywallo][Restore] \u274C FAILED:", error instanceof Error ? error.message : error);
664
667
  return [];
665
668
  }
666
669
  }
@@ -728,7 +731,7 @@ function usePaywallTracking() {
728
731
 
729
732
  // src/domains/paywall/usePaywallActions.ts
730
733
  var SCREEN_HEIGHT = Dimensions.get("window").height;
731
- function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId) {
734
+ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId, openAnim) {
732
735
  const [isPurchasing, setIsPurchasing] = useState(false);
733
736
  const [isClosing, setIsClosing] = useState(false);
734
737
  const slideAnim = useRef(new Animated.Value(0)).current;
@@ -737,7 +740,8 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
737
740
  const handleClose = useCallback2(() => {
738
741
  if (isClosing) return;
739
742
  setIsClosing(true);
740
- Animated.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 300, useNativeDriver: true }).start(() => {
743
+ const animTarget = openAnim ?? slideAnim;
744
+ Animated.timing(animTarget, { toValue: SCREEN_HEIGHT, duration: 350, useNativeDriver: true }).start(() => {
741
745
  if (paywallConfig) {
742
746
  const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
743
747
  void trackDismissed({ placement, paywallId: paywallConfig.id, durationSeconds, action: "close", variantKey, campaignId });
@@ -746,7 +750,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
746
750
  setIsClosing(false);
747
751
  onResult({ presented: true, purchased: false, cancelled: true, restored: false });
748
752
  });
749
- }, [isClosing, onResult, paywallConfig, placement, slideAnim, trackDismissed, variantKey, campaignId]);
753
+ }, [isClosing, openAnim, onResult, paywallConfig, placement, slideAnim, trackDismissed, variantKey, campaignId]);
750
754
  const handlePurchase = useCallback2(async (productId) => {
751
755
  if (isPurchasing) return;
752
756
  setIsPurchasing(true);
@@ -764,7 +768,6 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
764
768
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
765
769
  }
766
770
  } catch (error) {
767
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Purchase error:", error);
768
771
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
769
772
  } finally {
770
773
  setIsPurchasing(false);
@@ -785,7 +788,6 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
785
788
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
786
789
  }
787
790
  } catch (error) {
788
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Restore error:", error);
789
791
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
790
792
  } finally {
791
793
  setIsPurchasing(false);
@@ -860,11 +862,6 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
860
862
  const storeProducts = await getIAPService().loadProducts(productIds);
861
863
  const map = /* @__PURE__ */ new Map();
862
864
  for (const p of storeProducts) map.set(p.productId, p);
863
- if (PaywalloClient.getConfig()?.debug) {
864
- for (const [id, p] of map) {
865
- console.warn(`[Paywallo][CURRENCY-CHECK] product=${id} currency=${p.currency} price="${p.price}" localizedPrice="${p.localizedPrice}" priceValue=${p.priceValue} fallbackUSD=${!p.currency || p.currency === "USD" ? "POSSIBLE" : "NO"}`);
866
- }
867
- }
868
865
  setProducts(map);
869
866
  } catch {
870
867
  setProducts(/* @__PURE__ */ new Map());
@@ -885,7 +882,6 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
885
882
  }
886
883
  } catch (err) {
887
884
  const e = err instanceof Error ? err : new PaywallError(PAYWALL_ERROR_CODES.LOAD_FAILED, String(err));
888
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Error loading paywall:", e.message);
889
885
  setError(e);
890
886
  }
891
887
  };
@@ -894,18 +890,103 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
894
890
  return { paywallConfig, products, error };
895
891
  }
896
892
 
893
+ // src/utils/localization.ts
894
+ import { NativeModules as NativeModules2, Platform as Platform3 } from "react-native";
895
+ var currentLanguage = "pt-BR";
896
+ var defaultLanguage = "pt-BR";
897
+ function setCurrentLanguage(language) {
898
+ currentLanguage = language;
899
+ }
900
+ function setDefaultLanguage(language) {
901
+ defaultLanguage = language;
902
+ }
903
+ function getCurrentLanguage() {
904
+ return currentLanguage;
905
+ }
906
+ function getDefaultLanguage() {
907
+ return defaultLanguage;
908
+ }
909
+ function getLocalizedString(text) {
910
+ if (!text) return "";
911
+ if (typeof text === "string") return text;
912
+ return text[currentLanguage] ?? text[defaultLanguage] ?? Object.values(text)[0] ?? "";
913
+ }
914
+ function detectDeviceLanguage() {
915
+ try {
916
+ if (Platform3.OS === "ios") {
917
+ const settings = NativeModules2.SettingsManager?.settings;
918
+ return settings?.AppleLocale ?? settings?.AppleLanguages?.[0] ?? "en-US";
919
+ }
920
+ return NativeModules2.I18nManager?.localeIdentifier ?? "en-US";
921
+ } catch {
922
+ return "en-US";
923
+ }
924
+ }
925
+ var SDK_STRINGS = {
926
+ paywallErrorTitle: {
927
+ "pt-BR": "Algo deu errado",
928
+ "pt": "Algo deu errado",
929
+ "en": "Something went wrong",
930
+ "en-US": "Something went wrong",
931
+ "en-GB": "Something went wrong",
932
+ "es": "Algo sali\xF3 mal",
933
+ "es-419": "Algo sali\xF3 mal"
934
+ },
935
+ paywallRetryButton: {
936
+ "pt-BR": "Tentar novamente",
937
+ "pt": "Tentar novamente",
938
+ "en": "Try again",
939
+ "en-US": "Try again",
940
+ "en-GB": "Try again",
941
+ "es": "Reintentar",
942
+ "es-419": "Reintentar"
943
+ },
944
+ paywallCloseButton: {
945
+ "pt-BR": "Fechar",
946
+ "pt": "Fechar",
947
+ "en": "Close",
948
+ "en-US": "Close",
949
+ "en-GB": "Close",
950
+ "es": "Cerrar",
951
+ "es-419": "Cerrar"
952
+ }
953
+ };
954
+ function getSdkString(key) {
955
+ const map = SDK_STRINGS[key];
956
+ const lang = currentLanguage;
957
+ if (lang in map) return map[lang];
958
+ const base = lang.split("-")[0];
959
+ if (base && base in map) return map[base];
960
+ if (defaultLanguage in map) return map[defaultLanguage];
961
+ return Object.values(map)[0];
962
+ }
963
+ function initLocalization(options) {
964
+ if (options?.defaultLanguage) {
965
+ defaultLanguage = options.defaultLanguage;
966
+ }
967
+ if (options?.detectDevice !== false) {
968
+ const deviceLanguage = detectDeviceLanguage();
969
+ currentLanguage = deviceLanguage;
970
+ }
971
+ }
972
+
897
973
  // src/domains/paywall/PaywallWebView.tsx
898
974
  import { useCallback as useCallback3, useEffect as useEffect2, useMemo, useRef as useRef3, useState as useState3 } from "react";
899
975
  import {
900
976
  Linking,
901
977
  NativeEventEmitter,
902
- NativeModules as NativeModules2,
978
+ NativeModules as NativeModules3,
903
979
  requireNativeComponent,
904
980
  StyleSheet,
905
981
  View
906
982
  } from "react-native";
907
983
 
908
984
  // src/domains/paywall/parseWebViewMessage.ts
985
+ function deriveMessageId(message) {
986
+ if (message.id) return message.id;
987
+ const payloadKey = message.payload?.productId ?? message.payload?.url ?? (message.payload?.timestamp != null ? String(message.payload.timestamp) : "");
988
+ return `${message.type}:${payloadKey}`;
989
+ }
909
990
  function parseWebViewMessage(raw) {
910
991
  try {
911
992
  const parsed = JSON.parse(raw);
@@ -929,7 +1010,7 @@ function buildPaywallInjectionScript(data) {
929
1010
  secondaryProductId: data.secondaryProductId
930
1011
  }
931
1012
  };
932
- 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;`;
1013
+ 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;`;
933
1014
  }
934
1015
  function buildPurchaseStateScript(isPurchasing) {
935
1016
  return `if(window.receiveNativeMessage){window.receiveNativeMessage({type:"purchaseState",data:{isPurchasing:${isPurchasing}}});}true;`;
@@ -947,10 +1028,17 @@ function getNativeWebView() {
947
1028
  var webViewEmitter = null;
948
1029
  function getWebViewEmitter() {
949
1030
  if (!webViewEmitter) {
950
- webViewEmitter = new NativeEventEmitter(NativeModules2.PaywalloWebViewModule);
1031
+ webViewEmitter = new NativeEventEmitter(NativeModules3.PaywalloWebViewModule);
951
1032
  }
952
1033
  return webViewEmitter;
953
1034
  }
1035
+ function parseErrorEvent(raw) {
1036
+ return {
1037
+ code: typeof raw["code"] === "number" ? raw["code"] : -1,
1038
+ description: typeof raw["description"] === "string" ? raw["description"] : "WebView error",
1039
+ url: typeof raw["url"] === "string" ? raw["url"] : void 0
1040
+ };
1041
+ }
954
1042
  function PaywallWebView({
955
1043
  paywallId,
956
1044
  craftData,
@@ -958,16 +1046,51 @@ function PaywallWebView({
958
1046
  primaryProductId,
959
1047
  secondaryProductId,
960
1048
  isPurchasing,
1049
+ webUrl: webUrlProp,
961
1050
  onPurchase,
962
1051
  onClose,
963
1052
  onRestore,
964
- onReady
1053
+ onReady,
1054
+ onError
965
1055
  }) {
966
1056
  const paywallDataSent = useRef3(false);
967
1057
  const loadEndReceived = useRef3(false);
968
1058
  const [scriptToInject, setScriptToInject] = useState3(void 0);
1059
+ const onErrorRef = useRef3(onError);
1060
+ onErrorRef.current = onError;
1061
+ const onReadyRef = useRef3(onReady);
1062
+ onReadyRef.current = onReady;
969
1063
  const NativeWebView = useMemo(() => getNativeWebView(), []);
970
- const webUrl = useMemo(() => PaywalloClient.getWebUrl() ?? "http://localhost:3000", []);
1064
+ const [resolvedWebUrl, setResolvedWebUrl] = useState3(
1065
+ () => webUrlProp ?? PaywalloClient.getWebUrl()
1066
+ );
1067
+ useEffect2(() => {
1068
+ if (webUrlProp !== void 0) {
1069
+ setResolvedWebUrl(webUrlProp ?? null);
1070
+ return;
1071
+ }
1072
+ const url = PaywalloClient.getWebUrl();
1073
+ if (url) {
1074
+ setResolvedWebUrl(url);
1075
+ return;
1076
+ }
1077
+ const interval = setInterval(() => {
1078
+ const u = PaywalloClient.getWebUrl();
1079
+ if (u) {
1080
+ setResolvedWebUrl(u);
1081
+ clearInterval(interval);
1082
+ }
1083
+ }, 200);
1084
+ return () => clearInterval(interval);
1085
+ }, [webUrlProp]);
1086
+ useEffect2(() => {
1087
+ if (!resolvedWebUrl) {
1088
+ onErrorRef.current?.({
1089
+ code: -1,
1090
+ description: "Paywall URL not configured. SDK may not be initialized."
1091
+ });
1092
+ }
1093
+ }, []);
971
1094
  const buildAndSetInjectionScript = useCallback3(() => {
972
1095
  if (!craftData || paywallDataSent.current) return;
973
1096
  paywallDataSent.current = true;
@@ -978,19 +1101,21 @@ function PaywallWebView({
978
1101
  secondaryProductId
979
1102
  });
980
1103
  setScriptToInject(script);
981
- onReady?.();
982
- }, [craftData, products, primaryProductId, secondaryProductId, onReady]);
983
- const lastProcessedRef = useRef3({ type: "", time: 0 });
1104
+ }, [craftData, products, primaryProductId, secondaryProductId]);
1105
+ const processedIdsRef = useRef3(/* @__PURE__ */ new Set());
984
1106
  const handleParsedMessage = useCallback3(
985
1107
  (message) => {
986
- const now = Date.now();
987
- if (message.type === lastProcessedRef.current.type && now - lastProcessedRef.current.time < 500) {
988
- return;
989
- }
990
- lastProcessedRef.current = { type: message.type, time: now };
1108
+ const msgId = deriveMessageId(message);
1109
+ if (processedIdsRef.current.has(msgId)) return;
1110
+ processedIdsRef.current.add(msgId);
1111
+ setTimeout(() => processedIdsRef.current.delete(msgId), 5e3);
991
1112
  switch (message.type) {
992
1113
  case "ready":
993
- if (!paywallDataSent.current) buildAndSetInjectionScript();
1114
+ if (!paywallDataSent.current) {
1115
+ buildAndSetInjectionScript();
1116
+ } else {
1117
+ onReadyRef.current?.();
1118
+ }
994
1119
  break;
995
1120
  case "purchase":
996
1121
  if (message.payload?.productId) onPurchase(message.payload.productId);
@@ -1025,17 +1150,51 @@ function PaywallWebView({
1025
1150
  const message = parseWebViewMessage(event.data);
1026
1151
  if (message) handleParsedMessage(message);
1027
1152
  });
1153
+ const errorSub = emitter.addListener(
1154
+ "PaywalloWebViewError",
1155
+ (event) => {
1156
+ onErrorRef.current?.(parseErrorEvent(event));
1157
+ }
1158
+ );
1028
1159
  return () => {
1029
1160
  loadEndSub.remove();
1030
1161
  messageSub.remove();
1162
+ errorSub.remove();
1031
1163
  };
1032
1164
  }, [buildAndSetInjectionScript, handleParsedMessage]);
1165
+ useEffect2(() => {
1166
+ const timer = setTimeout(() => {
1167
+ if (!paywallDataSent.current) {
1168
+ onErrorRef.current?.({
1169
+ code: -1001,
1170
+ description: "Paywall load timed out after 10 seconds"
1171
+ });
1172
+ }
1173
+ }, 1e4);
1174
+ return () => clearTimeout(timer);
1175
+ }, []);
1033
1176
  const handleLoadEnd = useCallback3(() => {
1034
1177
  if (!loadEndReceived.current) {
1035
1178
  loadEndReceived.current = true;
1036
1179
  buildAndSetInjectionScript();
1037
1180
  }
1038
1181
  }, [buildAndSetInjectionScript]);
1182
+ const handleNativeError = useCallback3((event) => {
1183
+ const ne = event.nativeEvent;
1184
+ let errorData;
1185
+ if (ne["payload"] && typeof ne["payload"] === "object") {
1186
+ errorData = ne["payload"];
1187
+ } else if (typeof ne["data"] === "string") {
1188
+ try {
1189
+ errorData = JSON.parse(ne["data"]);
1190
+ } catch {
1191
+ errorData = {};
1192
+ }
1193
+ } else {
1194
+ errorData = {};
1195
+ }
1196
+ onErrorRef.current?.(parseErrorEvent(errorData));
1197
+ }, []);
1039
1198
  const handleMessage = useCallback3(
1040
1199
  (event) => {
1041
1200
  const message = parseWebViewMessage(event.nativeEvent.data);
@@ -1047,7 +1206,8 @@ function PaywallWebView({
1047
1206
  if (!paywallDataSent.current) return;
1048
1207
  setScriptToInject(buildPurchaseStateScript(isPurchasing));
1049
1208
  }, [isPurchasing]);
1050
- const paywallUrl = `${webUrl}/paywall/${paywallId}`;
1209
+ if (!resolvedWebUrl) return null;
1210
+ const paywallUrl = `${resolvedWebUrl}/paywall/${paywallId}`;
1051
1211
  return /* @__PURE__ */ jsx(View, { style: styles.container, children: /* @__PURE__ */ jsx(
1052
1212
  NativeWebView,
1053
1213
  {
@@ -1055,6 +1215,7 @@ function PaywallWebView({
1055
1215
  injectedJavaScript: scriptToInject,
1056
1216
  onMessage: handleMessage,
1057
1217
  onLoadEnd: handleLoadEnd,
1218
+ onError: handleNativeError,
1058
1219
  style: styles.webview
1059
1220
  }
1060
1221
  ) });
@@ -1065,8 +1226,20 @@ var styles = StyleSheet.create({
1065
1226
  });
1066
1227
 
1067
1228
  // src/domains/paywall/PaywallModal.tsx
1068
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1229
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1069
1230
  var SCREEN_HEIGHT2 = Dimensions2.get("window").height;
1231
+ function ErrorFallback({ description, onRetry, onClose }) {
1232
+ const overrides = PaywalloClient.getConfig()?.errorStrings;
1233
+ const title = overrides?.title ?? getSdkString("paywallErrorTitle");
1234
+ const retryLabel = overrides?.retry ?? getSdkString("paywallRetryButton");
1235
+ const closeLabel = overrides?.close ?? getSdkString("paywallCloseButton");
1236
+ return /* @__PURE__ */ jsx2(View2, { style: styles2.fallbackContainer, children: /* @__PURE__ */ jsxs(View2, { style: styles2.fallbackCard, children: [
1237
+ /* @__PURE__ */ jsx2(Text, { style: styles2.fallbackTitle, children: title }),
1238
+ /* @__PURE__ */ jsx2(Text, { style: styles2.fallbackDescription, children: description }),
1239
+ /* @__PURE__ */ jsx2(Pressable, { style: styles2.retryButton, onPress: onRetry, children: /* @__PURE__ */ jsx2(Text, { style: styles2.retryButtonText, children: retryLabel }) }),
1240
+ /* @__PURE__ */ jsx2(Pressable, { style: styles2.closeButton, onPress: onClose, children: /* @__PURE__ */ jsx2(Text, { style: styles2.closeButtonText, children: closeLabel }) })
1241
+ ] }) });
1242
+ }
1070
1243
  function PaywallModal({
1071
1244
  placement,
1072
1245
  visible,
@@ -1084,10 +1257,35 @@ function PaywallModal({
1084
1257
  variantKey,
1085
1258
  campaignId
1086
1259
  );
1087
- const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId);
1088
1260
  const openAnim = useRef4(new Animated2.Value(SCREEN_HEIGHT2)).current;
1261
+ const [modalVisible, setModalVisible] = useState4(false);
1262
+ const handleResult = useCallback4((result) => {
1263
+ setModalVisible(false);
1264
+ onResult(result);
1265
+ }, [onResult]);
1266
+ const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, handleResult, variantKey, campaignId, openAnim);
1267
+ const [webViewError, setWebViewError] = useState4(null);
1268
+ const [retryCount, setRetryCount] = useState4(0);
1269
+ const retryCountRef = useRef4(0);
1270
+ const handleWebViewError = useCallback4(
1271
+ (err) => {
1272
+ if (retryCountRef.current >= 1) {
1273
+ setWebViewError({ code: err.code, description: err.description });
1274
+ } else {
1275
+ retryCountRef.current += 1;
1276
+ setRetryCount(retryCountRef.current);
1277
+ }
1278
+ },
1279
+ []
1280
+ );
1281
+ const handleRetry = useCallback4(() => {
1282
+ retryCountRef.current = 0;
1283
+ setWebViewError(null);
1284
+ setRetryCount(0);
1285
+ }, []);
1089
1286
  useEffect3(() => {
1090
1287
  if (visible) {
1288
+ setModalVisible(true);
1091
1289
  openAnim.setValue(SCREEN_HEIGHT2);
1092
1290
  Animated2.timing(openAnim, {
1093
1291
  toValue: 0,
@@ -1097,11 +1295,11 @@ function PaywallModal({
1097
1295
  }).start();
1098
1296
  }
1099
1297
  }, [visible, openAnim]);
1100
- if (!visible) return null;
1298
+ if (!modalVisible && !visible) return null;
1101
1299
  return /* @__PURE__ */ jsx2(
1102
1300
  Modal,
1103
1301
  {
1104
- visible,
1302
+ visible: modalVisible,
1105
1303
  animationType: "none",
1106
1304
  presentationStyle: "overFullScreen",
1107
1305
  onRequestClose: handleClose,
@@ -1109,7 +1307,14 @@ function PaywallModal({
1109
1307
  statusBarTranslucent: true,
1110
1308
  children: /* @__PURE__ */ jsx2(View2, { style: styles2.overlay, children: /* @__PURE__ */ jsx2(Animated2.View, { style: [styles2.animatedContainer, { transform: [{ translateY: openAnim }] }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.container, children: [
1111
1309
  error && /* @__PURE__ */ jsx2(View2, { style: styles2.errorContainer, children: /* @__PURE__ */ jsx2(Text, { style: styles2.errorText, children: error.message }) }),
1112
- !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ jsx2(
1310
+ !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ jsx2(Fragment, { children: webViewError ? /* @__PURE__ */ jsx2(
1311
+ ErrorFallback,
1312
+ {
1313
+ description: webViewError.description,
1314
+ onRetry: handleRetry,
1315
+ onClose: handleClose
1316
+ }
1317
+ ) : /* @__PURE__ */ jsx2(
1113
1318
  PaywallWebView,
1114
1319
  {
1115
1320
  paywallId: paywallConfig.id,
@@ -1120,39 +1325,67 @@ function PaywallModal({
1120
1325
  isPurchasing,
1121
1326
  onClose: handleClose,
1122
1327
  onPurchase: handlePurchase,
1123
- onRestore: handleRestore
1124
- }
1125
- )
1328
+ onRestore: handleRestore,
1329
+ onError: handleWebViewError
1330
+ },
1331
+ retryCount
1332
+ ) })
1126
1333
  ] }) }) })
1127
1334
  }
1128
1335
  );
1129
1336
  }
1130
1337
  var styles2 = StyleSheet2.create({
1131
- overlay: {
1338
+ overlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.3)" },
1339
+ animatedContainer: { flex: 1, backgroundColor: "#fff" },
1340
+ container: { flex: 1, backgroundColor: "#fff" },
1341
+ errorContainer: { flex: 1, justifyContent: "center", alignItems: "center", padding: 20 },
1342
+ errorText: { fontSize: 16, color: "#ef4444", textAlign: "center" },
1343
+ fallbackContainer: {
1132
1344
  flex: 1,
1133
- backgroundColor: "rgba(0,0,0,0.3)"
1345
+ justifyContent: "center",
1346
+ alignItems: "center",
1347
+ backgroundColor: "#111010",
1348
+ padding: 24
1134
1349
  },
1135
- animatedContainer: {
1136
- flex: 1,
1137
- backgroundColor: "#fff",
1138
- borderTopLeftRadius: 0,
1139
- borderTopRightRadius: 0
1350
+ fallbackCard: {
1351
+ width: "100%",
1352
+ maxWidth: 360,
1353
+ alignItems: "center",
1354
+ backgroundColor: "rgba(255,255,255,0.07)",
1355
+ borderRadius: 16,
1356
+ padding: 28
1140
1357
  },
1141
- container: {
1142
- flex: 1,
1143
- backgroundColor: "#fff"
1358
+ fallbackTitle: {
1359
+ fontSize: 18,
1360
+ fontWeight: "600",
1361
+ color: "#f5f5f5",
1362
+ marginBottom: 10,
1363
+ textAlign: "center"
1144
1364
  },
1145
- errorContainer: {
1146
- flex: 1,
1147
- justifyContent: "center",
1365
+ fallbackDescription: {
1366
+ fontSize: 14,
1367
+ color: "rgba(255,255,255,0.55)",
1368
+ textAlign: "center",
1369
+ marginBottom: 28,
1370
+ lineHeight: 20
1371
+ },
1372
+ retryButton: {
1373
+ width: "100%",
1374
+ backgroundColor: "#ffffff",
1375
+ borderRadius: 10,
1376
+ paddingVertical: 14,
1148
1377
  alignItems: "center",
1149
- padding: 20
1378
+ marginBottom: 12
1150
1379
  },
1151
- errorText: {
1152
- fontSize: 16,
1153
- color: "#ef4444",
1154
- textAlign: "center"
1155
- }
1380
+ retryButtonText: { fontSize: 15, fontWeight: "600", color: "#111010" },
1381
+ closeButton: {
1382
+ width: "100%",
1383
+ borderRadius: 10,
1384
+ paddingVertical: 14,
1385
+ alignItems: "center",
1386
+ backgroundColor: "rgba(255,255,255,0.1)"
1387
+ },
1388
+ closeButtonText: { fontSize: 15, fontWeight: "500", color: "rgba(255,255,255,0.7)" }
1156
1389
  });
1157
1390
 
1158
1391
  // src/domains/paywall/PaywallErrorBoundary.tsx
@@ -1165,8 +1398,7 @@ var PaywallErrorBoundary = class extends Component {
1165
1398
  static getDerivedStateFromError() {
1166
1399
  return { hasError: true };
1167
1400
  }
1168
- componentDidCatch(error) {
1169
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Paywall render error:", error.message);
1401
+ componentDidCatch(_error) {
1170
1402
  }
1171
1403
  render() {
1172
1404
  return this.state.hasError ? null : this.props.children;
@@ -1188,9 +1420,9 @@ function usePaywallContext() {
1188
1420
  }
1189
1421
 
1190
1422
  // src/domains/paywall/usePaywallPresenter.ts
1191
- import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef5, useState as useState4 } from "react";
1192
- function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView) {
1193
- const [showingPreloadedWebView, setShowingPreloadedWebView] = useState4(false);
1423
+ import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef5, useState as useState5 } from "react";
1424
+ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
1425
+ const [showingPreloadedWebView, setShowingPreloadedWebView] = useState5(false);
1194
1426
  const resolverRef = useRef5(null);
1195
1427
  const presentedAtRef = useRef5(Date.now());
1196
1428
  useEffect4(() => {
@@ -1201,23 +1433,23 @@ function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreload
1201
1433
  }
1202
1434
  };
1203
1435
  }, []);
1204
- const presentPreloadedWebView = useCallback4(() => {
1436
+ const presentPreloadedWebView = useCallback5(() => {
1205
1437
  return new Promise((resolve) => {
1206
1438
  resolverRef.current = resolve;
1207
1439
  presentedAtRef.current = Date.now();
1208
1440
  setShowingPreloadedWebView(true);
1209
1441
  });
1210
1442
  }, []);
1211
- const resolvePreloadedPaywall = useCallback4((result) => {
1443
+ const resolvePreloadedPaywall = useCallback5((result) => {
1212
1444
  if (resolverRef.current) {
1213
1445
  resolverRef.current(result);
1214
1446
  resolverRef.current = null;
1215
1447
  }
1216
1448
  }, []);
1217
- const handlePreloadedWebViewReady = useCallback4(() => {
1449
+ const handlePreloadedWebViewReady = useCallback5(() => {
1218
1450
  setPreloadedWebView((prev) => prev ? { ...prev, loaded: true } : null);
1219
1451
  }, [setPreloadedWebView]);
1220
- const handlePreloadedClose = useCallback4(() => {
1452
+ const handlePreloadedClose = useCallback5(() => {
1221
1453
  const track = async () => {
1222
1454
  try {
1223
1455
  const apiClient = PaywalloClient.getApiClient();
@@ -1238,9 +1470,8 @@ function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreload
1238
1470
  };
1239
1471
  void track();
1240
1472
  setShowingPreloadedWebView(false);
1241
- clearPreloadedWebView();
1242
1473
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: true, restored: false });
1243
- }, [preloadedWebView, clearPreloadedWebView, resolvePreloadedPaywall]);
1474
+ }, [preloadedWebView, resolvePreloadedPaywall]);
1244
1475
  return { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef };
1245
1476
  }
1246
1477
 
@@ -1447,10 +1678,7 @@ var AffiliateManager = class {
1447
1678
  );
1448
1679
  }
1449
1680
  }
1450
- log(...args) {
1451
- if (this.debug) {
1452
- console.warn("[AffiliateManager]", ...args);
1453
- }
1681
+ log(..._args) {
1454
1682
  }
1455
1683
  };
1456
1684
  var affiliateManager = new AffiliateManager();
@@ -1467,138 +1695,394 @@ var PaywalloAffiliate = {
1467
1695
  getWithdrawals: () => affiliateManager.getWithdrawals()
1468
1696
  };
1469
1697
 
1470
- // src/core/ApiClient.ts
1471
- import { Platform as Platform3 } from "react-native";
1472
-
1473
- // src/core/apiUrlUtils.ts
1474
- var DEFAULT_WEB_URL = "https://paywallo.com.br";
1475
- function deriveWebUrl(baseUrl) {
1476
- try {
1477
- const url = new URL(baseUrl);
1478
- if (url.hostname.startsWith("192.168") || url.hostname === "localhost") {
1479
- url.port = "3000";
1480
- return url.toString().replace(/\/$/, "");
1481
- }
1482
- if (url.hostname.startsWith("api.")) {
1483
- url.hostname = url.hostname.replace("api.", "app.");
1484
- return url.toString().replace(/\/$/, "");
1485
- }
1486
- return DEFAULT_WEB_URL;
1487
- } catch {
1488
- return DEFAULT_WEB_URL;
1489
- }
1490
- }
1491
- function buildConditionalFlagUrl(baseUrl, flagKey, context) {
1492
- const url = new URL(`${baseUrl}/api/v1/conditional-flags/${flagKey}`);
1493
- if (context.platform) url.searchParams.set("platform", context.platform);
1494
- if (context.appVersion) url.searchParams.set("appVersion", context.appVersion);
1495
- if (context.country) url.searchParams.set("country", context.country);
1496
- if (context.distinctId) url.searchParams.set("distinctId", context.distinctId);
1497
- return url.toString();
1498
- }
1499
-
1500
- // src/domains/iap/apiPurchaseMethods.ts
1501
- async function validatePurchase(client, appKey, platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
1502
- const response = await client.post(
1503
- `/purchases/validate/${appKey}`,
1504
- {
1505
- platform,
1506
- receipt,
1507
- productId,
1508
- transactionId,
1509
- priceLocal,
1510
- currency,
1511
- country,
1512
- distinctId,
1513
- ...paywallPlacement !== void 0 && { paywallPlacement },
1514
- ...variantKey !== void 0 && { variantKey }
1515
- },
1516
- false
1517
- );
1518
- return response.data;
1519
- }
1520
- async function getSubscriptionStatus(client, baseUrl, appKey, distinctId) {
1521
- const url = new URL(`${baseUrl}/purchases/status/${appKey}`);
1522
- url.searchParams.set("distinctId", distinctId);
1523
- const response = await client.get(url.toString());
1524
- return response.data;
1525
- }
1526
- async function getEmergencyPaywall(client) {
1527
- try {
1528
- const response = await client.get("/api/v1/emergency-paywall");
1529
- if (response.status === 404) return { enabled: false };
1530
- return response.data;
1531
- } catch {
1532
- return { enabled: false };
1533
- }
1534
- }
1535
- async function getCampaignData(client, baseUrl, placement, distinctId, context) {
1536
- const url = new URL(`${baseUrl}/api/v1/campaigns/${placement}`);
1537
- url.searchParams.set("distinctId", distinctId);
1538
- if (context) url.searchParams.set("context", JSON.stringify(context));
1539
- const response = await client.get(url.toString());
1540
- if (response.status === 404) return null;
1541
- const data = response.data;
1542
- if ("paywall" in data && data.paywall === null && !("campaignId" in data)) return null;
1543
- return data;
1698
+ // src/domains/subscription/SubscriptionCache.ts
1699
+ var LEGACY_CACHE_KEY = "subscription_cache";
1700
+ var CACHE_KEY_PREFIX = "subscription_cache:";
1701
+ var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
1702
+ function keyFor(distinctId) {
1703
+ return `${CACHE_KEY_PREFIX}${distinctId}`;
1544
1704
  }
1545
-
1546
- // src/core/http/types.ts
1547
- var DEFAULT_RETRY_CONFIG = {
1548
- maxRetries: 2,
1549
- baseDelay: 500,
1550
- maxDelay: 4e3,
1551
- retryableStatusCodes: [408, 429, 500, 502, 503, 504]
1552
- };
1553
- var DEFAULT_HTTP_CONFIG = {
1554
- timeout: 1e4,
1555
- retry: DEFAULT_RETRY_CONFIG,
1556
- debug: false
1557
- };
1558
-
1559
- // src/core/http/HttpClient.ts
1560
- var HttpClient = class {
1561
- constructor(baseUrl, config) {
1562
- this.config = {
1563
- baseUrl,
1564
- timeout: config?.timeout ?? DEFAULT_HTTP_CONFIG.timeout,
1565
- retry: { ...DEFAULT_RETRY_CONFIG, ...config?.retry },
1566
- debug: config?.debug ?? false
1567
- };
1568
- }
1569
- async get(path, options) {
1570
- return this.request(path, { ...options, method: "GET" });
1571
- }
1572
- async post(path, body, options) {
1573
- return this.request(path, { ...options, method: "POST", body });
1705
+ var SubscriptionCache = class {
1706
+ constructor(ttl = DEFAULT_TTL) {
1707
+ this.memoryCache = /* @__PURE__ */ new Map();
1708
+ this.legacyCleanupDone = false;
1709
+ this.ttl = ttl;
1710
+ this.storage = new SecureStorage();
1574
1711
  }
1575
- async request(path, options) {
1576
- const url = this.buildUrl(path);
1577
- const fetchOptions = this.buildFetchOptions(options);
1578
- const timeout = options.timeout ?? this.config.timeout;
1579
- if (options.skipRetry) {
1580
- return this.executeRequest(url, fetchOptions, timeout);
1712
+ async cleanupLegacyCache() {
1713
+ if (this.legacyCleanupDone) return;
1714
+ this.legacyCleanupDone = true;
1715
+ try {
1716
+ await this.storage.remove(LEGACY_CACHE_KEY);
1717
+ } catch {
1581
1718
  }
1582
- return this.executeWithRetry(url, fetchOptions, options);
1583
1719
  }
1584
- async executeWithRetry(url, fetchOptions, options) {
1585
- const retryConfig = this.config.retry;
1586
- const timeout = options.timeout ?? this.config.timeout;
1587
- const state = { attempt: 0, lastError: null };
1588
- while (state.attempt <= retryConfig.maxRetries) {
1589
- try {
1590
- const response = await this.executeRequest(url, fetchOptions, timeout);
1591
- if (this.shouldRetry(response.status, retryConfig)) {
1592
- if (state.attempt < retryConfig.maxRetries) {
1593
- state.attempt++;
1594
- const delay = this.calculateDelay(state.attempt, retryConfig);
1595
- await this.sleep(delay);
1596
- continue;
1597
- }
1598
- }
1599
- return response;
1600
- } catch (error) {
1601
- state.lastError = error instanceof Error ? error : new ClientError(CLIENT_ERROR_CODES.UNKNOWN, String(error));
1720
+ async get(distinctId) {
1721
+ await this.cleanupLegacyCache();
1722
+ if (!distinctId) return null;
1723
+ const memEntry = this.memoryCache.get(distinctId);
1724
+ if (memEntry && !this.isExpired(memEntry)) {
1725
+ return memEntry;
1726
+ }
1727
+ try {
1728
+ const stored = await this.storage.get(keyFor(distinctId));
1729
+ if (!stored) {
1730
+ return null;
1731
+ }
1732
+ const parsed = JSON.parse(stored);
1733
+ if (typeof parsed !== "object" || parsed === null || !("data" in parsed) || typeof parsed.data !== "object" || parsed.data === null || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") {
1734
+ await this.storage.remove(keyFor(distinctId));
1735
+ return null;
1736
+ }
1737
+ const cached = parsed;
1738
+ if (cached.data.subscription?.expiresAt) {
1739
+ cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
1740
+ }
1741
+ if (cached.data.subscription?.originalPurchaseDate) {
1742
+ cached.data.subscription.originalPurchaseDate = new Date(
1743
+ cached.data.subscription.originalPurchaseDate
1744
+ );
1745
+ }
1746
+ if (cached.data.subscription?.latestPurchaseDate) {
1747
+ cached.data.subscription.latestPurchaseDate = new Date(
1748
+ cached.data.subscription.latestPurchaseDate
1749
+ );
1750
+ }
1751
+ if (cached.data.subscription?.cancellationDate) {
1752
+ cached.data.subscription.cancellationDate = new Date(
1753
+ cached.data.subscription.cancellationDate
1754
+ );
1755
+ }
1756
+ if (cached.data.subscription?.gracePeriodExpiresAt) {
1757
+ cached.data.subscription.gracePeriodExpiresAt = new Date(
1758
+ cached.data.subscription.gracePeriodExpiresAt
1759
+ );
1760
+ }
1761
+ cached.isStale = this.isExpired(cached);
1762
+ if (!cached.isStale) {
1763
+ this.memoryCache.set(distinctId, cached);
1764
+ }
1765
+ return cached;
1766
+ } catch {
1767
+ return null;
1768
+ }
1769
+ }
1770
+ async set(distinctId, data) {
1771
+ if (!distinctId) return;
1772
+ const cached = {
1773
+ data,
1774
+ cachedAt: Date.now(),
1775
+ isStale: false
1776
+ };
1777
+ this.memoryCache.set(distinctId, cached);
1778
+ try {
1779
+ await this.storage.set(keyFor(distinctId), JSON.stringify(cached));
1780
+ } catch {
1781
+ }
1782
+ }
1783
+ async invalidate(distinctId) {
1784
+ if (!distinctId) return;
1785
+ this.memoryCache.delete(distinctId);
1786
+ try {
1787
+ await this.storage.remove(keyFor(distinctId));
1788
+ } catch {
1789
+ }
1790
+ }
1791
+ async invalidateAll() {
1792
+ this.memoryCache.clear();
1793
+ try {
1794
+ await this.storage.remove(LEGACY_CACHE_KEY);
1795
+ } catch {
1796
+ }
1797
+ }
1798
+ isExpired(cached) {
1799
+ return Date.now() - cached.cachedAt > this.ttl;
1800
+ }
1801
+ setTTL(ttl) {
1802
+ this.ttl = ttl;
1803
+ }
1804
+ };
1805
+ var subscriptionCache = new SubscriptionCache();
1806
+
1807
+ // src/domains/subscription/SubscriptionManager.ts
1808
+ import { Platform as Platform4 } from "react-native";
1809
+
1810
+ // src/domains/session/SessionError.ts
1811
+ var SessionError = class extends PaywalloError {
1812
+ constructor(code, message) {
1813
+ super("session", code, message);
1814
+ this.name = "SessionError";
1815
+ }
1816
+ };
1817
+ var SESSION_ERROR_CODES = {
1818
+ NOT_INITIALIZED: "SESSION_NOT_INITIALIZED",
1819
+ START_FAILED: "SESSION_START_FAILED",
1820
+ END_FAILED: "SESSION_END_FAILED",
1821
+ RESTORE_FAILED: "SESSION_RESTORE_FAILED"
1822
+ };
1823
+
1824
+ // src/domains/subscription/SubscriptionManager.ts
1825
+ var SubscriptionManagerClass = class {
1826
+ constructor() {
1827
+ this.config = null;
1828
+ this.listeners = /* @__PURE__ */ new Set();
1829
+ this.userId = null;
1830
+ this.cache = new SubscriptionCache();
1831
+ }
1832
+ cacheKey() {
1833
+ return this.userId ?? "__anonymous__";
1834
+ }
1835
+ init(config) {
1836
+ this.config = config;
1837
+ if (config.cacheTTL) {
1838
+ this.cache.setTTL(config.cacheTTL);
1839
+ }
1840
+ }
1841
+ setUserId(userId) {
1842
+ if (this.userId !== userId) {
1843
+ this.userId = userId;
1844
+ void this.cache.invalidateAll();
1845
+ }
1846
+ }
1847
+ async hasActiveSubscription(forceRefresh = false) {
1848
+ const status = await this.getSubscriptionStatus(forceRefresh);
1849
+ return status.hasActiveSubscription;
1850
+ }
1851
+ async getSubscription(forceRefresh = false) {
1852
+ const status = await this.getSubscriptionStatus(forceRefresh);
1853
+ return status.subscription;
1854
+ }
1855
+ async getEntitlements(forceRefresh = false) {
1856
+ const status = await this.getSubscriptionStatus(forceRefresh);
1857
+ return status.entitlements;
1858
+ }
1859
+ async getSubscriptionStatus(forceRefresh = false) {
1860
+ if (!this.config) {
1861
+ return this.getEmptyStatus();
1862
+ }
1863
+ if (!forceRefresh) {
1864
+ const cached = await this.cache.get(this.cacheKey());
1865
+ if (cached && !cached.isStale) {
1866
+ return cached.data;
1867
+ }
1868
+ }
1869
+ try {
1870
+ const status = await this.fetchSubscriptionStatus();
1871
+ await this.cache.set(this.cacheKey(), status);
1872
+ this.notifyListeners(status);
1873
+ return status;
1874
+ } catch (error) {
1875
+ this.log("Failed to fetch subscription status", error);
1876
+ const cached = await this.cache.get(this.cacheKey());
1877
+ if (cached) {
1878
+ return cached.data;
1879
+ }
1880
+ return this.getEmptyStatus();
1881
+ }
1882
+ }
1883
+ async restorePurchases() {
1884
+ if (!this.config) {
1885
+ throw new SessionError(
1886
+ SESSION_ERROR_CODES.NOT_INITIALIZED,
1887
+ "SubscriptionManager not initialized"
1888
+ );
1889
+ }
1890
+ await this.cache.invalidate(this.cacheKey());
1891
+ return this.getSubscriptionStatus(true);
1892
+ }
1893
+ async fetchSubscriptionStatus() {
1894
+ if (!this.config) {
1895
+ return this.getEmptyStatus();
1896
+ }
1897
+ const url = new URL(`${this.config.serverUrl}/subscriptions/status`);
1898
+ if (this.userId) {
1899
+ url.searchParams.set("userId", this.userId);
1900
+ }
1901
+ url.searchParams.set("platform", Platform4.OS);
1902
+ const response = await fetch(url.toString(), {
1903
+ method: "GET",
1904
+ headers: {
1905
+ "Content-Type": "application/json",
1906
+ "X-App-Key": this.config.appKey
1907
+ }
1908
+ });
1909
+ if (!response.ok) {
1910
+ throw new SessionError(
1911
+ SESSION_ERROR_CODES.RESTORE_FAILED,
1912
+ `Failed to fetch subscription status: ${response.status}`
1913
+ );
1914
+ }
1915
+ const data = await response.json();
1916
+ if (data.subscription) {
1917
+ data.subscription.expiresAt = new Date(data.subscription.expiresAt);
1918
+ data.subscription.originalPurchaseDate = new Date(data.subscription.originalPurchaseDate);
1919
+ data.subscription.latestPurchaseDate = new Date(data.subscription.latestPurchaseDate);
1920
+ if (data.subscription.cancellationDate) {
1921
+ data.subscription.cancellationDate = new Date(data.subscription.cancellationDate);
1922
+ }
1923
+ if (data.subscription.gracePeriodExpiresAt) {
1924
+ data.subscription.gracePeriodExpiresAt = new Date(data.subscription.gracePeriodExpiresAt);
1925
+ }
1926
+ }
1927
+ return data;
1928
+ }
1929
+ getEmptyStatus() {
1930
+ return {
1931
+ hasActiveSubscription: false,
1932
+ subscription: null,
1933
+ entitlements: []
1934
+ };
1935
+ }
1936
+ addListener(listener) {
1937
+ this.listeners.add(listener);
1938
+ return () => this.listeners.delete(listener);
1939
+ }
1940
+ notifyListeners(status) {
1941
+ for (const listener of this.listeners) {
1942
+ listener(status);
1943
+ }
1944
+ }
1945
+ async onPurchaseComplete() {
1946
+ await this.cache.invalidate(this.cacheKey());
1947
+ await this.getSubscriptionStatus(true);
1948
+ }
1949
+ log(..._args) {
1950
+ }
1951
+ };
1952
+ var subscriptionManager = new SubscriptionManagerClass();
1953
+
1954
+ // src/core/ApiClient.ts
1955
+ import { Platform as Platform5 } from "react-native";
1956
+
1957
+ // src/core/apiUrlUtils.ts
1958
+ var DEFAULT_WEB_URL = "https://paywallo.com.br";
1959
+ function deriveWebUrl(baseUrl) {
1960
+ try {
1961
+ const url = new URL(baseUrl);
1962
+ if (url.hostname.startsWith("192.168") || url.hostname === "localhost") {
1963
+ url.port = "3000";
1964
+ return url.toString().replace(/\/$/, "");
1965
+ }
1966
+ if (url.hostname.startsWith("api.")) {
1967
+ url.hostname = url.hostname.replace("api.", "app.");
1968
+ return url.toString().replace(/\/$/, "");
1969
+ }
1970
+ return DEFAULT_WEB_URL;
1971
+ } catch {
1972
+ return DEFAULT_WEB_URL;
1973
+ }
1974
+ }
1975
+ function buildConditionalFlagUrl(baseUrl, flagKey, context) {
1976
+ const url = new URL(`${baseUrl}/api/v1/conditional-flags/${flagKey}`);
1977
+ if (context.platform) url.searchParams.set("platform", context.platform);
1978
+ if (context.appVersion) url.searchParams.set("appVersion", context.appVersion);
1979
+ if (context.country) url.searchParams.set("country", context.country);
1980
+ if (context.distinctId) url.searchParams.set("distinctId", context.distinctId);
1981
+ return url.toString();
1982
+ }
1983
+
1984
+ // src/domains/iap/apiPurchaseMethods.ts
1985
+ async function validatePurchase(client, appKey, platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
1986
+ const response = await client.post(
1987
+ `/purchases/validate/${appKey}`,
1988
+ {
1989
+ platform,
1990
+ receipt,
1991
+ productId,
1992
+ transactionId,
1993
+ priceLocal,
1994
+ currency,
1995
+ country,
1996
+ distinctId,
1997
+ ...paywallPlacement !== void 0 && { paywallPlacement },
1998
+ ...variantKey !== void 0 && { variantKey }
1999
+ },
2000
+ false
2001
+ );
2002
+ return response.data;
2003
+ }
2004
+ async function getSubscriptionStatus(client, baseUrl, appKey, distinctId) {
2005
+ const url = new URL(`${baseUrl}/purchases/status/${appKey}`);
2006
+ url.searchParams.set("distinctId", distinctId);
2007
+ const response = await client.get(url.toString());
2008
+ return response.data;
2009
+ }
2010
+ async function getEmergencyPaywall(client) {
2011
+ try {
2012
+ const response = await client.get("/api/v1/emergency-paywall");
2013
+ if (response.status === 404) return { enabled: false };
2014
+ return response.data;
2015
+ } catch {
2016
+ return { enabled: false };
2017
+ }
2018
+ }
2019
+ async function getCampaignData(client, baseUrl, placement, distinctId, context) {
2020
+ const url = new URL(`${baseUrl}/api/v1/campaigns/${placement}`);
2021
+ url.searchParams.set("distinctId", distinctId);
2022
+ if (context) url.searchParams.set("context", JSON.stringify(context));
2023
+ const response = await client.get(url.toString());
2024
+ if (response.status === 404) return null;
2025
+ const data = response.data;
2026
+ if ("paywall" in data && data.paywall === null && !("campaignId" in data)) return null;
2027
+ return data;
2028
+ }
2029
+
2030
+ // src/core/http/types.ts
2031
+ var DEFAULT_RETRY_CONFIG = {
2032
+ maxRetries: 2,
2033
+ baseDelay: 500,
2034
+ maxDelay: 4e3,
2035
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504]
2036
+ };
2037
+ var DEFAULT_HTTP_CONFIG = {
2038
+ timeout: 1e4,
2039
+ retry: DEFAULT_RETRY_CONFIG,
2040
+ debug: false
2041
+ };
2042
+
2043
+ // src/core/http/HttpClient.ts
2044
+ var HttpClient = class {
2045
+ constructor(baseUrl, config) {
2046
+ this.config = {
2047
+ baseUrl,
2048
+ timeout: config?.timeout ?? DEFAULT_HTTP_CONFIG.timeout,
2049
+ retry: { ...DEFAULT_RETRY_CONFIG, ...config?.retry },
2050
+ debug: config?.debug ?? false
2051
+ };
2052
+ }
2053
+ async get(path, options) {
2054
+ return this.request(path, { ...options, method: "GET" });
2055
+ }
2056
+ async post(path, body, options) {
2057
+ return this.request(path, { ...options, method: "POST", body });
2058
+ }
2059
+ async request(path, options) {
2060
+ const url = this.buildUrl(path);
2061
+ const fetchOptions = this.buildFetchOptions(options);
2062
+ const timeout = options.timeout ?? this.config.timeout;
2063
+ if (options.skipRetry) {
2064
+ return this.executeRequest(url, fetchOptions, timeout);
2065
+ }
2066
+ return this.executeWithRetry(url, fetchOptions, options);
2067
+ }
2068
+ async executeWithRetry(url, fetchOptions, options) {
2069
+ const retryConfig = this.config.retry;
2070
+ const timeout = options.timeout ?? this.config.timeout;
2071
+ const state = { attempt: 0, lastError: null };
2072
+ while (state.attempt <= retryConfig.maxRetries) {
2073
+ try {
2074
+ const response = await this.executeRequest(url, fetchOptions, timeout);
2075
+ if (this.shouldRetry(response.status, retryConfig)) {
2076
+ if (state.attempt < retryConfig.maxRetries) {
2077
+ state.attempt++;
2078
+ const delay = this.calculateDelay(state.attempt, retryConfig);
2079
+ await this.sleep(delay);
2080
+ continue;
2081
+ }
2082
+ }
2083
+ return response;
2084
+ } catch (error) {
2085
+ state.lastError = error instanceof Error ? error : new ClientError(CLIENT_ERROR_CODES.UNKNOWN, String(error));
1602
2086
  if (this.isNetworkError(state.lastError) && state.attempt < retryConfig.maxRetries) {
1603
2087
  state.attempt++;
1604
2088
  const delay = this.calculateDelay(state.attempt, retryConfig);
@@ -1700,10 +2184,7 @@ var HttpClient = class {
1700
2184
  sleep(ms) {
1701
2185
  return new Promise((resolve) => setTimeout(resolve, ms));
1702
2186
  }
1703
- log(message, data) {
1704
- if (this.config.debug) {
1705
- console.warn("[HttpClient]", message, data ?? "");
1706
- }
2187
+ log(..._args) {
1707
2188
  }
1708
2189
  setDebug(debug) {
1709
2190
  this.config.debug = debug;
@@ -1847,10 +2328,7 @@ var NetworkMonitorClass = class {
1847
2328
  this.initialized = false;
1848
2329
  this.currentState = "unknown";
1849
2330
  }
1850
- log(message, data) {
1851
- if (this.config.debug) {
1852
- console.warn("[NetworkMonitor]", message, data ?? "");
1853
- }
2331
+ log(..._args) {
1854
2332
  }
1855
2333
  setDebug(debug) {
1856
2334
  this.config.debug = debug;
@@ -1880,6 +2358,8 @@ function normalizeForComparison(obj) {
1880
2358
  return "{" + pairs.join(",") + "}";
1881
2359
  }
1882
2360
  function findDuplicateIndex(queue, item) {
2361
+ const byId = queue.findIndex((existing) => existing.id === item.id);
2362
+ if (byId !== -1) return byId;
1883
2363
  const normalizedBody = normalizeForComparison(item.body);
1884
2364
  return queue.findIndex(
1885
2365
  (existing) => existing.url === item.url && normalizeForComparison(existing.body) === normalizedBody
@@ -1948,9 +2428,9 @@ var OfflineQueueClass = class {
1948
2428
  void this.saveToStorage();
1949
2429
  }
1950
2430
  }
1951
- async enqueue(method, url, body, headers) {
2431
+ async enqueue(method, url, body, headers, id) {
1952
2432
  const item = {
1953
- id: generateUUID(),
2433
+ id: id ?? generateUUID(),
1954
2434
  method,
1955
2435
  url,
1956
2436
  body,
@@ -2034,7 +2514,6 @@ var OfflineQueueClass = class {
2034
2514
  this.emit({ type: "processing:end", queueSize: this.queue.length });
2035
2515
  return { processed, failed };
2036
2516
  }
2037
- /** Returns true if the item was permanently removed (counted as failed). */
2038
2517
  async handleItemFailure(item, error, permanent = false) {
2039
2518
  if (permanent || item.attempts >= item.maxAttempts) {
2040
2519
  await this.removeItem(item.id);
@@ -2096,10 +2575,7 @@ var OfflineQueueClass = class {
2096
2575
  }
2097
2576
  }
2098
2577
  }
2099
- log(message, data) {
2100
- if (this.config.debug) {
2101
- console.warn("[OfflineQueue]", message, data ?? "");
2102
- }
2578
+ log(..._args) {
2103
2579
  }
2104
2580
  setDebug(debug) {
2105
2581
  this.config.debug = debug;
@@ -2241,10 +2717,7 @@ var QueueProcessorClass = class {
2241
2717
  this.httpClient = null;
2242
2718
  this.initialized = false;
2243
2719
  }
2244
- log(message, data) {
2245
- if (this.config.debug) {
2246
- console.warn("[QueueProcessor]", message, data ?? "");
2247
- }
2720
+ log(..._args) {
2248
2721
  }
2249
2722
  setDebug(debug) {
2250
2723
  this.config.debug = debug;
@@ -2256,10 +2729,9 @@ var queueProcessor = new QueueProcessorClass();
2256
2729
  var ApiCache = class {
2257
2730
  constructor() {
2258
2731
  this.CACHE_TTL = 3e5;
2259
- // 5 minutes
2260
2732
  this.CACHE_NULL_TTL = 3e4;
2261
- // 30 seconds — for null/404 variants
2262
2733
  this.MAX_SIZE = 200;
2734
+ this.STALE_WINDOW_MS = 864e5;
2263
2735
  this.variantCache = /* @__PURE__ */ new Map();
2264
2736
  this.paywallCache = /* @__PURE__ */ new Map();
2265
2737
  this.campaignCache = /* @__PURE__ */ new Map();
@@ -2297,7 +2769,7 @@ var ApiCache = class {
2297
2769
  }
2298
2770
  getStaleVariant(key) {
2299
2771
  const entry = this.variantCache.get(key);
2300
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2772
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2301
2773
  return void 0;
2302
2774
  }
2303
2775
  getPaywall(key) {
@@ -2309,19 +2781,19 @@ var ApiCache = class {
2309
2781
  }
2310
2782
  getStalePaywall(key) {
2311
2783
  const entry = this.paywallCache.get(key);
2312
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2784
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2313
2785
  return void 0;
2314
2786
  }
2315
2787
  getCampaign(key) {
2316
2788
  return this.getCached(this.campaignCache, key);
2317
2789
  }
2318
- setCampaign(key, value) {
2319
- this.setCached(this.campaignCache, key, value);
2790
+ setCampaign(key, value, ttlOverride) {
2791
+ this.setCached(this.campaignCache, key, value, ttlOverride);
2320
2792
  this.evictExpiredEntries(this.campaignCache);
2321
2793
  }
2322
2794
  getStaleCampaign(key) {
2323
2795
  const entry = this.campaignCache.get(key);
2324
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2796
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2325
2797
  return void 0;
2326
2798
  }
2327
2799
  };
@@ -2329,7 +2801,7 @@ var ApiCache = class {
2329
2801
  // src/core/ApiClient.ts
2330
2802
  var SDK_VERSION = "1.0.0";
2331
2803
  var DEFAULT_BASE_URL = "https://panel.lucasqueiroga.shop";
2332
- var ApiClient = class {
2804
+ var _ApiClient = class _ApiClient {
2333
2805
  constructor(appKey, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
2334
2806
  this.cache = new ApiCache();
2335
2807
  this.appKey = appKey;
@@ -2340,11 +2812,12 @@ var ApiClient = class {
2340
2812
  this.offlineQueueEnabled = offlineQueueEnabled;
2341
2813
  this.httpClient = new HttpClient(this.baseUrl, { timeout, debug });
2342
2814
  }
2343
- // ─── Infrastructure ──────────────────────────────────────────────────────────
2344
- /** Exposed only for QueueProcessor initialization. Prefer get()/post() for requests. */
2345
2815
  getHttpClient() {
2346
2816
  return this.httpClient;
2347
2817
  }
2818
+ getBaseUrl() {
2819
+ return this.baseUrl;
2820
+ }
2348
2821
  getWebUrl() {
2349
2822
  return this.webUrl;
2350
2823
  }
@@ -2370,7 +2843,6 @@ var ApiClient = class {
2370
2843
  async clearQueue() {
2371
2844
  await offlineQueue.clear();
2372
2845
  }
2373
- // ─── Public HTTP facade (headers injected automatically) ─────────────────────
2374
2846
  async get(path) {
2375
2847
  return this.httpClient.get(path, { headers: { "X-App-Key": this.appKey } });
2376
2848
  }
@@ -2384,29 +2856,28 @@ var ApiClient = class {
2384
2856
  message,
2385
2857
  context,
2386
2858
  sdkVersion: SDK_VERSION,
2387
- platform: Platform3.OS
2859
+ platform: Platform5.OS
2388
2860
  }, true);
2389
2861
  } catch {
2390
2862
  }
2391
2863
  }
2392
- // ─── Business methods ────────────────────────────────────────────────────────
2393
2864
  async trackEvent(eventName, distinctId, properties, timestamp) {
2394
2865
  await this.postWithQueue("/api/v1/events", {
2395
2866
  eventName,
2396
2867
  distinctId,
2397
- properties: { ...properties, platform: Platform3.OS },
2868
+ properties: { ...properties, platform: Platform5.OS },
2398
2869
  timestamp: timestamp ?? Date.now(),
2399
2870
  environment: this.environment.toLowerCase()
2400
2871
  }, `event:${eventName}`);
2401
2872
  }
2402
- async identify(distinctId, properties, email) {
2403
- await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: Platform3.OS }, "identify");
2873
+ async identify(distinctId, properties, email, deviceId) {
2874
+ await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: Platform5.OS, ...deviceId && { deviceId } }, "identify");
2404
2875
  }
2405
2876
  async startSession(distinctId, sessionId, platform) {
2406
2877
  await this.postWithQueue("/api/v1/sessions/start", {
2407
2878
  distinctId,
2408
2879
  sessionId,
2409
- platform: platform ?? Platform3.OS,
2880
+ platform: platform ?? Platform5.OS,
2410
2881
  environment: this.environment.toLowerCase()
2411
2882
  }, `session.start:${sessionId}`);
2412
2883
  return { success: true };
@@ -2419,6 +2890,11 @@ var ApiClient = class {
2419
2890
  const key = `${flagKey}:${distinctId}`;
2420
2891
  const hit = this.cache.getVariant(key);
2421
2892
  if (hit !== void 0) return hit;
2893
+ const persisted = await this.readFlagFromStorage(flagKey, distinctId);
2894
+ if (persisted !== null) {
2895
+ this.cache.setVariant(key, persisted);
2896
+ return persisted;
2897
+ }
2422
2898
  try {
2423
2899
  const res = await this.get(`/api/v1/flags/${flagKey}?distinctId=${encodeURIComponent(distinctId)}`);
2424
2900
  if (res.status === 404) {
@@ -2433,6 +2909,7 @@ var ApiClient = class {
2433
2909
  return { variant: null };
2434
2910
  }
2435
2911
  this.cache.setVariant(key, res.data);
2912
+ await this.writeFlagToStorage(flagKey, distinctId, res.data);
2436
2913
  return res.data;
2437
2914
  } catch (error) {
2438
2915
  this.log("Failed to get variant:", flagKey, error);
@@ -2478,7 +2955,7 @@ var ApiClient = class {
2478
2955
  const result = await getCampaignData(this, this.baseUrl, placement, distinctId, context);
2479
2956
  if (!result) {
2480
2957
  this.log("No campaign found for placement:", placement);
2481
- this.cache.setCampaign(key, null);
2958
+ this.cache.setCampaign(key, null, this.cache.nullTTL);
2482
2959
  return null;
2483
2960
  }
2484
2961
  this.log("Got campaign:", placement, result.variantKey);
@@ -2491,6 +2968,32 @@ var ApiClient = class {
2491
2968
  return null;
2492
2969
  }
2493
2970
  }
2971
+ async getPrimaryCampaign(distinctId) {
2972
+ const key = `primary:${distinctId}`;
2973
+ const hit = this.cache.getCampaign(key);
2974
+ if (hit !== void 0) return hit;
2975
+ try {
2976
+ const res = await this.get(`/campaigns/public/primary?distinctId=${encodeURIComponent(distinctId)}`);
2977
+ if (res.status === 404) {
2978
+ this.cache.setCampaign(key, null, this.cache.nullTTL);
2979
+ return null;
2980
+ }
2981
+ if (!res.ok) {
2982
+ this.log("Non-OK response for primary campaign:", res.status);
2983
+ const stale = this.cache.getStaleCampaign(key);
2984
+ if (stale !== void 0) return stale;
2985
+ return null;
2986
+ }
2987
+ this.log("Got primary campaign:", res.data?.campaignId);
2988
+ this.cache.setCampaign(key, res.data);
2989
+ return res.data;
2990
+ } catch (error) {
2991
+ this.log("Failed to get primary campaign:", error);
2992
+ const stale = this.cache.getStaleCampaign(key);
2993
+ if (stale !== void 0) return stale;
2994
+ return null;
2995
+ }
2996
+ }
2494
2997
  async validatePurchase(platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
2495
2998
  const result = await validatePurchase(
2496
2999
  this,
@@ -2514,6 +3017,35 @@ var ApiClient = class {
2514
3017
  this.log("Subscription status:", result.hasActiveSubscription);
2515
3018
  return result;
2516
3019
  }
3020
+ async evaluateFlags(keys, distinctId) {
3021
+ const query = keys.map(encodeURIComponent).join(",");
3022
+ const path = `/api/v1/flags/evaluate?keys=${query}`;
3023
+ try {
3024
+ const res = await this.httpClient.get(path, {
3025
+ headers: { "X-App-Key": this.appKey, "x-distinct-id": distinctId }
3026
+ });
3027
+ if (!res.ok) {
3028
+ this.log("Non-OK response for evaluateFlags:", res.status);
3029
+ return {};
3030
+ }
3031
+ const raw = res.data;
3032
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
3033
+ const record = raw;
3034
+ const result = {};
3035
+ for (const key of Object.keys(record)) {
3036
+ const value = record[key];
3037
+ const variant = typeof value === "boolean" ? String(value) : typeof value === "string" ? value : null;
3038
+ result[key] = variant;
3039
+ const flagVariant = { variant };
3040
+ this.cache.setVariant(`${key}:${distinctId}`, flagVariant);
3041
+ await this.writeFlagToStorage(key, distinctId, flagVariant);
3042
+ }
3043
+ return result;
3044
+ } catch (error) {
3045
+ this.log("Failed to evaluateFlags:", error);
3046
+ return {};
3047
+ }
3048
+ }
2517
3049
  async getConditionalFlag(flagKey, context) {
2518
3050
  try {
2519
3051
  const url = buildConditionalFlagUrl(this.baseUrl, flagKey, context);
@@ -2530,11 +3062,37 @@ var ApiClient = class {
2530
3062
  return { value: false, flagKey };
2531
3063
  }
2532
3064
  }
2533
- // ─── Private helpers ─────────────────────────────────────────────────────────
2534
- /**
2535
- * POST with offline queue fallback. Enqueues when offline or on network failure.
2536
- * Throws only when offlineQueueEnabled is false and the request fails.
2537
- */
3065
+ async getVariantFromCache(flagKey, distinctId) {
3066
+ const key = `${flagKey}:${distinctId}`;
3067
+ const hit = this.cache.getVariant(key);
3068
+ if (hit !== void 0) return hit;
3069
+ const persisted = await this.readFlagFromStorage(flagKey, distinctId);
3070
+ if (persisted !== null) {
3071
+ this.cache.setVariant(key, persisted);
3072
+ return persisted;
3073
+ }
3074
+ return null;
3075
+ }
3076
+ async readFlagFromStorage(flagKey, distinctId) {
3077
+ try {
3078
+ const raw = await secureStorage.get(`flag:${flagKey}:${distinctId}`);
3079
+ if (!raw) return null;
3080
+ const parsed = JSON.parse(raw);
3081
+ if (typeof parsed !== "object" || parsed === null || !("variant" in parsed) || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") return null;
3082
+ const { variant, payload, cachedAt } = parsed;
3083
+ if (Date.now() - cachedAt > _ApiClient.FLAG_MAX_AGE_MS) return null;
3084
+ return { variant, payload };
3085
+ } catch {
3086
+ return null;
3087
+ }
3088
+ }
3089
+ async writeFlagToStorage(flagKey, distinctId, flag) {
3090
+ try {
3091
+ const value = JSON.stringify({ variant: flag.variant, payload: flag.payload, cachedAt: Date.now() });
3092
+ await secureStorage.set(`flag:${flagKey}:${distinctId}`, value);
3093
+ } catch {
3094
+ }
3095
+ }
2538
3096
  async postWithQueue(url, payload, label) {
2539
3097
  if (this.offlineQueueEnabled && !networkMonitor.isOnline()) {
2540
3098
  await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
@@ -2553,10 +3111,11 @@ var ApiClient = class {
2553
3111
  throw error;
2554
3112
  }
2555
3113
  }
2556
- log(...args) {
2557
- if (this.debug) console.warn("[Paywallo API]", ...args);
3114
+ log(..._args) {
2558
3115
  }
2559
3116
  };
3117
+ _ApiClient.FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
3118
+ var ApiClient = _ApiClient;
2560
3119
 
2561
3120
  // src/domains/campaign/CampaignError.ts
2562
3121
  var CampaignError = class extends PaywalloError {
@@ -2575,39 +3134,72 @@ var CAMPAIGN_ERROR_CODES = {
2575
3134
 
2576
3135
  // src/domains/campaign/CampaignGateService.ts
2577
3136
  var PRELOAD_CACHE_TTL_MS = 5 * 60 * 1e3;
3137
+ var PRELOAD_WAIT_MAX_MS = 2e3;
3138
+ var PRELOAD_WAIT_INTERVAL_MS = 100;
2578
3139
  var CampaignGateService = class {
2579
3140
  constructor() {
2580
3141
  this.preloadedCampaigns = /* @__PURE__ */ new Map();
3142
+ this.pendingPreloads = /* @__PURE__ */ new Set();
2581
3143
  }
2582
3144
  async getCampaign(apiClient, placement, context) {
2583
3145
  const distinctId = identityManager.getDistinctId();
2584
3146
  if (!distinctId) return null;
2585
3147
  return apiClient.getCampaign(placement, distinctId, context);
2586
3148
  }
2587
- async presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context) {
2588
- const campaign = await this.getCampaign(apiClient, placement, context);
3149
+ async waitForPreload(placement) {
3150
+ if (!this.pendingPreloads.has(placement)) return;
3151
+ const deadline = Date.now() + PRELOAD_WAIT_MAX_MS;
3152
+ await new Promise((resolve) => {
3153
+ const check = () => {
3154
+ if (!this.pendingPreloads.has(placement) || Date.now() >= deadline) {
3155
+ resolve();
3156
+ return;
3157
+ }
3158
+ setTimeout(check, PRELOAD_WAIT_INTERVAL_MS);
3159
+ };
3160
+ check();
3161
+ });
3162
+ }
3163
+ async presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
3164
+ await this.waitForPreload(placement);
3165
+ const preloaded = this.getPreloadedCampaign(placement);
3166
+ const campaign = preloaded ?? await this.getCampaign(apiClient, placement, context);
2589
3167
  if (!campaign) {
2590
3168
  return { presented: false, purchased: false, cancelled: false, restored: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, "No campaign found") };
2591
3169
  }
3170
+ if (!context?.forceShow) {
3171
+ const isActive = await hasActive();
3172
+ if (isActive) {
3173
+ return { presented: false, purchased: false, cancelled: false, restored: false, skippedReason: "subscriber" };
3174
+ }
3175
+ }
2592
3176
  if (campaignPresenter) return campaignPresenter(campaign);
2593
3177
  if (!paywallPresenter) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
2594
- return { ...await paywallPresenter(campaign.paywall.placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
3178
+ return { ...await paywallPresenter(placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
2595
3179
  }
2596
3180
  async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2597
- if (await hasActive()) return true;
2598
- const r = await this.presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context);
3181
+ const isActive = await hasActive();
3182
+ if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo] requireSubscription \u2014 hasActive: ${isActive}`);
3183
+ if (isActive) return true;
3184
+ const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context);
2599
3185
  return r.purchased || r.restored;
2600
3186
  }
2601
3187
  async preloadCampaign(apiClient, placement, context) {
3188
+ this.pendingPreloads.add(placement);
2602
3189
  try {
2603
3190
  const campaign = await this.getCampaign(apiClient, placement, context);
2604
3191
  if (!campaign?.paywall) {
2605
3192
  return { success: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, `Campaign or paywall not found: ${placement}`) };
2606
3193
  }
2607
3194
  this.preloadedCampaigns.set(placement, { campaign, preloadedAt: Date.now() });
3195
+ if (PaywalloClient.getConfig()?.debug) {
3196
+ console.info(`[Paywallo PRELOAD] data cached \u2014 placement: ${placement}`);
3197
+ }
2608
3198
  return { success: true };
2609
3199
  } catch (error) {
2610
3200
  return { success: false, error: error instanceof Error ? error : new PaywalloError("panel", "PANEL_UNKNOWN", String(error)) };
3201
+ } finally {
3202
+ this.pendingPreloads.delete(placement);
2611
3203
  }
2612
3204
  }
2613
3205
  getPreloadedCampaign(placement) {
@@ -2621,12 +3213,13 @@ var CampaignGateService = class {
2621
3213
  }
2622
3214
  clearPreloaded() {
2623
3215
  this.preloadedCampaigns.clear();
3216
+ this.pendingPreloads.clear();
2624
3217
  }
2625
3218
  };
2626
3219
  var campaignGateService = new CampaignGateService();
2627
3220
 
2628
3221
  // src/domains/identity/AdvertisingIdManager.ts
2629
- import { Platform as Platform4 } from "react-native";
3222
+ import { Platform as Platform6 } from "react-native";
2630
3223
  var ZERO_IDFA = "00000000-0000-0000-0000-000000000000";
2631
3224
  var AdvertisingIdManager = class {
2632
3225
  constructor() {
@@ -2637,7 +3230,7 @@ var AdvertisingIdManager = class {
2637
3230
  const fallback = { idfv: null, idfa: null, gaid: null, attStatus: "unavailable" };
2638
3231
  try {
2639
3232
  const tracking = await import("expo-tracking-transparency");
2640
- if (Platform4.OS === "ios") {
3233
+ if (Platform6.OS === "ios") {
2641
3234
  if (!tracking.isAvailable()) {
2642
3235
  const idfv2 = await this.collectIdfv();
2643
3236
  this.cached = { ...fallback, idfv: idfv2 };
@@ -2656,7 +3249,7 @@ var AdvertisingIdManager = class {
2656
3249
  this.cached = { idfv, idfa, gaid: null, attStatus: status };
2657
3250
  return this.cached;
2658
3251
  }
2659
- if (Platform4.OS === "android") {
3252
+ if (Platform6.OS === "android") {
2660
3253
  const androidStatus = await tracking.getTrackingPermissionsAsync();
2661
3254
  const optedIn = androidStatus.granted;
2662
3255
  const rawGaid = optedIn ? tracking.getAdvertisingId() : null;
@@ -2715,14 +3308,14 @@ var FacebookIdManager = class {
2715
3308
  var facebookIdManager = new FacebookIdManager();
2716
3309
 
2717
3310
  // src/domains/identity/InstallTracker.ts
2718
- import { Dimensions as Dimensions3, Platform as Platform6 } from "react-native";
3311
+ import { Dimensions as Dimensions3, Platform as Platform8 } from "react-native";
2719
3312
 
2720
3313
  // src/domains/identity/InstallReferrerManager.ts
2721
- import { Platform as Platform5 } from "react-native";
3314
+ import { Platform as Platform7 } from "react-native";
2722
3315
  var REFERRER_TIMEOUT_MS = 5e3;
2723
3316
  var InstallReferrerManager = class {
2724
3317
  async getReferrer() {
2725
- if (Platform5.OS !== "android") {
3318
+ if (Platform7.OS !== "android") {
2726
3319
  return this.emptyResult();
2727
3320
  }
2728
3321
  let timerId;
@@ -2784,20 +3377,6 @@ var installReferrerManager = new InstallReferrerManager();
2784
3377
  // src/domains/session/SessionManager.ts
2785
3378
  import { AppState as AppState2 } from "react-native";
2786
3379
 
2787
- // src/domains/session/SessionError.ts
2788
- var SessionError = class extends PaywalloError {
2789
- constructor(code, message) {
2790
- super("session", code, message);
2791
- this.name = "SessionError";
2792
- }
2793
- };
2794
- var SESSION_ERROR_CODES = {
2795
- NOT_INITIALIZED: "SESSION_NOT_INITIALIZED",
2796
- START_FAILED: "SESSION_START_FAILED",
2797
- END_FAILED: "SESSION_END_FAILED",
2798
- RESTORE_FAILED: "SESSION_RESTORE_FAILED"
2799
- };
2800
-
2801
3380
  // src/domains/session/sessionTracking.ts
2802
3381
  async function trackSessionStart(apiClient, sessionId, sessionStartedAt) {
2803
3382
  const distinctId = identityManager.getDistinctId();
@@ -3025,10 +3604,7 @@ var SessionManager = class {
3025
3604
  );
3026
3605
  }
3027
3606
  }
3028
- log(...args) {
3029
- if (this.debug) {
3030
- console.warn("[Paywallo Session]", ...args);
3031
- }
3607
+ log(..._args) {
3032
3608
  }
3033
3609
  };
3034
3610
  var sessionManager = new SessionManager();
@@ -3083,9 +3659,6 @@ var InstallTracker = class {
3083
3659
  locale
3084
3660
  };
3085
3661
  } catch {
3086
- if (this.debug) {
3087
- console.warn("[Paywallo] Failed to collect device data for install event");
3088
- }
3089
3662
  }
3090
3663
  const [fbAnonId, referrer, adIds] = await Promise.all([
3091
3664
  facebookIdManager.getAnonId(),
@@ -3095,7 +3668,7 @@ var InstallTracker = class {
3095
3668
  try {
3096
3669
  await apiClient.trackEvent("$app_installed", distinctId, {
3097
3670
  installedAt,
3098
- platform: Platform6.OS,
3671
+ platform: Platform8.OS,
3099
3672
  ...sessionId && { sessionId },
3100
3673
  ...deviceData,
3101
3674
  ...fbAnonId && { fbAnonId },
@@ -3116,13 +3689,7 @@ var InstallTracker = class {
3116
3689
  await AsyncStorage3.setItem("@paywallo:install_tracked", String(installedAt));
3117
3690
  } catch {
3118
3691
  }
3119
- if (this.debug && !stored) {
3120
- console.warn("[Paywallo] Install event tracked but flag storage failed \u2014 may re-track on next launch");
3121
- }
3122
- } catch (error) {
3123
- if (this.debug) {
3124
- console.warn("[Paywallo] Failed to track install:", error);
3125
- }
3692
+ } catch {
3126
3693
  }
3127
3694
  }
3128
3695
  };
@@ -3137,6 +3704,8 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3137
3704
  this.config = null;
3138
3705
  this.apiClient = null;
3139
3706
  this.initPromise = null;
3707
+ this.readyPromise = null;
3708
+ this.resolveReady = null;
3140
3709
  this.paywallPresenter = null;
3141
3710
  this.campaignPresenter = null;
3142
3711
  this.subscriptionGetter = null;
@@ -3146,29 +3715,30 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3146
3715
  this.pendingConfig = null;
3147
3716
  this.networkCleanup = null;
3148
3717
  this.initAttempts = 0;
3718
+ this.autoPreloadedPlacement = null;
3719
+ this.autoPreloadPlacementPromise = null;
3720
+ this.sessionFlagsMap = /* @__PURE__ */ new Map();
3149
3721
  }
3150
3722
  async init(config) {
3151
- console.warn("[Paywallo INIT]", "init() called \u2014 appKey:", config.appKey ? config.appKey.substring(0, 8) + "..." : "MISSING");
3723
+ if (!this.readyPromise) {
3724
+ this.readyPromise = new Promise((resolve) => {
3725
+ this.resolveReady = resolve;
3726
+ });
3727
+ }
3152
3728
  if (this.config && this.apiClient) {
3153
- console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3154
3729
  return;
3155
3730
  }
3156
3731
  if (this.initPromise) {
3157
- console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3158
3732
  await this.initPromise;
3159
- console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3160
3733
  return;
3161
3734
  }
3162
3735
  if (!config.appKey) throw new ClientError(CLIENT_ERROR_CODES.MISSING_APP_KEY, "appKey is required");
3163
- console.warn("[Paywallo INIT]", "Starting fresh init...");
3164
3736
  this.pendingConfig = config;
3165
3737
  this.initAttempts = 0;
3166
3738
  this.initPromise = this.initWithRetry(config);
3167
3739
  try {
3168
3740
  await this.initPromise;
3169
- console.warn("[Paywallo INIT]", "init() SUCCESS");
3170
3741
  } catch (error) {
3171
- console.warn("[Paywallo INIT]", "init() FAILED:", error instanceof Error ? error.message : String(error));
3172
3742
  this.initPromise = null;
3173
3743
  this.config = null;
3174
3744
  this.apiClient = null;
@@ -3177,6 +3747,18 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3177
3747
  throw error;
3178
3748
  }
3179
3749
  }
3750
+ isReady() {
3751
+ return this.config !== null && this.apiClient !== null;
3752
+ }
3753
+ waitUntilReady() {
3754
+ if (this.isReady()) return Promise.resolve();
3755
+ if (!this.readyPromise) {
3756
+ this.readyPromise = new Promise((resolve) => {
3757
+ this.resolveReady = resolve;
3758
+ });
3759
+ }
3760
+ return this.readyPromise;
3761
+ }
3180
3762
  async initWithRetry(config) {
3181
3763
  while (this.initAttempts <= _PaywalloClientClass.MAX_INIT_RETRIES) {
3182
3764
  try {
@@ -3191,7 +3773,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3191
3773
  this.initAttempts++;
3192
3774
  if (this.initAttempts > _PaywalloClientClass.MAX_INIT_RETRIES) throw error;
3193
3775
  const delay = _PaywalloClientClass.RETRY_DELAYS[this.initAttempts - 1] ?? 1e4;
3194
- if (config.debug) console.warn("[Paywallo]", `Init attempt ${this.initAttempts} failed, retrying in ${delay}ms`, error);
3195
3776
  await new Promise((resolve) => setTimeout(resolve, delay));
3196
3777
  }
3197
3778
  }
@@ -3202,11 +3783,8 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3202
3783
  this.networkCleanup = networkMonitor.addListener((state) => {
3203
3784
  if (state !== "online") return;
3204
3785
  if (this.config && this.apiClient) return;
3205
- if (config.debug) console.warn("[Paywallo]", "Network recovered \u2014 retrying init");
3206
3786
  this.initPromise = this.initWithRetry(config);
3207
- this.initPromise.then(() => {
3208
- if (config.debug) console.warn("[Paywallo]", "Init succeeded after network recovery");
3209
- }).catch(() => {
3787
+ this.initPromise.catch(() => {
3210
3788
  this.reportInitFailure(config, null, "NETWORK_RECOVERY_FAILED");
3211
3789
  });
3212
3790
  });
@@ -3235,38 +3813,79 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3235
3813
  } catch {
3236
3814
  }
3237
3815
  }
3816
+ async resolveSessionFlags(keys, apiClient, distinctId) {
3817
+ try {
3818
+ const results = await apiClient.evaluateFlags(keys, distinctId);
3819
+ this.sessionFlagsMap.clear();
3820
+ for (const key of keys) {
3821
+ this.sessionFlagsMap.set(key, key in results ? results[key] : null);
3822
+ }
3823
+ } catch (error) {
3824
+ for (const key of keys) {
3825
+ this.sessionFlagsMap.set(key, null);
3826
+ }
3827
+ }
3828
+ }
3238
3829
  async doInit(config) {
3239
3830
  const environment = config.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "Sandbox" : "Production");
3240
3831
  const offlineQueueEnabled = config.offlineQueueEnabled !== false;
3241
- console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3242
3832
  await Promise.all([
3243
3833
  networkMonitor.initialize({ debug: config.debug }),
3244
3834
  offlineQueue.initialize({ debug: config.debug }).then(
3245
3835
  () => offlineQueue.clearItemsWithInvalidAppKey(config.appKey)
3246
3836
  )
3247
3837
  ]);
3248
- console.warn("[Paywallo INIT]", "Step 1 DONE");
3249
- console.warn("[Paywallo INIT]", "Step 2: ApiClient + queueProcessor + affiliate...");
3250
3838
  this.apiClient = new ApiClient(config.appKey, config.debug, environment, offlineQueueEnabled, config.timeout);
3251
3839
  queueProcessor.initialize(this.apiClient.getHttpClient(), { debug: config.debug });
3252
3840
  affiliateManager.initialize(this.apiClient, config.debug);
3253
- console.warn("[Paywallo INIT]", "Step 2 DONE");
3254
- console.warn("[Paywallo INIT]", "Step 3: identityManager + sessionManager...");
3841
+ subscriptionManager.init({
3842
+ serverUrl: this.apiClient.getBaseUrl(),
3843
+ appKey: config.appKey,
3844
+ cacheTTL: config.subscriptionCacheTTL,
3845
+ debug: config.debug
3846
+ });
3255
3847
  await Promise.all([
3256
3848
  identityManager.initialize(this.apiClient, config.debug),
3257
3849
  sessionManager.initialize(this.apiClient, config.sessionConfig, config.debug)
3258
3850
  ]);
3259
3851
  const distinctId = identityManager.getDistinctId();
3260
- console.warn("[Paywallo INIT]", "Step 3 DONE \u2014 distinctId:", distinctId ? distinctId.substring(0, 8) + "..." : "EMPTY");
3261
3852
  if (config.autoStartSession !== false) {
3262
3853
  sessionManager.startSession().catch(() => {
3263
- if (config.debug) console.warn("[Paywallo]", "Session start failed during init \u2014 continuing");
3264
3854
  });
3265
3855
  }
3266
3856
  new InstallTracker(config.debug).trackIfNeeded(this.apiClient, config.requestATT === true).catch(() => {
3267
3857
  });
3858
+ if (config.sessionFlags && config.sessionFlags.length > 0) {
3859
+ await this.resolveSessionFlags(config.sessionFlags, this.apiClient, distinctId);
3860
+ }
3268
3861
  this.config = config;
3269
- console.warn("[Paywallo INIT]", "COMPLETE \u2014 SDK ready, environment:", environment);
3862
+ if (this.resolveReady) {
3863
+ this.resolveReady();
3864
+ this.resolveReady = null;
3865
+ }
3866
+ if (config.debug) console.info("[Paywallo INIT] complete");
3867
+ if (this.apiClient) {
3868
+ const apiClientRef = this.apiClient;
3869
+ if (config.autoPreloadCampaign) {
3870
+ this.autoPreloadedPlacement = config.autoPreloadCampaign;
3871
+ this.autoPreloadPlacementPromise = Promise.resolve(config.autoPreloadCampaign);
3872
+ if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${config.autoPreloadCampaign}`);
3873
+ void campaignGateService.preloadCampaign(apiClientRef, config.autoPreloadCampaign).catch(() => {
3874
+ });
3875
+ } else {
3876
+ this.autoPreloadPlacementPromise = apiClientRef.getPrimaryCampaign(distinctId).then((primary) => {
3877
+ const placement = primary?.placement ?? primary?.paywall?.placement;
3878
+ if (placement) {
3879
+ this.autoPreloadedPlacement = placement;
3880
+ if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${placement}`);
3881
+ void campaignGateService.preloadCampaign(apiClientRef, placement).catch(() => {
3882
+ });
3883
+ return placement;
3884
+ }
3885
+ return null;
3886
+ }).catch(() => null);
3887
+ }
3888
+ }
3270
3889
  }
3271
3890
  async identify(options) {
3272
3891
  this.ensureInitialized();
@@ -3276,7 +3895,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3276
3895
  const apiClient = this.getApiClientOrThrow();
3277
3896
  const distinctId = identityManager.getDistinctId();
3278
3897
  if (!distinctId) {
3279
- console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3280
3898
  return;
3281
3899
  }
3282
3900
  if (!isValidEventName(eventName)) {
@@ -3284,25 +3902,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3284
3902
  throw new ClientError(CLIENT_ERROR_CODES.INVALID_EVENT_NAME, msg);
3285
3903
  }
3286
3904
  const sessionId = sessionManager.getSessionId();
3287
- if (eventName === "$purchase_completed") {
3288
- console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3289
- distinctId: distinctId.substring(0, 8) + "...",
3290
- productId: options?.properties?.productId,
3291
- priceUsd: options?.properties?.priceUsd,
3292
- price: options?.properties?.price,
3293
- currency: options?.properties?.currency,
3294
- placement: options?.properties?.placement
3295
- }));
3296
- } else if (eventName === "$paywall_viewed" || eventName === "$paywall_closed") {
3297
- console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3298
- distinctId: distinctId.substring(0, 8) + "...",
3299
- placement: options?.properties?.placement,
3300
- variantKey: options?.properties?.variantKey,
3301
- timeOnPaywall: options?.properties?.timeOnPaywall
3302
- }));
3303
- } else {
3304
- console.warn("[Paywallo TRACK]", eventName, "distinctId:", distinctId.substring(0, 8) + "...");
3305
- }
3306
3905
  await apiClient.trackEvent(eventName, distinctId, {
3307
3906
  ...identityManager.getProperties(),
3308
3907
  ...options?.properties,
@@ -3313,7 +3912,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3313
3912
  const apiClient = this.getApiClientOrThrow();
3314
3913
  const distinctId = identityManager.getDistinctId();
3315
3914
  if (!distinctId) {
3316
- if (this.config?.debug) console.warn("[Paywallo] Skipping event \u2014 no distinctId available");
3317
3915
  return;
3318
3916
  }
3319
3917
  if (!Number.isInteger(index) || index < 0) {
@@ -3337,7 +3935,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3337
3935
  const apiClient = this.getApiClientOrThrow();
3338
3936
  const distinctId = identityManager.getDistinctId();
3339
3937
  if (!distinctId) {
3340
- if (this.config?.debug) console.warn("[Paywallo] Skipping event \u2014 no distinctId available");
3341
3938
  return;
3342
3939
  }
3343
3940
  if (!actionName || typeof actionName !== "string") {
@@ -3352,22 +3949,32 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3352
3949
  ...sessionId && { sessionId }
3353
3950
  });
3354
3951
  }
3952
+ async getVariantCached(flagKey, defaultValue) {
3953
+ this.ensureInitialized();
3954
+ const apiClient = this.getApiClientOrThrow();
3955
+ const distinctId = identityManager.getDistinctId();
3956
+ const result = await apiClient.getVariantFromCache(flagKey, distinctId);
3957
+ if (result) return result;
3958
+ void apiClient.getVariant(flagKey, distinctId).catch(() => {
3959
+ });
3960
+ return { variant: defaultValue ?? null, payload: void 0 };
3961
+ }
3355
3962
  async getVariant(flagKey) {
3356
3963
  try {
3357
3964
  const distinctId = identityManager.getDistinctId();
3358
3965
  if (!distinctId) {
3359
- console.warn("[Paywallo AB]", "getVariant SKIPPED \u2014 no distinctId", flagKey);
3360
3966
  return { variant: null };
3361
3967
  }
3362
- const result = await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3363
- console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3364
- return result;
3968
+ return await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3365
3969
  } catch (error) {
3366
- if (this.config?.debug) console.warn("[Paywallo]", "getVariant failed \u2014 returning null", error);
3367
3970
  if (this.apiClient) void this.apiClient.reportError("GET_VARIANT_FAILED", error instanceof Error ? error.message : String(error));
3368
3971
  return { variant: null };
3369
3972
  }
3370
3973
  }
3974
+ async evaluateFlags(keys) {
3975
+ this.ensureInitialized();
3976
+ return this.getApiClientOrThrow().evaluateFlags(keys, identityManager.getDistinctId());
3977
+ }
3371
3978
  async getConditionalFlag(flagKey, context) {
3372
3979
  try {
3373
3980
  const distinctId = identityManager.getDistinctId();
@@ -3375,7 +3982,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3375
3982
  const result = await this.getApiClientOrThrow().getConditionalFlag(flagKey, { distinctId, ...context });
3376
3983
  return result.value;
3377
3984
  } catch (error) {
3378
- if (this.config?.debug) console.warn("[Paywallo]", "getConditionalFlag failed \u2014 returning false", error);
3379
3985
  if (this.apiClient) void this.apiClient.reportError("GET_CONDITIONAL_FLAG_FAILED", error instanceof Error ? error.message : String(error));
3380
3986
  return false;
3381
3987
  }
@@ -3393,18 +3999,22 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3393
3999
  }
3394
4000
  async presentCampaign(placement, context) {
3395
4001
  this.ensureInitialized();
3396
- return campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, this.paywallPresenter, this.campaignPresenter, context);
4002
+ return campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, () => this.hasActiveSubscription(), this.paywallPresenter, this.campaignPresenter, context);
3397
4003
  }
3398
4004
  async hasActiveSubscription() {
3399
4005
  this.ensureInitialized();
3400
4006
  try {
3401
- if (this.activeChecker) return await this.activeChecker();
4007
+ if (this.activeChecker) {
4008
+ return await this.activeChecker();
4009
+ }
3402
4010
  const distinctId = identityManager.getDistinctId();
3403
- if (!distinctId || !this.apiClient) return false;
3404
- return (await this.apiClient.getSubscriptionStatus(distinctId)).hasActiveSubscription;
3405
- } catch {
3406
- if (this.config?.debug) console.warn("[Paywallo]", "hasActiveSubscription failed \u2014 returning false");
3407
- if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", "hasActiveSubscription failed");
4011
+ if (!distinctId || !this.apiClient) {
4012
+ return false;
4013
+ }
4014
+ const status = await this.apiClient.getSubscriptionStatus(distinctId);
4015
+ return status.hasActiveSubscription;
4016
+ } catch (error) {
4017
+ if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", error instanceof Error ? error.message : "hasActiveSubscription failed");
3408
4018
  return false;
3409
4019
  }
3410
4020
  }
@@ -3418,7 +4028,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3418
4028
  if (!status.subscription) return null;
3419
4029
  return { ...status.subscription, expiresAt: status.subscription.expiresAt ? new Date(status.subscription.expiresAt) : null };
3420
4030
  } catch {
3421
- if (this.config?.debug) console.warn("[Paywallo]", "getSubscription failed \u2014 returning null");
3422
4031
  return null;
3423
4032
  }
3424
4033
  }
@@ -3429,19 +4038,24 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3429
4038
  }
3430
4039
  async reset() {
3431
4040
  await identityManager.reset();
4041
+ await subscriptionCache.invalidateAll();
3432
4042
  }
3433
4043
  async fullReset() {
3434
4044
  const wasDebug = this.config?.debug ?? false;
3435
4045
  await sessionManager.endSession();
3436
4046
  await identityManager.reset();
4047
+ await subscriptionCache.invalidateAll();
3437
4048
  queueProcessor.dispose();
3438
4049
  offlineQueue.dispose();
3439
4050
  networkMonitor.dispose();
3440
4051
  campaignGateService.clearPreloaded();
3441
4052
  this.cleanupNetworkRecovery();
4053
+ this.sessionFlagsMap.clear();
3442
4054
  this.apiClient = null;
3443
4055
  this.config = null;
3444
4056
  this.initPromise = null;
4057
+ this.readyPromise = null;
4058
+ this.resolveReady = null;
3445
4059
  this.pendingConfig = null;
3446
4060
  this.initAttempts = 0;
3447
4061
  this.paywallPresenter = null;
@@ -3450,7 +4064,13 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3450
4064
  this.activeChecker = null;
3451
4065
  this.restoreHandler = null;
3452
4066
  this.lastOnboardingStepTimestamp = null;
3453
- if (wasDebug) console.warn("[Paywallo]", "Full reset complete");
4067
+ this.autoPreloadedPlacement = null;
4068
+ this.autoPreloadPlacementPromise = null;
4069
+ }
4070
+ getSessionFlag(key) {
4071
+ if (!this.config || !this.apiClient) return null;
4072
+ const value = this.sessionFlagsMap.get(key);
4073
+ return value !== void 0 ? value : null;
3454
4074
  }
3455
4075
  getConfig() {
3456
4076
  return this.config;
@@ -3539,6 +4159,15 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3539
4159
  getPreloadedCampaign(placement) {
3540
4160
  return campaignGateService.getPreloadedCampaign(placement);
3541
4161
  }
4162
+ isPreloaded(placement) {
4163
+ return campaignGateService.getPreloadedCampaign(placement) !== null;
4164
+ }
4165
+ getAutoPreloadedPlacement() {
4166
+ return this.autoPreloadedPlacement;
4167
+ }
4168
+ waitForAutoPreloadedPlacement() {
4169
+ return this.autoPreloadPlacementPromise ?? Promise.resolve(null);
4170
+ }
3542
4171
  isOnline() {
3543
4172
  return networkMonitor.isOnline();
3544
4173
  }
@@ -3558,57 +4187,15 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3558
4187
  ensureInitialized() {
3559
4188
  if (!this.config || !this.apiClient) throw new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, "SDK not initialized. Call Paywallo.init() first");
3560
4189
  }
3561
- };
3562
- _PaywalloClientClass.MAX_INIT_RETRIES = 3;
3563
- _PaywalloClientClass.RETRY_DELAYS = [2e3, 5e3, 1e4];
3564
- var PaywalloClientClass = _PaywalloClientClass;
3565
- var PaywalloClient = new PaywalloClientClass();
3566
-
3567
- // src/context/PaywalloContext.ts
3568
- import { createContext as createContext2 } from "react";
3569
- var PaywalloContext = createContext2(null);
3570
-
3571
- // src/utils/localization.ts
3572
- import { NativeModules as NativeModules3, Platform as Platform7 } from "react-native";
3573
- var currentLanguage = "pt-BR";
3574
- var defaultLanguage = "pt-BR";
3575
- function setCurrentLanguage(language) {
3576
- currentLanguage = language;
3577
- }
3578
- function setDefaultLanguage(language) {
3579
- defaultLanguage = language;
3580
- }
3581
- function getCurrentLanguage() {
3582
- return currentLanguage;
3583
- }
3584
- function getDefaultLanguage() {
3585
- return defaultLanguage;
3586
- }
3587
- function getLocalizedString(text) {
3588
- if (!text) return "";
3589
- if (typeof text === "string") return text;
3590
- return text[currentLanguage] ?? text[defaultLanguage] ?? Object.values(text)[0] ?? "";
3591
- }
3592
- function detectDeviceLanguage() {
3593
- try {
3594
- if (Platform7.OS === "ios") {
3595
- const settings = NativeModules3.SettingsManager?.settings;
3596
- return settings?.AppleLocale ?? settings?.AppleLanguages?.[0] ?? "en-US";
3597
- }
3598
- return NativeModules3.I18nManager?.localeIdentifier ?? "en-US";
3599
- } catch {
3600
- return "en-US";
3601
- }
3602
- }
3603
- function initLocalization(options) {
3604
- if (options?.defaultLanguage) {
3605
- defaultLanguage = options.defaultLanguage;
3606
- }
3607
- if (options?.detectDevice !== false) {
3608
- const deviceLanguage = detectDeviceLanguage();
3609
- currentLanguage = deviceLanguage;
3610
- }
3611
- }
4190
+ };
4191
+ _PaywalloClientClass.MAX_INIT_RETRIES = 3;
4192
+ _PaywalloClientClass.RETRY_DELAYS = [2e3, 5e3, 1e4];
4193
+ var PaywalloClientClass = _PaywalloClientClass;
4194
+ var PaywalloClient = new PaywalloClientClass();
4195
+
4196
+ // src/context/PaywalloContext.ts
4197
+ import { createContext as createContext2 } from "react";
4198
+ var PaywalloContext = createContext2(null);
3612
4199
 
3613
4200
  // src/hooks/usePaywallo.ts
3614
4201
  import { useContext as useContext2 } from "react";
@@ -3621,7 +4208,7 @@ function usePaywallo() {
3621
4208
  }
3622
4209
 
3623
4210
  // src/hooks/useProducts.ts
3624
- import { useCallback as useCallback5, useEffect as useEffect5, useState as useState5 } from "react";
4211
+ import { useCallback as useCallback6, useEffect as useEffect5, useState as useState6 } from "react";
3625
4212
 
3626
4213
  // src/utils/productMappers.ts
3627
4214
  function mapToIAPProduct(product) {
@@ -3635,7 +4222,8 @@ function mapToIAPProduct(product) {
3635
4222
  type: product.type === "subscription" ? "subs" : "inapp",
3636
4223
  subscriptionPeriodAndroid: product.subscriptionPeriod,
3637
4224
  introductoryPrice: product.introductoryPrice,
3638
- freeTrialPeriodAndroid: product.freeTrialPeriod
4225
+ freeTrialPeriodAndroid: product.freeTrialPeriod,
4226
+ trialDays: product.trialDays ?? 0
3639
4227
  };
3640
4228
  }
3641
4229
  function mapToFormattedProduct(product) {
@@ -3650,6 +4238,7 @@ function mapToFormattedProduct(product) {
3650
4238
  subscriptionPeriod: product.subscriptionPeriod,
3651
4239
  introductoryPrice: product.introductoryPrice,
3652
4240
  freeTrialPeriod: product.freeTrialPeriod,
4241
+ trialDays: product.trialDays ?? 0,
3653
4242
  pricePerMonth: product.pricePerMonth,
3654
4243
  pricePerWeek: product.pricePerWeek,
3655
4244
  pricePerDay: product.pricePerDay
@@ -3658,11 +4247,11 @@ function mapToFormattedProduct(product) {
3658
4247
 
3659
4248
  // src/hooks/useProducts.ts
3660
4249
  function useProducts(initialProductIds) {
3661
- const [products, setProducts] = useState5([]);
3662
- const [formattedProducts, setFormattedProducts] = useState5([]);
3663
- const [isLoading, setIsLoading] = useState5(false);
3664
- const [error, setError] = useState5(null);
3665
- const loadProducts = useCallback5(async (productIds) => {
4250
+ const [products, setProducts] = useState6([]);
4251
+ const [formattedProducts, setFormattedProducts] = useState6([]);
4252
+ const [isLoading, setIsLoading] = useState6(false);
4253
+ const [error, setError] = useState6(null);
4254
+ const loadProducts = useCallback6(async (productIds) => {
3666
4255
  setIsLoading(true);
3667
4256
  setError(null);
3668
4257
  try {
@@ -3681,19 +4270,19 @@ function useProducts(initialProductIds) {
3681
4270
  void loadProducts(initialProductIds);
3682
4271
  }
3683
4272
  }, []);
3684
- const getProduct = useCallback5(
4273
+ const getProduct = useCallback6(
3685
4274
  (productId) => {
3686
4275
  return products.find((p) => p.productId === productId);
3687
4276
  },
3688
4277
  [products]
3689
4278
  );
3690
- const getFormattedProduct = useCallback5(
4279
+ const getFormattedProduct = useCallback6(
3691
4280
  (productId) => {
3692
4281
  return formattedProducts.find((p) => p.productId === productId);
3693
4282
  },
3694
4283
  [formattedProducts]
3695
4284
  );
3696
- const getPriceInfo = useCallback5(
4285
+ const getPriceInfo = useCallback6(
3697
4286
  (productId) => {
3698
4287
  const p = formattedProducts.find((fp) => fp.productId === productId);
3699
4288
  if (!p) return void 0;
@@ -3722,39 +4311,49 @@ function useProducts(initialProductIds) {
3722
4311
  }
3723
4312
 
3724
4313
  // src/hooks/usePurchase.ts
3725
- import { useCallback as useCallback6, useState as useState6 } from "react";
4314
+ import { useCallback as useCallback7, useState as useState7 } from "react";
3726
4315
  function usePurchase() {
3727
- const [state, setState] = useState6("idle");
3728
- const [error, setError] = useState6(null);
3729
- const purchase = useCallback6(async (productId) => {
4316
+ const [state, setState] = useState7("idle");
4317
+ const [error, setError] = useState7(null);
4318
+ const purchase = useCallback7(async (productId) => {
3730
4319
  setState("purchasing");
3731
4320
  setError(null);
3732
4321
  try {
3733
4322
  const iapService = getIAPService();
3734
- const result = await iapService.purchase(productId);
4323
+ const result = await iapService.purchase(productId, {
4324
+ onValidating: () => setState("validating")
4325
+ });
3735
4326
  if (result.success && result.purchase) {
3736
4327
  setState("idle");
3737
4328
  return {
3738
- productId: result.purchase.productId,
3739
- transactionId: result.purchase.transactionId,
3740
- transactionDate: result.purchase.transactionDate,
3741
- transactionReceipt: result.purchase.receipt
4329
+ status: "success",
4330
+ purchase: {
4331
+ productId: result.purchase.productId,
4332
+ transactionId: result.purchase.transactionId,
4333
+ transactionDate: result.purchase.transactionDate,
4334
+ transactionReceipt: result.purchase.receipt
4335
+ }
3742
4336
  };
3743
4337
  }
3744
4338
  if (result.error) {
3745
- const purchaseError = createPurchaseError("PURCHASE_FAILED", result.error.message);
3746
- setError(purchaseError);
4339
+ setError(result.error);
3747
4340
  setState("error");
3748
- throw result.error;
4341
+ return { status: "failed", error: result.error };
3749
4342
  }
3750
4343
  setState("idle");
3751
- throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, "Purchase cancelled by user", true);
4344
+ return { status: "cancelled" };
3752
4345
  } catch (err) {
4346
+ const purchaseError = err instanceof PurchaseError ? err : createPurchaseError("PURCHASE_FAILED", err instanceof Error ? err.message : String(err));
4347
+ if (purchaseError.userCancelled) {
4348
+ setState("idle");
4349
+ return { status: "cancelled" };
4350
+ }
4351
+ setError(purchaseError);
3753
4352
  setState("error");
3754
- throw err;
4353
+ return { status: "failed", error: purchaseError };
3755
4354
  }
3756
4355
  }, []);
3757
- const restore = useCallback6(async () => {
4356
+ const restore = useCallback7(async () => {
3758
4357
  setState("restoring");
3759
4358
  setError(null);
3760
4359
  try {
@@ -3780,231 +4379,19 @@ function usePurchase() {
3780
4379
  restore,
3781
4380
  error,
3782
4381
  isLoading: state === "loading",
3783
- isPurchasing: state === "purchasing",
4382
+ isPurchasing: state === "purchasing" || state === "validating",
4383
+ isValidating: state === "validating",
3784
4384
  isRestoring: state === "restoring"
3785
4385
  };
3786
4386
  }
3787
4387
 
3788
4388
  // src/hooks/useSubscription.ts
3789
- import { useCallback as useCallback7, useEffect as useEffect6, useState as useState7 } from "react";
3790
-
3791
- // src/domains/subscription/SubscriptionManager.ts
3792
- import { Platform as Platform8 } from "react-native";
3793
-
3794
- // src/domains/subscription/SubscriptionCache.ts
3795
- var CACHE_KEY = "subscription_cache";
3796
- var DEFAULT_TTL = 5 * 60 * 1e3;
3797
- var SubscriptionCache = class {
3798
- constructor(ttl = DEFAULT_TTL) {
3799
- this.memoryCache = null;
3800
- this.ttl = ttl;
3801
- this.storage = new SecureStorage();
3802
- }
3803
- async get() {
3804
- if (this.memoryCache && !this.isExpired(this.memoryCache)) {
3805
- return this.memoryCache;
3806
- }
3807
- try {
3808
- const stored = await this.storage.get(CACHE_KEY);
3809
- if (!stored) {
3810
- return null;
3811
- }
3812
- const cached = JSON.parse(stored);
3813
- if (cached.data.subscription?.expiresAt) {
3814
- cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
3815
- }
3816
- if (cached.data.subscription?.originalPurchaseDate) {
3817
- cached.data.subscription.originalPurchaseDate = new Date(
3818
- cached.data.subscription.originalPurchaseDate
3819
- );
3820
- }
3821
- if (cached.data.subscription?.latestPurchaseDate) {
3822
- cached.data.subscription.latestPurchaseDate = new Date(
3823
- cached.data.subscription.latestPurchaseDate
3824
- );
3825
- }
3826
- if (cached.data.subscription?.cancellationDate) {
3827
- cached.data.subscription.cancellationDate = new Date(
3828
- cached.data.subscription.cancellationDate
3829
- );
3830
- }
3831
- if (cached.data.subscription?.gracePeriodExpiresAt) {
3832
- cached.data.subscription.gracePeriodExpiresAt = new Date(
3833
- cached.data.subscription.gracePeriodExpiresAt
3834
- );
3835
- }
3836
- cached.isStale = this.isExpired(cached);
3837
- if (!cached.isStale) {
3838
- this.memoryCache = cached;
3839
- }
3840
- return cached;
3841
- } catch {
3842
- return null;
3843
- }
3844
- }
3845
- async set(data) {
3846
- const cached = {
3847
- data,
3848
- cachedAt: Date.now(),
3849
- isStale: false
3850
- };
3851
- this.memoryCache = cached;
3852
- try {
3853
- await this.storage.set(CACHE_KEY, JSON.stringify(cached));
3854
- } catch {
3855
- }
3856
- }
3857
- async invalidate() {
3858
- this.memoryCache = null;
3859
- try {
3860
- await this.storage.remove(CACHE_KEY);
3861
- } catch {
3862
- }
3863
- }
3864
- isExpired(cached) {
3865
- return Date.now() - cached.cachedAt > this.ttl;
3866
- }
3867
- setTTL(ttl) {
3868
- this.ttl = ttl;
3869
- }
3870
- };
3871
-
3872
- // src/domains/subscription/SubscriptionManager.ts
3873
- var SubscriptionManagerClass = class {
3874
- constructor() {
3875
- this.config = null;
3876
- this.listeners = /* @__PURE__ */ new Set();
3877
- this.userId = null;
3878
- this.cache = new SubscriptionCache();
3879
- }
3880
- init(config) {
3881
- this.config = config;
3882
- if (config.cacheTTL) {
3883
- this.cache.setTTL(config.cacheTTL);
3884
- }
3885
- }
3886
- setUserId(userId) {
3887
- if (this.userId !== userId) {
3888
- this.userId = userId;
3889
- void this.cache.invalidate();
3890
- }
3891
- }
3892
- async hasActiveSubscription(forceRefresh = false) {
3893
- const status = await this.getSubscriptionStatus(forceRefresh);
3894
- return status.hasActiveSubscription;
3895
- }
3896
- async getSubscription(forceRefresh = false) {
3897
- const status = await this.getSubscriptionStatus(forceRefresh);
3898
- return status.subscription;
3899
- }
3900
- async getEntitlements(forceRefresh = false) {
3901
- const status = await this.getSubscriptionStatus(forceRefresh);
3902
- return status.entitlements;
3903
- }
3904
- async getSubscriptionStatus(forceRefresh = false) {
3905
- if (!this.config) {
3906
- return this.getEmptyStatus();
3907
- }
3908
- if (!forceRefresh) {
3909
- const cached = await this.cache.get();
3910
- if (cached && !cached.isStale) {
3911
- return cached.data;
3912
- }
3913
- }
3914
- try {
3915
- const status = await this.fetchSubscriptionStatus();
3916
- await this.cache.set(status);
3917
- this.notifyListeners(status);
3918
- return status;
3919
- } catch (error) {
3920
- this.log("Failed to fetch subscription status", error);
3921
- const cached = await this.cache.get();
3922
- if (cached) {
3923
- return cached.data;
3924
- }
3925
- return this.getEmptyStatus();
3926
- }
3927
- }
3928
- async restorePurchases() {
3929
- if (!this.config) {
3930
- throw new SessionError(
3931
- SESSION_ERROR_CODES.NOT_INITIALIZED,
3932
- "SubscriptionManager not initialized"
3933
- );
3934
- }
3935
- await this.cache.invalidate();
3936
- return this.getSubscriptionStatus(true);
3937
- }
3938
- async fetchSubscriptionStatus() {
3939
- if (!this.config) {
3940
- return this.getEmptyStatus();
3941
- }
3942
- const url = new URL(`${this.config.serverUrl}/subscriptions/status`);
3943
- if (this.userId) {
3944
- url.searchParams.set("userId", this.userId);
3945
- }
3946
- url.searchParams.set("platform", Platform8.OS);
3947
- const response = await fetch(url.toString(), {
3948
- method: "GET",
3949
- headers: {
3950
- "Content-Type": "application/json",
3951
- "X-App-Key": this.config.appKey
3952
- }
3953
- });
3954
- if (!response.ok) {
3955
- throw new SessionError(
3956
- SESSION_ERROR_CODES.RESTORE_FAILED,
3957
- `Failed to fetch subscription status: ${response.status}`
3958
- );
3959
- }
3960
- const data = await response.json();
3961
- if (data.subscription) {
3962
- data.subscription.expiresAt = new Date(data.subscription.expiresAt);
3963
- data.subscription.originalPurchaseDate = new Date(data.subscription.originalPurchaseDate);
3964
- data.subscription.latestPurchaseDate = new Date(data.subscription.latestPurchaseDate);
3965
- if (data.subscription.cancellationDate) {
3966
- data.subscription.cancellationDate = new Date(data.subscription.cancellationDate);
3967
- }
3968
- if (data.subscription.gracePeriodExpiresAt) {
3969
- data.subscription.gracePeriodExpiresAt = new Date(data.subscription.gracePeriodExpiresAt);
3970
- }
3971
- }
3972
- return data;
3973
- }
3974
- getEmptyStatus() {
3975
- return {
3976
- hasActiveSubscription: false,
3977
- subscription: null,
3978
- entitlements: []
3979
- };
3980
- }
3981
- addListener(listener) {
3982
- this.listeners.add(listener);
3983
- return () => this.listeners.delete(listener);
3984
- }
3985
- notifyListeners(status) {
3986
- for (const listener of this.listeners) {
3987
- listener(status);
3988
- }
3989
- }
3990
- async onPurchaseComplete() {
3991
- await this.cache.invalidate();
3992
- await this.getSubscriptionStatus(true);
3993
- }
3994
- log(message, data) {
3995
- if (this.config?.debug) {
3996
- console.warn(`[SubscriptionManager] ${message}`, data ?? "");
3997
- }
3998
- }
3999
- };
4000
- var subscriptionManager = new SubscriptionManagerClass();
4001
-
4002
- // src/hooks/useSubscription.ts
4389
+ import { useCallback as useCallback8, useEffect as useEffect6, useState as useState8 } from "react";
4003
4390
  function useSubscription() {
4004
- const [status, setStatus] = useState7(null);
4005
- const [isLoading, setIsLoading] = useState7(true);
4006
- const [error, setError] = useState7(null);
4007
- const refresh = useCallback7(async () => {
4391
+ const [status, setStatus] = useState8(null);
4392
+ const [isLoading, setIsLoading] = useState8(true);
4393
+ const [error, setError] = useState8(null);
4394
+ const refresh = useCallback8(async () => {
4008
4395
  setIsLoading(true);
4009
4396
  setError(null);
4010
4397
  try {
@@ -4017,7 +4404,7 @@ function useSubscription() {
4017
4404
  setIsLoading(false);
4018
4405
  }
4019
4406
  }, []);
4020
- const restore = useCallback7(async () => {
4407
+ const restore = useCallback8(async () => {
4021
4408
  setIsLoading(true);
4022
4409
  setError(null);
4023
4410
  try {
@@ -4054,11 +4441,11 @@ function useSubscription() {
4054
4441
  }
4055
4442
 
4056
4443
  // src/PaywalloProvider.tsx
4057
- import { useCallback as useCallback12, useEffect as useEffect7, useMemo as useMemo2, useRef as useRef8, useState as useState11 } from "react";
4444
+ import { useCallback as useCallback13, useEffect as useEffect7, useLayoutEffect, useMemo as useMemo2, useRef as useRef8, useState as useState12 } from "react";
4058
4445
  import { Animated as Animated3, Dimensions as Dimensions4, Easing as Easing2, StyleSheet as StyleSheet3 } from "react-native";
4059
4446
 
4060
4447
  // src/hooks/useCampaignPreload.ts
4061
- import { useCallback as useCallback8, useRef as useRef6, useState as useState8 } from "react";
4448
+ import { useCallback as useCallback9, useRef as useRef6, useState as useState9 } from "react";
4062
4449
 
4063
4450
  // src/utils/serverProduct.ts
4064
4451
  var PERIOD_MAP = {
@@ -4116,26 +4503,25 @@ var CACHE_TTL_MS = 5 * 60 * 1e3;
4116
4503
  function isCacheEntryValid(entry) {
4117
4504
  return !!entry && Date.now() - entry.preloadedAt < CACHE_TTL_MS;
4118
4505
  }
4119
- function consumeCacheEntry(cache, placement) {
4506
+ function getCacheEntry(cache, placement) {
4120
4507
  const entry = cache.get(placement);
4121
4508
  if (!isCacheEntryValid(entry)) return null;
4122
- cache.delete(placement);
4123
4509
  return entry ?? null;
4124
4510
  }
4125
4511
 
4126
4512
  // src/hooks/useCampaignPreload.ts
4127
4513
  function useCampaignPreload() {
4128
- const [preloadedWebView, setPreloadedWebView] = useState8(null);
4514
+ const [preloadedWebView, setPreloadedWebView] = useState9(null);
4129
4515
  const preloadedCampaignsRef = useRef6(/* @__PURE__ */ new Map());
4130
- const isPreloadValid = useCallback8(
4516
+ const isPreloadValid = useCallback9(
4131
4517
  (placement) => isCacheEntryValid(preloadedCampaignsRef.current.get(placement)),
4132
4518
  []
4133
4519
  );
4134
- const consumePreloadedCampaign = useCallback8(
4135
- (placement) => consumeCacheEntry(preloadedCampaignsRef.current, placement),
4520
+ const consumePreloadedCampaign = useCallback9(
4521
+ (placement) => getCacheEntry(preloadedCampaignsRef.current, placement),
4136
4522
  []
4137
4523
  );
4138
- const preloadCampaign = useCallback8(
4524
+ const preloadCampaign = useCallback9(
4139
4525
  async (placement, context) => {
4140
4526
  try {
4141
4527
  const campaign = await PaywalloClient.getCampaign(placement, context);
@@ -4180,6 +4566,7 @@ function useCampaignPreload() {
4180
4566
  campaignId: campaign.campaignId,
4181
4567
  variantKey: campaign.variantKey
4182
4568
  });
4569
+ if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo WEBVIEW] mounting hidden \u2014 placement: ${placement}`);
4183
4570
  return { success: true };
4184
4571
  } catch (error) {
4185
4572
  return {
@@ -4201,11 +4588,11 @@ function useCampaignPreload() {
4201
4588
  }
4202
4589
 
4203
4590
  // src/hooks/useProductLoader.ts
4204
- import { useCallback as useCallback9, useState as useState9 } from "react";
4591
+ import { useCallback as useCallback10, useState as useState10 } from "react";
4205
4592
  function useProductLoader() {
4206
- const [products, setProducts] = useState9(/* @__PURE__ */ new Map());
4207
- const [isLoadingProducts, setIsLoadingProducts] = useState9(false);
4208
- const refreshProducts = useCallback9(async (productIds) => {
4593
+ const [products, setProducts] = useState10(/* @__PURE__ */ new Map());
4594
+ const [isLoadingProducts, setIsLoadingProducts] = useState10(false);
4595
+ const refreshProducts = useCallback10(async (productIds) => {
4209
4596
  setIsLoadingProducts(true);
4210
4597
  try {
4211
4598
  const iapService = getIAPService();
@@ -4214,7 +4601,6 @@ function useProductLoader() {
4214
4601
  for (const p of loaded) map.set(p.productId, p);
4215
4602
  setProducts(map);
4216
4603
  } catch (error) {
4217
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Product load failed:", error);
4218
4604
  } finally {
4219
4605
  setIsLoadingProducts(false);
4220
4606
  }
@@ -4223,10 +4609,10 @@ function useProductLoader() {
4223
4609
  }
4224
4610
 
4225
4611
  // src/hooks/usePurchaseOrchestrator.ts
4226
- import { useCallback as useCallback10 } from "react";
4227
- function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef) {
4612
+ import { useCallback as useCallback11 } from "react";
4613
+ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, invalidateSubscriptionCache) {
4228
4614
  const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
4229
- const handlePreloadedPurchase = useCallback10(
4615
+ const handlePreloadedPurchase = useCallback11(
4230
4616
  async (productId) => {
4231
4617
  try {
4232
4618
  if (preloadedWebView) {
@@ -4249,19 +4635,19 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4249
4635
  await trackDismissed({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, durationSeconds, action: "purchase", variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4250
4636
  await trackPurchased({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, productId, variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4251
4637
  }
4638
+ invalidateSubscriptionCache();
4252
4639
  setShowingPreloadedWebView(false);
4253
4640
  clearPreloadedWebView();
4254
4641
  resolvePreloadedPaywall({ presented: true, purchased: true, cancelled: false, restored: false });
4255
4642
  } catch (error) {
4256
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Preloaded purchase error:", error);
4257
4643
  setShowingPreloadedWebView(false);
4258
4644
  clearPreloadedWebView();
4259
4645
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
4260
4646
  }
4261
4647
  },
4262
- [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased]
4648
+ [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased, invalidateSubscriptionCache]
4263
4649
  );
4264
- const handlePreloadedRestore = useCallback10(async () => {
4650
+ const handlePreloadedRestore = useCallback11(async () => {
4265
4651
  try {
4266
4652
  const transactions = await getIAPService().restore();
4267
4653
  if (transactions.length === 0) return;
@@ -4269,30 +4655,72 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4269
4655
  const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
4270
4656
  await trackDismissed({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, durationSeconds, action: "restore", variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4271
4657
  }
4658
+ invalidateSubscriptionCache();
4272
4659
  setShowingPreloadedWebView(false);
4273
4660
  clearPreloadedWebView();
4274
4661
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: true });
4275
4662
  } catch (error) {
4276
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Preloaded restore error:", error);
4277
4663
  setShowingPreloadedWebView(false);
4278
4664
  clearPreloadedWebView();
4279
4665
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
4280
4666
  }
4281
- }, [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed]);
4667
+ }, [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, invalidateSubscriptionCache]);
4282
4668
  return { handlePreloadedPurchase, handlePreloadedRestore };
4283
4669
  }
4284
4670
 
4285
4671
  // src/hooks/useSubscriptionSync.ts
4286
- import { useCallback as useCallback11, useRef as useRef7, useState as useState10 } from "react";
4672
+ import { useCallback as useCallback12, useRef as useRef7, useState as useState11 } from "react";
4287
4673
  var CACHE_TTL_MS2 = 5 * 60 * 1e3;
4288
- var ACTIVE_STATUSES = ["active", "in_grace_period"];
4674
+ var ACTIVE_STATUSES = ["active", "grace_period"];
4675
+ function toDomainSubscriptionInfo(sub) {
4676
+ return {
4677
+ id: "",
4678
+ productId: sub.productId,
4679
+ status: sub.status,
4680
+ expiresAt: sub.expiresAt ?? /* @__PURE__ */ new Date(0),
4681
+ originalPurchaseDate: /* @__PURE__ */ new Date(0),
4682
+ latestPurchaseDate: /* @__PURE__ */ new Date(0),
4683
+ willRenew: sub.autoRenewEnabled,
4684
+ isTrial: false,
4685
+ isIntroductoryPeriod: false,
4686
+ platform: sub.platform
4687
+ };
4688
+ }
4689
+ function toDomainResponse(status) {
4690
+ return {
4691
+ hasActiveSubscription: status.hasActiveSubscription,
4692
+ subscription: status.subscription ? toDomainSubscriptionInfo(status.subscription) : null,
4693
+ entitlements: []
4694
+ };
4695
+ }
4696
+ function isSubscriptionStillActive(sub) {
4697
+ if (!sub) return false;
4698
+ if (!ACTIVE_STATUSES.includes(sub.status)) return false;
4699
+ if (sub.expiresAt && sub.expiresAt.getTime() !== 0 && sub.expiresAt < /* @__PURE__ */ new Date()) return false;
4700
+ return true;
4701
+ }
4702
+ async function resolveOfflineFromCache(distinctId) {
4703
+ try {
4704
+ const cached = await subscriptionCache.get(distinctId);
4705
+ if (!cached) return false;
4706
+ const sub = cached.data.subscription;
4707
+ return isSubscriptionStillActive(sub ? { status: sub.status, expiresAt: sub.expiresAt ?? null } : null);
4708
+ } catch {
4709
+ return false;
4710
+ }
4711
+ }
4712
+ function isActiveCacheEntry(entry) {
4713
+ return isSubscriptionStillActive(entry.data);
4714
+ }
4289
4715
  function useSubscriptionSync() {
4290
- const [subscription, setSubscription] = useState10(null);
4716
+ const [subscription, setSubscription] = useState11(null);
4291
4717
  const cacheRef = useRef7(null);
4292
- const invalidateCache = useCallback11(() => {
4718
+ const invalidateCache = useCallback12(() => {
4293
4719
  cacheRef.current = null;
4720
+ const distinctId = PaywalloClient.getDistinctId();
4721
+ if (distinctId) void subscriptionCache.invalidate(distinctId);
4294
4722
  }, []);
4295
- const fetchAndCache = useCallback11(async () => {
4723
+ const fetchAndCache = useCallback12(async () => {
4296
4724
  const apiClient = PaywalloClient.getApiClient();
4297
4725
  const distinctId = PaywalloClient.getDistinctId();
4298
4726
  if (!apiClient || !distinctId) return null;
@@ -4303,18 +4731,37 @@ function useSubscriptionSync() {
4303
4731
  } : null;
4304
4732
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4305
4733
  setSubscription(sub);
4734
+ void subscriptionCache.set(distinctId, toDomainResponse(status));
4306
4735
  return sub;
4307
4736
  }, []);
4308
- const hasActiveSubscription = useCallback11(async () => {
4737
+ const hasActiveSubscription = useCallback12(async () => {
4309
4738
  const apiClient = PaywalloClient.getApiClient();
4310
4739
  const distinctId = PaywalloClient.getDistinctId();
4311
4740
  if (!apiClient || !distinctId) return false;
4312
4741
  const cached = cacheRef.current;
4313
4742
  if (cached && cached.expiresAt > Date.now()) {
4314
- if (!cached.data) return false;
4315
- if (!ACTIVE_STATUSES.includes(cached.data.status)) return false;
4316
- if (cached.data.expiresAt && cached.data.expiresAt < /* @__PURE__ */ new Date()) return false;
4317
- return true;
4743
+ return isActiveCacheEntry(cached);
4744
+ }
4745
+ try {
4746
+ const persisted = await subscriptionCache.get(distinctId);
4747
+ if (persisted && !persisted.isStale) {
4748
+ const persistedSub = persisted.data.subscription;
4749
+ const sub = persistedSub ? {
4750
+ productId: persistedSub.productId,
4751
+ status: persistedSub.status,
4752
+ expiresAt: persistedSub.expiresAt ?? null,
4753
+ platform: persistedSub.platform,
4754
+ autoRenewEnabled: persistedSub.willRenew,
4755
+ inGracePeriod: persistedSub.status === "grace_period"
4756
+ } : null;
4757
+ const stillActive = isSubscriptionStillActive(sub);
4758
+ if (stillActive) {
4759
+ cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4760
+ setSubscription(sub);
4761
+ return true;
4762
+ }
4763
+ }
4764
+ } catch {
4318
4765
  }
4319
4766
  try {
4320
4767
  const status = await apiClient.getSubscriptionStatus(distinctId);
@@ -4324,12 +4771,13 @@ function useSubscriptionSync() {
4324
4771
  } : null;
4325
4772
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4326
4773
  setSubscription(sub);
4774
+ void subscriptionCache.set(distinctId, toDomainResponse(status));
4327
4775
  return status.hasActiveSubscription;
4328
4776
  } catch {
4329
- return false;
4777
+ return await resolveOfflineFromCache(distinctId);
4330
4778
  }
4331
4779
  }, []);
4332
- const getSubscription = useCallback11(async () => {
4780
+ const getSubscription = useCallback12(async () => {
4333
4781
  const apiClient = PaywalloClient.getApiClient();
4334
4782
  const distinctId = PaywalloClient.getDistinctId();
4335
4783
  if (!apiClient || !distinctId) return null;
@@ -4338,7 +4786,22 @@ function useSubscriptionSync() {
4338
4786
  try {
4339
4787
  return await fetchAndCache();
4340
4788
  } catch {
4341
- return null;
4789
+ try {
4790
+ const persisted = await subscriptionCache.get(distinctId);
4791
+ if (!persisted?.data.subscription) return null;
4792
+ const s = persisted.data.subscription;
4793
+ const normalisedStatus = s.status === "grace_period" ? "in_grace_period" : s.status;
4794
+ return {
4795
+ productId: s.productId,
4796
+ status: normalisedStatus,
4797
+ expiresAt: s.expiresAt ?? null,
4798
+ platform: s.platform,
4799
+ autoRenewEnabled: s.willRenew,
4800
+ inGracePeriod: normalisedStatus === "in_grace_period"
4801
+ };
4802
+ } catch {
4803
+ return null;
4804
+ }
4342
4805
  }
4343
4806
  }, [fetchAndCache]);
4344
4807
  return { subscription, hasActiveSubscription, getSubscription, invalidateCache };
@@ -4347,24 +4810,49 @@ function useSubscriptionSync() {
4347
4810
  // src/PaywalloProvider.tsx
4348
4811
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
4349
4812
  function PaywalloProvider({ children, config }) {
4350
- const [isInitialized, setIsInitialized] = useState11(false);
4351
- const [initError, setInitError] = useState11(null);
4352
- const [distinctId, setDistinctId] = useState11(null);
4353
- const [activePaywall, setActivePaywall] = useState11(null);
4813
+ const [isInitialized, setIsInitialized] = useState12(false);
4814
+ const [initError, setInitError] = useState12(null);
4815
+ const [distinctId, setDistinctId] = useState12(null);
4816
+ const [activePaywall, setActivePaywall] = useState12(null);
4354
4817
  const { products, isLoadingProducts, refreshProducts } = useProductLoader();
4355
4818
  const { subscription, hasActiveSubscription, getSubscription, invalidateCache } = useSubscriptionSync();
4356
4819
  const { preloadedWebView, setPreloadedWebView, preloadCampaign, consumePreloadedCampaign, isPreloadValid } = useCampaignPreload();
4357
- const clearPreloadedWebView = useCallback12(() => setPreloadedWebView(null), [setPreloadedWebView]);
4358
- const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView);
4820
+ const clearPreloadedWebView = useCallback13(() => setPreloadedWebView(null), [setPreloadedWebView]);
4821
+ const autoPreloadTriggeredRef = useRef8(false);
4822
+ const preloadedWebViewRef = useRef8(preloadedWebView);
4823
+ useEffect7(() => {
4824
+ preloadedWebViewRef.current = preloadedWebView;
4825
+ }, [preloadedWebView]);
4826
+ const triggerRepreload = useCallback13((placement) => {
4827
+ void PaywalloClient.preloadCampaign(placement).catch(() => {
4828
+ });
4829
+ }, []);
4830
+ const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady: baseHandlePreloadedWebViewReady, handlePreloadedClose: baseHandlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, setPreloadedWebView);
4831
+ const SCREEN_HEIGHT3 = useMemo2(() => Dimensions4.get("window").height, []);
4832
+ const slideAnim = useRef8(new Animated3.Value(SCREEN_HEIGHT3)).current;
4833
+ const handlePreloadedWebViewReady = useCallback13(() => {
4834
+ if (PaywalloClient.getConfig()?.debug) {
4835
+ console.info(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
4836
+ }
4837
+ baseHandlePreloadedWebViewReady();
4838
+ }, [config?.debug, preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
4839
+ const handlePreloadedClose = useCallback13(() => {
4840
+ const placement = preloadedWebView?.placement;
4841
+ Animated3.timing(slideAnim, { toValue: SCREEN_HEIGHT3, duration: 200, easing: Easing2.in(Easing2.cubic), useNativeDriver: true }).start(() => {
4842
+ baseHandlePreloadedClose();
4843
+ if (placement) triggerRepreload(placement);
4844
+ });
4845
+ }, [baseHandlePreloadedClose, preloadedWebView, triggerRepreload, slideAnim, SCREEN_HEIGHT3]);
4359
4846
  const { handlePreloadedPurchase: rawPreloadedPurchase, handlePreloadedRestore } = usePurchaseOrchestrator(
4360
4847
  preloadedWebView,
4361
4848
  resolvePreloadedPaywall,
4362
4849
  clearPreloadedWebView,
4363
4850
  setShowingPreloadedWebView,
4364
- presentedAtRef
4851
+ presentedAtRef,
4852
+ invalidateCache
4365
4853
  );
4366
- const [isPreloadPurchasing, setIsPreloadPurchasing] = useState11(false);
4367
- const handlePreloadedPurchase = useCallback12(async (productId) => {
4854
+ const [isPreloadPurchasing, setIsPreloadPurchasing] = useState12(false);
4855
+ const handlePreloadedPurchase = useCallback13(async (productId) => {
4368
4856
  setIsPreloadPurchasing(true);
4369
4857
  try {
4370
4858
  await rawPreloadedPurchase(productId);
@@ -4374,28 +4862,34 @@ function PaywalloProvider({ children, config }) {
4374
4862
  }, [rawPreloadedPurchase]);
4375
4863
  useEffect7(() => {
4376
4864
  initLocalization({ detectDevice: true });
4377
- PaywalloClient.init(config).then(() => {
4865
+ PaywalloClient.init(config).then(async () => {
4378
4866
  setDistinctId(PaywalloClient.getDistinctId());
4379
4867
  setIsInitialized(true);
4868
+ if (!autoPreloadTriggeredRef.current) {
4869
+ autoPreloadTriggeredRef.current = true;
4870
+ const placement = await PaywalloClient.waitForAutoPreloadedPlacement();
4871
+ if (placement) void preloadCampaign(placement).catch(() => {
4872
+ });
4873
+ }
4380
4874
  }).catch((err) => {
4381
4875
  setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
4382
4876
  });
4383
- }, [config]);
4384
- const getPaywallConfig = useCallback12(async (p) => {
4877
+ }, [config, preloadCampaign]);
4878
+ const getPaywallConfig = useCallback13(async (p) => {
4385
4879
  const api = PaywalloClient.getApiClient();
4386
4880
  return api ? api.getPaywall(p) : null;
4387
4881
  }, []);
4388
- const presentPaywall = useCallback12((placement) => {
4882
+ const presentPaywall = useCallback13((placement) => {
4389
4883
  return new Promise((resolve) => setActivePaywall({ placement, resolver: resolve }));
4390
4884
  }, []);
4391
- const trackCampaignImpression = useCallback12((placement, variantKey, campaignId) => {
4885
+ const trackCampaignImpression = useCallback13((placement, variantKey, campaignId) => {
4392
4886
  const api = PaywalloClient.getApiClient();
4393
4887
  if (!api) return;
4394
4888
  const sessionId = PaywalloClient.getSessionId();
4395
4889
  void api.trackEvent("$campaign_impression", PaywalloClient.getDistinctId(), { placement, variantKey, campaignId, ...sessionId && { sessionId } }).catch(() => {
4396
4890
  });
4397
4891
  }, []);
4398
- const presentCampaign = useCallback12(
4892
+ const presentCampaign = useCallback13(
4399
4893
  async (placement, context) => {
4400
4894
  try {
4401
4895
  if (preloadedWebView?.placement === placement && preloadedWebView.loaded && isPreloadValid(placement)) {
@@ -4464,7 +4958,7 @@ function PaywalloProvider({ children, config }) {
4464
4958
  },
4465
4959
  [preloadedWebView, isPreloadValid, consumePreloadedCampaign, presentPreloadedWebView, trackCampaignImpression]
4466
4960
  );
4467
- const restorePurchases = useCallback12(async () => {
4961
+ const restorePurchases = useCallback13(async () => {
4468
4962
  invalidateCache();
4469
4963
  try {
4470
4964
  const txs = await getIAPService().restore();
@@ -4473,34 +4967,60 @@ function PaywalloProvider({ children, config }) {
4473
4967
  return { success: false, restoredProducts: [], error: error instanceof Error ? error : new PurchaseError(PURCHASE_ERROR_CODES.RESTORE_FAILED, String(error)) };
4474
4968
  }
4475
4969
  }, [invalidateCache]);
4476
- const handlePaywallResult = useCallback12((result) => {
4970
+ const handlePaywallResult = useCallback13((result) => {
4477
4971
  if (activePaywall) {
4478
4972
  activePaywall.resolver(result);
4479
4973
  setActivePaywall(null);
4480
4974
  }
4481
4975
  }, [activePaywall]);
4976
+ const bridgePaywallPresenter = useCallback13((placement) => {
4977
+ if (PaywalloClient.getConfig()?.debug) {
4978
+ console.info(`[Paywallo PRESENT] called \u2014 preloaded: ${preloadedWebViewRef.current?.loaded === true} placement: ${placement}`);
4979
+ }
4980
+ if (preloadedWebViewRef.current?.placement === placement) {
4981
+ if (preloadedWebViewRef.current.loaded) {
4982
+ return presentPreloadedWebView();
4983
+ }
4984
+ return new Promise((resolve) => {
4985
+ let elapsed = 0;
4986
+ const poll = () => {
4987
+ if (preloadedWebViewRef.current?.loaded) {
4988
+ resolve(presentPreloadedWebView());
4989
+ return;
4990
+ }
4991
+ elapsed += 100;
4992
+ if (elapsed >= 500) {
4993
+ resolve(presentCampaign(placement));
4994
+ return;
4995
+ }
4996
+ setTimeout(poll, 100);
4997
+ };
4998
+ setTimeout(poll, 100);
4999
+ });
5000
+ }
5001
+ return presentCampaign(placement);
5002
+ }, [presentPreloadedWebView, presentCampaign]);
4482
5003
  useEffect7(() => {
4483
5004
  if (!isInitialized) return;
4484
5005
  const onEmergency = async (id) => {
4485
5006
  try {
4486
- await presentPaywall(id);
5007
+ await bridgePaywallPresenter(id);
4487
5008
  } catch {
4488
5009
  }
4489
5010
  };
4490
- PaywalloClient.registerPaywallPresenter(presentPaywall);
5011
+ PaywalloClient.registerPaywallPresenter(bridgePaywallPresenter);
4491
5012
  PaywalloClient.registerSubscriptionGetter(getSubscription);
4492
5013
  PaywalloClient.registerActiveChecker(hasActiveSubscription);
4493
5014
  PaywalloClient.registerRestoreHandler(restorePurchases);
4494
5015
  PaywalloClient.registerEmergencyPaywallHandler(onEmergency);
4495
- }, [isInitialized, presentPaywall, getSubscription, hasActiveSubscription, restorePurchases]);
4496
- useEffect7(() => {
4497
- if (activePaywall) setPreloadedWebView(null);
4498
- }, [activePaywall, setPreloadedWebView]);
4499
- const noOp = useCallback12(() => {
5016
+ }, [isInitialized, bridgePaywallPresenter, getSubscription, hasActiveSubscription, restorePurchases]);
5017
+ const isReady = isInitialized && !isLoadingProducts;
5018
+ const noOp = useCallback13(() => {
4500
5019
  }, []);
4501
5020
  const contextValue = useMemo2(() => ({
4502
5021
  config: PaywalloClient.getConfig(),
4503
5022
  isInitialized,
5023
+ isReady,
4504
5024
  initError,
4505
5025
  distinctId,
4506
5026
  subscription,
@@ -4516,6 +5036,7 @@ function PaywalloProvider({ children, config }) {
4516
5036
  refreshProducts
4517
5037
  }), [
4518
5038
  isInitialized,
5039
+ isReady,
4519
5040
  initError,
4520
5041
  distinctId,
4521
5042
  subscription,
@@ -4530,20 +5051,16 @@ function PaywalloProvider({ children, config }) {
4530
5051
  getPaywallConfig,
4531
5052
  refreshProducts
4532
5053
  ]);
4533
- const SCREEN_HEIGHT3 = useMemo2(() => Dimensions4.get("window").height, []);
4534
- const slideAnim = useRef8(new Animated3.Value(SCREEN_HEIGHT3)).current;
4535
- useEffect7(() => {
5054
+ useLayoutEffect(() => {
4536
5055
  if (showingPreloadedWebView) {
4537
5056
  slideAnim.setValue(SCREEN_HEIGHT3);
4538
- Animated3.timing(slideAnim, { toValue: 0, duration: 320, easing: Easing2.out(Easing2.cubic), useNativeDriver: true }).start();
4539
- } else {
4540
- slideAnim.setValue(SCREEN_HEIGHT3);
5057
+ Animated3.timing(slideAnim, { toValue: 0, duration: 200, easing: Easing2.out(Easing2.cubic), useNativeDriver: true }).start();
4541
5058
  }
4542
5059
  }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT3]);
4543
5060
  const preloadStyle = [
4544
5061
  styles3.preloadOverlay,
4545
5062
  showingPreloadedWebView ? styles3.visible : styles3.hidden,
4546
- { transform: [{ translateY: showingPreloadedWebView ? slideAnim : SCREEN_HEIGHT3 }] }
5063
+ showingPreloadedWebView ? { transform: [{ translateY: slideAnim }] } : void 0
4547
5064
  ];
4548
5065
  return /* @__PURE__ */ jsxs2(PaywalloContext.Provider, { value: contextValue, children: [
4549
5066
  children,
@@ -4556,6 +5073,7 @@ function PaywalloProvider({ children, config }) {
4556
5073
  primaryProductId: preloadedWebView.primaryProductId,
4557
5074
  secondaryProductId: preloadedWebView.secondaryProductId,
4558
5075
  isPurchasing: isPreloadPurchasing,
5076
+ webUrl: isInitialized ? PaywalloClient.getWebUrl() : null,
4559
5077
  onReady: handlePreloadedWebViewReady,
4560
5078
  onClose: showingPreloadedWebView ? handlePreloadedClose : noOp,
4561
5079
  onPurchase: showingPreloadedWebView ? handlePreloadedPurchase : noOp,
@@ -4578,8 +5096,8 @@ function PaywalloProvider({ children, config }) {
4578
5096
  }
4579
5097
  var styles3 = StyleSheet3.create({
4580
5098
  preloadOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#ffffff" },
4581
- hidden: { zIndex: -1 },
4582
- visible: { zIndex: 9999 }
5099
+ hidden: { zIndex: -1, opacity: 0, width: 1, height: 1, overflow: "hidden" },
5100
+ visible: { zIndex: 9999, opacity: 1 }
4583
5101
  });
4584
5102
 
4585
5103
  // src/utils/productVariableResolver.ts