@virex-tech/paywallo-sdk 1.5.0 → 1.5.2

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
@@ -63,6 +63,7 @@ var CLIENT_ERROR_CODES = {
63
63
  MISSING_APP_KEY: "CLIENT_MISSING_APP_KEY",
64
64
  INVALID_EVENT_NAME: "CLIENT_INVALID_EVENT_NAME",
65
65
  PROVIDER_MISSING: "CLIENT_PROVIDER_MISSING",
66
+ INSECURE_REQUEST: "CLIENT_INSECURE_REQUEST",
66
67
  UNKNOWN: "CLIENT_UNKNOWN"
67
68
  };
68
69
 
@@ -129,10 +130,7 @@ function createPurchaseError(code, message) {
129
130
  }
130
131
 
131
132
  // src/domains/iap/NativeStoreKit.ts
132
- import { NativeModules, Platform } from "react-native";
133
-
134
- // src/domains/identity/IdentityManager.ts
135
- import DeviceInfo from "react-native-device-info";
133
+ import { NativeModules as NativeModules3, Platform } from "react-native";
136
134
 
137
135
  // src/domains/identity/IdentityError.ts
138
136
  var IdentityError = class extends PaywalloError {
@@ -152,13 +150,141 @@ var IDENTITY_ERROR_CODES = {
152
150
  // src/domains/identity/IdentityManager.ts
153
151
  init_uuid();
154
152
 
153
+ // src/utils/NativeDeviceInfo.ts
154
+ import { NativeModules } from "react-native";
155
+ var FALLBACK = {
156
+ deviceId: "unknown",
157
+ model: "unknown",
158
+ modelId: "unknown",
159
+ systemVersion: "0.0.0",
160
+ appVersion: "0.0.0",
161
+ buildNumber: "0",
162
+ bundleId: "unknown",
163
+ brand: "unknown",
164
+ systemName: "unknown",
165
+ totalDisk: 0,
166
+ freeDisk: 0,
167
+ totalRam: 0,
168
+ carrier: "unknown",
169
+ installerPackage: null,
170
+ idfv: null
171
+ };
172
+ var _debug = false;
173
+ var warnedOnce = false;
174
+ var cachedDeviceInfo = null;
175
+ function setNativeDeviceInfoDebug(debug) {
176
+ _debug = debug;
177
+ }
178
+ function isDeviceInfoResult(value) {
179
+ if (typeof value !== "object" || value === null) return false;
180
+ const v = value;
181
+ return typeof v["deviceId"] === "string" && typeof v["model"] === "string" && typeof v["systemVersion"] === "string" && typeof v["appVersion"] === "string" && typeof v["bundleId"] === "string";
182
+ }
183
+ function getNativeModule() {
184
+ const mod = NativeModules.PaywalloDevice;
185
+ if (!mod) {
186
+ if (!warnedOnce) {
187
+ warnedOnce = true;
188
+ console.warn("[Paywallo] NativeModules.PaywalloDevice not found \u2014 falling back to defaults");
189
+ }
190
+ return null;
191
+ }
192
+ return mod;
193
+ }
194
+ async function getDeviceInfo() {
195
+ if (cachedDeviceInfo !== null) return cachedDeviceInfo;
196
+ const mod = getNativeModule();
197
+ if (!mod) return FALLBACK;
198
+ const raw = await mod.getDeviceInfo();
199
+ if (!isDeviceInfoResult(raw)) return FALLBACK;
200
+ cachedDeviceInfo = raw;
201
+ return raw;
202
+ }
203
+ async function getInstallReferrer() {
204
+ const mod = getNativeModule();
205
+ if (!mod) return null;
206
+ try {
207
+ return await mod.getInstallReferrer();
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+ function isAvailable() {
213
+ return NativeModules.PaywalloDevice != null;
214
+ }
215
+ function clearCache() {
216
+ cachedDeviceInfo = null;
217
+ }
218
+ var nativeDeviceInfo = {
219
+ getDeviceInfo,
220
+ getInstallReferrer,
221
+ isAvailable,
222
+ clearCache
223
+ };
224
+
225
+ // src/core/storage/NativeStorage.ts
226
+ import { NativeModules as NativeModules2 } from "react-native";
227
+ var NativeModule = NativeModules2.PaywalloStorage;
228
+ var _debug2 = false;
229
+ var _warnedUnavailable = false;
230
+ function setNativeStorageDebug(debug) {
231
+ _debug2 = debug;
232
+ }
233
+ function isAvailable2() {
234
+ return NativeModule != null;
235
+ }
236
+ function warnIfUnavailable() {
237
+ if (NativeModule != null) return true;
238
+ if (!_warnedUnavailable) {
239
+ _warnedUnavailable = true;
240
+ if (_debug2) console.warn("[Paywallo] PaywalloStorage native module not available \u2014 storage will not persist");
241
+ }
242
+ return false;
243
+ }
244
+ function getModule() {
245
+ if (!warnIfUnavailable()) return null;
246
+ return NativeModule;
247
+ }
248
+ async function get(key) {
249
+ return getModule()?.get(key) ?? null;
250
+ }
251
+ async function set(key, value) {
252
+ return getModule()?.set(key, value) ?? false;
253
+ }
254
+ async function remove(key) {
255
+ return getModule()?.remove(key) ?? false;
256
+ }
257
+ async function secureGet(key) {
258
+ return getModule()?.secureGet(key) ?? null;
259
+ }
260
+ async function secureSet(key, value) {
261
+ return getModule()?.secureSet(key, value) ?? false;
262
+ }
263
+ async function secureRemove(key) {
264
+ return getModule()?.secureRemove(key) ?? false;
265
+ }
266
+ async function legacySecureGet(key) {
267
+ const mod = getModule();
268
+ if (!mod?.legacySecureGet) return null;
269
+ return mod.legacySecureGet(key);
270
+ }
271
+ var nativeStorage = {
272
+ isAvailable: isAvailable2,
273
+ get,
274
+ set,
275
+ remove,
276
+ secureGet,
277
+ secureSet,
278
+ secureRemove,
279
+ legacySecureGet
280
+ };
281
+
155
282
  // src/core/storage/SecureStorage.ts
156
- import AsyncStorage from "@react-native-async-storage/async-storage";
157
- import Keychain from "react-native-keychain";
158
- var KEYCHAIN_SERVICE_PREFIX = "com.paywallo.sdk.";
283
+ var KEYCHAIN_KEY_PREFIX = "com.paywallo.sdk.";
284
+ var REGULAR_KEY_PREFIX = "@paywallo:";
159
285
  var SecureStorage = class {
160
286
  // eslint-disable-next-line @typescript-eslint/no-useless-constructor
161
- constructor(_debug = false) {
287
+ constructor(_debug3 = false) {
162
288
  }
163
289
  async get(key) {
164
290
  const keychainValue = await this.getFromKeychain(key);
@@ -166,9 +292,10 @@ var SecureStorage = class {
166
292
  return keychainValue;
167
293
  }
168
294
  try {
169
- const value = await AsyncStorage.getItem(`@paywallo:${key}`);
295
+ const value = await nativeStorage.get(`${REGULAR_KEY_PREFIX}${key}`);
170
296
  if (value) {
171
297
  await this.setToKeychain(key, value);
298
+ nativeStorage.remove(`${REGULAR_KEY_PREFIX}${key}`).catch(() => void 0);
172
299
  }
173
300
  return value;
174
301
  } catch {
@@ -176,20 +303,13 @@ var SecureStorage = class {
176
303
  }
177
304
  }
178
305
  async set(key, value) {
179
- try {
180
- await Promise.all([
181
- AsyncStorage.setItem(`@paywallo:${key}`, value),
182
- this.setToKeychain(key, value)
183
- ]);
184
- return true;
185
- } catch {
186
- return false;
187
- }
306
+ return this.setToKeychain(key, value);
188
307
  }
189
308
  async remove(key) {
190
309
  try {
191
310
  await Promise.all([
192
- AsyncStorage.removeItem(`@paywallo:${key}`),
311
+ // Also remove the legacy plaintext copy if it exists (migration cleanup).
312
+ nativeStorage.remove(`${REGULAR_KEY_PREFIX}${key}`).catch(() => void 0),
193
313
  this.removeFromKeychain(key)
194
314
  ]);
195
315
  return true;
@@ -198,31 +318,25 @@ var SecureStorage = class {
198
318
  }
199
319
  }
200
320
  hasKeychainSupport() {
201
- return true;
321
+ return nativeStorage.isAvailable();
202
322
  }
203
323
  async getFromKeychain(key) {
204
324
  try {
205
- const result = await Keychain.getGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
206
- if (result && result.password) {
207
- return result.password;
208
- }
209
- return null;
325
+ return await nativeStorage.secureGet(`${KEYCHAIN_KEY_PREFIX}${key}`);
210
326
  } catch {
211
327
  return null;
212
328
  }
213
329
  }
214
330
  async setToKeychain(key, value) {
215
331
  try {
216
- await Keychain.setGenericPassword("panel", value, { service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
217
- return true;
332
+ return await nativeStorage.secureSet(`${KEYCHAIN_KEY_PREFIX}${key}`, value);
218
333
  } catch {
219
334
  return false;
220
335
  }
221
336
  }
222
337
  async removeFromKeychain(key) {
223
338
  try {
224
- await Keychain.resetGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
225
- return true;
339
+ return await nativeStorage.secureRemove(`${KEYCHAIN_KEY_PREFIX}${key}`);
226
340
  } catch {
227
341
  return false;
228
342
  }
@@ -230,6 +344,57 @@ var SecureStorage = class {
230
344
  };
231
345
  var secureStorage = new SecureStorage();
232
346
 
347
+ // src/core/storage/StorageMigration.ts
348
+ var MIGRATION_FLAG_KEY = "@paywallo:_migration_v152_done";
349
+ var LEGACY_SECURE_KEYS = [
350
+ "device_id",
351
+ "device_id_fallback",
352
+ "anon_id",
353
+ "user_email",
354
+ "user_properties"
355
+ ];
356
+ var KEYCHAIN_KEY_PREFIX2 = "com.paywallo.sdk.";
357
+ async function runStorageMigrationIfNeeded() {
358
+ if (!nativeStorage.isAvailable()) return;
359
+ const alreadyDone = await nativeStorage.get(MIGRATION_FLAG_KEY);
360
+ if (alreadyDone === "1") return;
361
+ await migrateAsyncStorage();
362
+ await migrateKeychainLegacy();
363
+ await nativeStorage.set(MIGRATION_FLAG_KEY, "1");
364
+ }
365
+ async function migrateAsyncStorage() {
366
+ try {
367
+ const mod = await import("@react-native-async-storage/async-storage");
368
+ const AsyncStorage = mod.default;
369
+ const allKeys = await AsyncStorage.getAllKeys();
370
+ const paywallo = allKeys.filter((k) => k.startsWith("@paywallo:"));
371
+ if (paywallo.length === 0) return;
372
+ const pairs = await AsyncStorage.multiGet(paywallo);
373
+ for (const [key, value] of pairs) {
374
+ if (!value) continue;
375
+ const existing = await nativeStorage.get(key);
376
+ if (!existing) {
377
+ await nativeStorage.set(key, value);
378
+ }
379
+ }
380
+ } catch {
381
+ }
382
+ }
383
+ async function migrateKeychainLegacy() {
384
+ for (const key of LEGACY_SECURE_KEYS) {
385
+ try {
386
+ const legacyValue = await nativeStorage.legacySecureGet(`${KEYCHAIN_KEY_PREFIX2}${key}`);
387
+ if (!legacyValue) continue;
388
+ const newSlotKey = `${KEYCHAIN_KEY_PREFIX2}${key}`;
389
+ const existing = await nativeStorage.secureGet(newSlotKey);
390
+ if (!existing) {
391
+ await nativeStorage.secureSet(newSlotKey, legacyValue);
392
+ }
393
+ } catch {
394
+ }
395
+ }
396
+ }
397
+
233
398
  // src/domains/identity/IdentityManager.ts
234
399
  var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
235
400
  var DEVICE_ID_KEY = "device_id";
@@ -246,18 +411,31 @@ var IdentityManager = class {
246
411
  this.secureStorage = null;
247
412
  this.debug = false;
248
413
  this.initialized = false;
414
+ this.initPromise = null;
249
415
  }
250
416
  async initialize(apiClient, debug = false) {
251
417
  if (this.initialized) {
252
418
  return this.deviceId ?? "";
253
419
  }
420
+ if (this.initPromise) {
421
+ return this.initPromise;
422
+ }
423
+ this.initPromise = this._doInitialize(apiClient, debug).finally(() => {
424
+ this.initPromise = null;
425
+ });
426
+ return this.initPromise;
427
+ }
428
+ async _doInitialize(apiClient, debug) {
254
429
  this.apiClient = apiClient;
255
430
  this.debug = debug;
256
431
  this.secureStorage = new SecureStorage(debug);
432
+ await runStorageMigrationIfNeeded();
257
433
  await this.loadPersistedState();
258
434
  if (!this.deviceId) {
259
435
  try {
260
- this.deviceId = await DeviceInfo.getUniqueId();
436
+ const info = await nativeDeviceInfo.getDeviceInfo();
437
+ const deviceId = info.deviceId;
438
+ this.deviceId = !deviceId || deviceId === "unknown" ? generateUUID() : deviceId;
261
439
  } catch {
262
440
  this.deviceId = generateUUID();
263
441
  }
@@ -289,9 +467,7 @@ var IdentityManager = class {
289
467
  }
290
468
  if (email) {
291
469
  if (!EMAIL_REGEX.test(email)) {
292
- if (this.debug) {
293
- console.warn("[Paywallo] Invalid email format, skipping email field");
294
- }
470
+ if (this.debug) console.warn("[Paywallo] Invalid email format, skipping email field");
295
471
  email = void 0;
296
472
  } else {
297
473
  this.email = email;
@@ -400,8 +576,8 @@ var IdentityManager = class {
400
576
  var identityManager = new IdentityManager();
401
577
 
402
578
  // src/domains/iap/NativeStoreKit.ts
403
- var PaywalloStoreKitNative = NativeModules.PaywalloStoreKit ?? null;
404
- function isAvailable() {
579
+ var PaywalloStoreKitNative = NativeModules3.PaywalloStoreKit ?? null;
580
+ function isAvailable3() {
405
581
  return PaywalloStoreKitNative != null;
406
582
  }
407
583
  function mapNativeProduct(native) {
@@ -477,7 +653,7 @@ function mapNativePurchase(native) {
477
653
  };
478
654
  }
479
655
  var nativeStoreKit = {
480
- isAvailable,
656
+ isAvailable: isAvailable3,
481
657
  async getProducts(productIds) {
482
658
  if (!PaywalloStoreKitNative) {
483
659
  return [];
@@ -494,8 +670,8 @@ var nativeStoreKit = {
494
670
  }
495
671
  const distinctId = identityManager.getDistinctId();
496
672
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
497
- if (distinctId && !uuidRegex.test(distinctId) && PaywalloClient.getConfig()?.debug) {
498
- console.warn("[Paywallo] distinctId is not a valid UUID for appAccountToken:", distinctId);
673
+ if (distinctId && !uuidRegex.test(distinctId)) {
674
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] distinctId is not a valid UUID for appAccountToken:", distinctId);
499
675
  }
500
676
  try {
501
677
  const result = await PaywalloStoreKitNative.purchase(productId, distinctId ?? null);
@@ -516,7 +692,7 @@ var nativeStoreKit = {
516
692
  try {
517
693
  await PaywalloStoreKitNative.finishTransaction(transactionId);
518
694
  } catch (err) {
519
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction native error", err);
695
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] finishTransaction native error", err);
520
696
  }
521
697
  },
522
698
  async getActiveTransactions() {
@@ -525,7 +701,7 @@ var nativeStoreKit = {
525
701
  const results = await PaywalloStoreKitNative.getActiveTransactions();
526
702
  return results.map(mapNativePurchase);
527
703
  } catch (err) {
528
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] getActiveTransactions failed", err);
704
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] getActiveTransactions failed", err);
529
705
  return [];
530
706
  }
531
707
  }
@@ -536,6 +712,7 @@ var IAPService = class {
536
712
  constructor(apiClient) {
537
713
  this.productsCache = /* @__PURE__ */ new Map();
538
714
  this.apiClient = null;
715
+ this.purchaseInFlight = null;
539
716
  this.apiClient = apiClient ?? null;
540
717
  }
541
718
  get debug() {
@@ -571,7 +748,7 @@ var IAPService = class {
571
748
  }
572
749
  return products;
573
750
  } catch (err) {
574
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] loadProducts failed", err);
751
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] loadProducts failed", err);
575
752
  return [];
576
753
  }
577
754
  }
@@ -617,7 +794,7 @@ var IAPService = class {
617
794
  try {
618
795
  await nativeStoreKit.finishTransaction(transactionId);
619
796
  } catch (err) {
620
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction failed", err);
797
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] finishTransaction failed", err);
621
798
  }
622
799
  }
623
800
  async purchase(productId, options) {
@@ -627,27 +804,36 @@ var IAPService = class {
627
804
  error: new PurchaseError(PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE, "In-app purchases are not available on this device")
628
805
  };
629
806
  }
807
+ if (this.purchaseInFlight) {
808
+ return {
809
+ success: false,
810
+ error: new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, "A purchase is already in progress")
811
+ };
812
+ }
813
+ this.purchaseInFlight = productId;
630
814
  const debug = PaywalloClient.getConfig()?.debug;
631
- if (debug) console.info(`[Paywallo PURCHASE] starting ${productId}`);
815
+ if (debug) console.log(`[Paywallo PURCHASE] starting ${productId}`);
632
816
  try {
633
817
  const purchase = await nativeStoreKit.purchase(productId);
634
818
  if (!purchase) {
635
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} cancelled`);
819
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} cancelled`);
636
820
  return { success: false };
637
821
  }
638
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} approved`, { transactionId: purchase.transactionId });
822
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} approved`, { transactionId: purchase.transactionId });
639
823
  await this.finishTransaction(purchase.transactionId);
640
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} finished`);
824
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} finished`);
641
825
  this.validateWithServer(purchase, productId, options).then(() => {
642
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} server validated`);
826
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} server validated`);
643
827
  }).catch((err) => {
644
- if (debug) console.warn("[Paywallo PURCHASE] background validation failed", err);
828
+ if (debug) console.error("[Paywallo PURCHASE] background validation failed", err);
645
829
  });
646
830
  return { success: true, purchase };
647
831
  } catch (error) {
648
- if (debug) console.warn(`[Paywallo PURCHASE] ${productId} failed`, error);
832
+ if (debug) console.error(`[Paywallo PURCHASE] ${productId} failed`, error);
649
833
  const e = error instanceof PurchaseError ? error : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, error instanceof Error ? error.message : String(error));
650
834
  return { success: false, error: e };
835
+ } finally {
836
+ this.purchaseInFlight = null;
651
837
  }
652
838
  }
653
839
  async restore() {
@@ -682,14 +868,14 @@ var IAPService = class {
682
868
  try {
683
869
  await nativeStoreKit.finishTransaction(tx.transactionId);
684
870
  } catch (err) {
685
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction failed", err);
871
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] finishTransaction failed", err);
686
872
  }
687
873
  validated.push(tx);
688
874
  }
689
875
  }
690
876
  return validated;
691
877
  } catch (err) {
692
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] restore failed", err);
878
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] restore failed", err);
693
879
  return [];
694
880
  }
695
881
  }
@@ -720,7 +906,7 @@ function usePaywallTracking() {
720
906
  ...sessionId && { sessionId }
721
907
  });
722
908
  } catch (err) {
723
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackDismissed failed", err);
909
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] trackDismissed failed", err);
724
910
  }
725
911
  }, []);
726
912
  const trackProductSelected = useCallback((opts) => {
@@ -735,7 +921,7 @@ function usePaywallTracking() {
735
921
  ...opts.campaignId && { campaignId: opts.campaignId },
736
922
  ...sessionId && { sessionId }
737
923
  }).catch((err) => {
738
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackProductSelected failed", err);
924
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] trackProductSelected failed", err);
739
925
  });
740
926
  }, []);
741
927
  const trackPurchased = useCallback(async (opts) => {
@@ -752,14 +938,13 @@ function usePaywallTracking() {
752
938
  ...sessionId && { sessionId }
753
939
  });
754
940
  } catch (err) {
755
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackPurchased failed", err);
941
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] trackPurchased failed", err);
756
942
  }
757
943
  }, []);
758
944
  return { trackDismissed, trackProductSelected, trackPurchased };
759
945
  }
760
946
 
761
947
  // src/domains/paywall/usePaywallActions.ts
762
- var SCREEN_HEIGHT = Dimensions.get("window").height;
763
948
  function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId, openAnim) {
764
949
  const [isPurchasing, setIsPurchasing] = useState(false);
765
950
  const [isClosing, setIsClosing] = useState(false);
@@ -770,7 +955,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
770
955
  if (isClosing) return;
771
956
  setIsClosing(true);
772
957
  const animTarget = openAnim ?? slideAnim;
773
- Animated.timing(animTarget, { toValue: SCREEN_HEIGHT, duration: 350, useNativeDriver: true }).start(() => {
958
+ Animated.timing(animTarget, { toValue: Dimensions.get("window").height, duration: 350, useNativeDriver: true }).start(() => {
774
959
  if (paywallConfig) {
775
960
  const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
776
961
  void trackDismissed({ placement, paywallId: paywallConfig.id, durationSeconds, action: "close", variantKey, campaignId });
@@ -859,6 +1044,7 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
859
1044
  trackingDoneRef.current = false;
860
1045
  return;
861
1046
  }
1047
+ let cancelled = false;
862
1048
  const load = async () => {
863
1049
  setError(null);
864
1050
  try {
@@ -879,27 +1065,30 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
879
1065
  if (!fetched) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_FOUND, `Paywall not found for placement: ${placement}`);
880
1066
  config = fetched;
881
1067
  }
1068
+ if (cancelled) return;
882
1069
  setPaywallConfig(config);
883
1070
  const productIds = [];
884
1071
  if (config.primaryProductId) productIds.push(config.primaryProductId);
885
1072
  if (config.secondaryProductId) productIds.push(config.secondaryProductId);
886
1073
  const snapProducts = preloadedProductsRef.current;
887
1074
  if (snapProducts && snapProducts.size > 0) {
888
- setProducts(snapProducts);
1075
+ if (!cancelled) setProducts(snapProducts);
889
1076
  } else if (productIds.length > 0) {
890
1077
  try {
891
1078
  const storeProducts = await getIAPService().loadProducts(productIds);
892
- const map = /* @__PURE__ */ new Map();
893
- for (const p of storeProducts) map.set(p.productId, p);
894
- setProducts(map);
1079
+ if (!cancelled) {
1080
+ const map = /* @__PURE__ */ new Map();
1081
+ for (const p of storeProducts) map.set(p.productId, p);
1082
+ setProducts(map);
1083
+ }
895
1084
  } catch (err) {
896
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] loadProducts in paywall failed", err);
897
- setProducts(/* @__PURE__ */ new Map());
1085
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] loadProducts in paywall failed", err);
1086
+ if (!cancelled) setProducts(/* @__PURE__ */ new Map());
898
1087
  }
899
1088
  } else {
900
- setProducts(/* @__PURE__ */ new Map());
1089
+ if (!cancelled) setProducts(/* @__PURE__ */ new Map());
901
1090
  }
902
- if (!trackingDoneRef.current) {
1091
+ if (!trackingDoneRef.current && !cancelled) {
903
1092
  trackingDoneRef.current = true;
904
1093
  const sessionId = PaywalloClient.getSessionId();
905
1094
  await apiClient.trackEvent("$paywall_viewed", PaywalloClient.getDistinctId(), {
@@ -911,17 +1100,21 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
911
1100
  });
912
1101
  }
913
1102
  } catch (err) {
1103
+ if (cancelled) return;
914
1104
  const e = err instanceof Error ? err : new PaywallError(PAYWALL_ERROR_CODES.LOAD_FAILED, String(err));
915
1105
  setError(e);
916
1106
  }
917
1107
  };
918
1108
  void load();
1109
+ return () => {
1110
+ cancelled = true;
1111
+ };
919
1112
  }, [placement, visible, variantKey, campaignId]);
920
1113
  return { paywallConfig, products, error };
921
1114
  }
922
1115
 
923
1116
  // src/utils/localization.ts
924
- import { NativeModules as NativeModules2, Platform as Platform3 } from "react-native";
1117
+ import { NativeModules as NativeModules4, Platform as Platform3 } from "react-native";
925
1118
  var currentLanguage = "pt-BR";
926
1119
  var defaultLanguage = "pt-BR";
927
1120
  function setCurrentLanguage(language) {
@@ -944,10 +1137,10 @@ function getLocalizedString(text) {
944
1137
  function detectDeviceLanguage() {
945
1138
  try {
946
1139
  if (Platform3.OS === "ios") {
947
- const settings = NativeModules2.SettingsManager?.settings;
1140
+ const settings = NativeModules4.SettingsManager?.settings;
948
1141
  return settings?.AppleLocale ?? settings?.AppleLanguages?.[0] ?? "en-US";
949
1142
  }
950
- return NativeModules2.I18nManager?.localeIdentifier ?? "en-US";
1143
+ return NativeModules4.I18nManager?.localeIdentifier ?? "en-US";
951
1144
  } catch {
952
1145
  return "en-US";
953
1146
  }
@@ -983,9 +1176,8 @@ var SDK_STRINGS = {
983
1176
  };
984
1177
  function getSdkString(key) {
985
1178
  const map = SDK_STRINGS[key];
986
- const lang = currentLanguage;
987
- if (lang in map) return map[lang];
988
- const base = lang.split("-")[0];
1179
+ if (currentLanguage in map) return map[currentLanguage];
1180
+ const base = currentLanguage.split("-")[0];
989
1181
  if (base && base in map) return map[base];
990
1182
  if (defaultLanguage in map) return map[defaultLanguage];
991
1183
  return Object.values(map)[0];
@@ -1005,13 +1197,21 @@ import { useCallback as useCallback3, useEffect as useEffect2, useMemo, useRef a
1005
1197
  import {
1006
1198
  Linking,
1007
1199
  NativeEventEmitter,
1008
- NativeModules as NativeModules3,
1200
+ NativeModules as NativeModules5,
1009
1201
  requireNativeComponent,
1010
1202
  StyleSheet,
1011
1203
  View
1012
1204
  } from "react-native";
1013
1205
 
1014
1206
  // src/domains/paywall/parseWebViewMessage.ts
1207
+ var ALLOWED_MESSAGE_TYPES = /* @__PURE__ */ new Set([
1208
+ "purchase",
1209
+ "close",
1210
+ "restore",
1211
+ "select-product",
1212
+ "open-url",
1213
+ "ready"
1214
+ ]);
1015
1215
  function deriveMessageId(message) {
1016
1216
  if (message.id) return message.id;
1017
1217
  const payloadKey = message.payload?.productId ?? message.payload?.url ?? (message.payload?.timestamp != null ? String(message.payload.timestamp) : "");
@@ -1020,10 +1220,20 @@ function deriveMessageId(message) {
1020
1220
  function parseWebViewMessage(raw) {
1021
1221
  try {
1022
1222
  const parsed = JSON.parse(raw);
1023
- if (typeof parsed?.type !== "string") {
1223
+ if (typeof parsed?.type !== "string" || !ALLOWED_MESSAGE_TYPES.has(parsed.type)) {
1024
1224
  return null;
1025
1225
  }
1026
- return parsed;
1226
+ const payload = parsed.payload;
1227
+ const safePayload = payload !== null && typeof payload === "object" ? {
1228
+ productId: typeof payload.productId === "string" ? String(payload.productId) : void 0,
1229
+ url: typeof payload.url === "string" ? String(payload.url) : void 0,
1230
+ timestamp: typeof payload.timestamp === "number" ? Number(payload.timestamp) : void 0
1231
+ } : void 0;
1232
+ return {
1233
+ type: parsed.type,
1234
+ id: typeof parsed.id === "string" ? parsed.id : void 0,
1235
+ payload: safePayload
1236
+ };
1027
1237
  } catch {
1028
1238
  return null;
1029
1239
  }
@@ -1058,7 +1268,7 @@ function getNativeWebView() {
1058
1268
  var webViewEmitter = null;
1059
1269
  function getWebViewEmitter() {
1060
1270
  if (!webViewEmitter) {
1061
- webViewEmitter = new NativeEventEmitter(NativeModules3.PaywalloWebViewModule);
1271
+ webViewEmitter = new NativeEventEmitter(NativeModules5.PaywalloWebViewModule);
1062
1272
  }
1063
1273
  return webViewEmitter;
1064
1274
  }
@@ -1133,12 +1343,24 @@ function PaywallWebView({
1133
1343
  setScriptToInject(script);
1134
1344
  }, [craftData, products, primaryProductId, secondaryProductId]);
1135
1345
  const processedIdsRef = useRef3(/* @__PURE__ */ new Set());
1346
+ const dedupeTimersRef = useRef3(/* @__PURE__ */ new Map());
1347
+ useEffect2(() => {
1348
+ return () => {
1349
+ for (const timerId of dedupeTimersRef.current.values()) clearTimeout(timerId);
1350
+ dedupeTimersRef.current.clear();
1351
+ processedIdsRef.current.clear();
1352
+ };
1353
+ }, []);
1136
1354
  const handleParsedMessage = useCallback3(
1137
1355
  (message) => {
1138
1356
  const msgId = deriveMessageId(message);
1139
1357
  if (processedIdsRef.current.has(msgId)) return;
1140
1358
  processedIdsRef.current.add(msgId);
1141
- setTimeout(() => processedIdsRef.current.delete(msgId), 5e3);
1359
+ const timerId = setTimeout(() => {
1360
+ processedIdsRef.current.delete(msgId);
1361
+ dedupeTimersRef.current.delete(msgId);
1362
+ }, 5e3);
1363
+ dedupeTimersRef.current.set(msgId, timerId);
1142
1364
  switch (message.type) {
1143
1365
  case "ready":
1144
1366
  if (!paywallDataSent.current) {
@@ -1159,7 +1381,7 @@ function PaywallWebView({
1159
1381
  case "open-url":
1160
1382
  if (message.payload?.url) {
1161
1383
  const url = message.payload.url;
1162
- if (/^https?:\/\//i.test(url) || /^mailto:/i.test(url)) {
1384
+ if (/^https?:\/\//i.test(url) || /^mailto:/i.test(url) || /^itms-apps:/i.test(url) || /^itms-services:/i.test(url) || /^market:/i.test(url)) {
1163
1385
  void Linking.openURL(url);
1164
1386
  }
1165
1387
  }
@@ -1257,7 +1479,6 @@ var styles = StyleSheet.create({
1257
1479
 
1258
1480
  // src/domains/paywall/PaywallModal.tsx
1259
1481
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1260
- var SCREEN_HEIGHT2 = Dimensions2.get("window").height;
1261
1482
  function ErrorFallback({ description, onRetry, onClose }) {
1262
1483
  const overrides = PaywalloClient.getConfig()?.errorStrings;
1263
1484
  const title = overrides?.title ?? getSdkString("paywallErrorTitle");
@@ -1287,7 +1508,8 @@ function PaywallModal({
1287
1508
  variantKey,
1288
1509
  campaignId
1289
1510
  );
1290
- const openAnim = useRef4(new Animated2.Value(SCREEN_HEIGHT2)).current;
1511
+ const screenHeight = Dimensions2.get("window").height;
1512
+ const openAnim = useRef4(new Animated2.Value(screenHeight)).current;
1291
1513
  const [modalVisible, setModalVisible] = useState4(false);
1292
1514
  const handleResult = useCallback4((result) => {
1293
1515
  setModalVisible(false);
@@ -1316,7 +1538,7 @@ function PaywallModal({
1316
1538
  useEffect3(() => {
1317
1539
  if (visible) {
1318
1540
  setModalVisible(true);
1319
- openAnim.setValue(SCREEN_HEIGHT2);
1541
+ openAnim.setValue(Dimensions2.get("window").height);
1320
1542
  Animated2.timing(openAnim, {
1321
1543
  toValue: 0,
1322
1544
  duration: 550,
@@ -1466,6 +1688,7 @@ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
1466
1688
  resolverRef.current({ presented: false, purchased: false, cancelled: true, restored: false });
1467
1689
  resolverRef.current = null;
1468
1690
  }
1691
+ setShowingPreloadedWebView(false);
1469
1692
  };
1470
1693
  }, []);
1471
1694
  const presentPreloadedWebView = useCallback5(() => {
@@ -1511,8 +1734,9 @@ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
1511
1734
  }
1512
1735
 
1513
1736
  // src/domains/subscription/SubscriptionCache.ts
1514
- var LEGACY_CACHE_KEY = "subscription_cache";
1737
+ var LEGACY_CACHE_KEY = "@panel:subscription_cache";
1515
1738
  var CACHE_KEY_PREFIX = "subscription_cache:";
1739
+ var CACHE_INDEX_KEY = "subscription_cache:__index__";
1516
1740
  var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
1517
1741
  function keyFor(distinctId) {
1518
1742
  return `${CACHE_KEY_PREFIX}${distinctId}`;
@@ -1592,9 +1816,30 @@ var SubscriptionCache = class {
1592
1816
  this.memoryCache.set(distinctId, cached);
1593
1817
  try {
1594
1818
  await this.storage.set(keyFor(distinctId), JSON.stringify(cached));
1819
+ await this.addToIndex(distinctId);
1595
1820
  } catch {
1596
1821
  }
1597
1822
  }
1823
+ async addToIndex(distinctId) {
1824
+ try {
1825
+ const raw = await this.storage.get(CACHE_INDEX_KEY);
1826
+ const ids = raw ? JSON.parse(raw) : [];
1827
+ if (!ids.includes(distinctId)) {
1828
+ ids.push(distinctId);
1829
+ await this.storage.set(CACHE_INDEX_KEY, JSON.stringify(ids));
1830
+ }
1831
+ } catch {
1832
+ }
1833
+ }
1834
+ async readIndex() {
1835
+ try {
1836
+ const raw = await this.storage.get(CACHE_INDEX_KEY);
1837
+ if (!raw) return [];
1838
+ return JSON.parse(raw);
1839
+ } catch {
1840
+ return [];
1841
+ }
1842
+ }
1598
1843
  async invalidate(distinctId) {
1599
1844
  if (!distinctId) return;
1600
1845
  this.memoryCache.delete(distinctId);
@@ -1604,11 +1849,15 @@ var SubscriptionCache = class {
1604
1849
  }
1605
1850
  }
1606
1851
  async invalidateAll() {
1852
+ const memKeys = Array.from(this.memoryCache.keys());
1853
+ const indexKeys = await this.readIndex();
1854
+ const allKeys = Array.from(/* @__PURE__ */ new Set([...memKeys, ...indexKeys]));
1607
1855
  this.memoryCache.clear();
1608
- try {
1609
- await this.storage.remove(LEGACY_CACHE_KEY);
1610
- } catch {
1611
- }
1856
+ await Promise.allSettled([
1857
+ this.storage.remove(LEGACY_CACHE_KEY),
1858
+ this.storage.remove(CACHE_INDEX_KEY),
1859
+ ...allKeys.map((k) => this.storage.remove(keyFor(k)))
1860
+ ]);
1612
1861
  }
1613
1862
  isExpired(cached) {
1614
1863
  return Date.now() - cached.cachedAt > this.ttl;
@@ -1764,7 +2013,7 @@ var SubscriptionManagerClass = class {
1764
2013
  var subscriptionManager = new SubscriptionManagerClass();
1765
2014
 
1766
2015
  // src/core/ApiClient.ts
1767
- import { Platform as Platform4 } from "react-native";
2016
+ import { Platform as Platform5 } from "react-native";
1768
2017
 
1769
2018
  // src/core/apiUrlUtils.ts
1770
2019
  var DEFAULT_WEB_URL = "https://paywallo.com.br";
@@ -1812,7 +2061,7 @@ async function validatePurchase(client, appKey, platform, receipt, productId, tr
1812
2061
  false
1813
2062
  );
1814
2063
  if (!response.ok) {
1815
- throw new Error(`Server validation failed: HTTP ${response.status}`);
2064
+ throw new PurchaseError(PURCHASE_ERROR_CODES.VALIDATION_FAILED, `Server validation failed: HTTP ${response.status}`);
1816
2065
  }
1817
2066
  return response.data;
1818
2067
  }
@@ -1821,7 +2070,7 @@ async function getSubscriptionStatus(client, baseUrl, appKey, distinctId) {
1821
2070
  url.searchParams.set("distinctId", distinctId);
1822
2071
  const response = await client.get(url.toString());
1823
2072
  if (!response.ok) {
1824
- throw new Error(`Subscription status check failed: HTTP ${response.status}`);
2073
+ throw new PurchaseError(PURCHASE_ERROR_CODES.NETWORK_ERROR, `Subscription status check failed: HTTP ${response.status}`);
1825
2074
  }
1826
2075
  return response.data;
1827
2076
  }
@@ -1863,7 +2112,7 @@ var HttpClient = class {
1863
2112
  constructor(baseUrl, config) {
1864
2113
  this.config = {
1865
2114
  baseUrl,
1866
- timeout: config?.timeout ?? DEFAULT_HTTP_CONFIG.timeout,
2115
+ timeout: config?.timeout ?? DEFAULT_HTTP_CONFIG.timeout ?? 1e4,
1867
2116
  retry: { ...DEFAULT_RETRY_CONFIG, ...config?.retry },
1868
2117
  debug: config?.debug ?? false
1869
2118
  };
@@ -1893,7 +2142,7 @@ var HttpClient = class {
1893
2142
  if (this.shouldRetry(response.status, retryConfig)) {
1894
2143
  if (state.attempt < retryConfig.maxRetries) {
1895
2144
  state.attempt++;
1896
- const delay = this.calculateDelay(state.attempt, retryConfig);
2145
+ const delay = this.calculateDelayForResponse(state.attempt, response.headers, retryConfig);
1897
2146
  await this.sleep(delay);
1898
2147
  continue;
1899
2148
  }
@@ -1947,7 +2196,7 @@ var HttpClient = class {
1947
2196
  };
1948
2197
  } catch (error) {
1949
2198
  this.log("Request failed", {
1950
- url,
2199
+ url: this.redactUrl(url),
1951
2200
  error: error instanceof Error ? error.message : String(error)
1952
2201
  });
1953
2202
  throw error;
@@ -1960,6 +2209,14 @@ var HttpClient = class {
1960
2209
  }
1961
2210
  buildUrl(path) {
1962
2211
  if (path.startsWith("http://") || path.startsWith("https://")) {
2212
+ if (path.startsWith("http://")) {
2213
+ const isLocalDev = /^http:\/\/(localhost|127\.0\.0\.1|192\.168\.\d{1,3}\.\d{1,3})/.test(path);
2214
+ if (!isLocalDev) {
2215
+ const sanitizedPath = path.split("?")[0];
2216
+ this.log("Blocked http:// request \u2014 only HTTPS is allowed in production:", sanitizedPath);
2217
+ throw new ClientError(CLIENT_ERROR_CODES.INSECURE_REQUEST, `Insecure HTTP request blocked: ${sanitizedPath}`);
2218
+ }
2219
+ }
1963
2220
  return path;
1964
2221
  }
1965
2222
  return `${this.config.baseUrl}${path}`;
@@ -1999,9 +2256,28 @@ var HttpClient = class {
1999
2256
  const jitter = Math.random() * 0.3 * exponentialDelay;
2000
2257
  return Math.min(exponentialDelay + jitter, config.maxDelay);
2001
2258
  }
2259
+ calculateDelayForResponse(attempt, headers, config) {
2260
+ const retryAfter = headers["retry-after"];
2261
+ if (retryAfter) {
2262
+ const seconds = Number(retryAfter);
2263
+ if (!Number.isNaN(seconds) && seconds > 0) {
2264
+ return Math.min(seconds * 1e3, config.maxDelay);
2265
+ }
2266
+ const date = new Date(retryAfter);
2267
+ if (!Number.isNaN(date.getTime())) {
2268
+ const ms = date.getTime() - Date.now();
2269
+ if (ms > 0) return Math.min(ms, config.maxDelay);
2270
+ }
2271
+ }
2272
+ return this.calculateDelay(attempt, config);
2273
+ }
2002
2274
  sleep(ms) {
2003
2275
  return new Promise((resolve) => setTimeout(resolve, ms));
2004
2276
  }
2277
+ /** Strips path segments that look like app keys (alphanumeric, 20–64 chars) from URLs before logging. */
2278
+ redactUrl(url) {
2279
+ return url.replace(/\/([a-zA-Z0-9_-]{20,64})(\?|$|\/)/g, "/[REDACTED]$2");
2280
+ }
2005
2281
  log(...args) {
2006
2282
  if (this.config.debug) console.log("[Paywallo:Http]", ...args);
2007
2283
  }
@@ -2053,7 +2329,17 @@ var NetworkMonitorClass = class {
2053
2329
  setupAppStateListener() {
2054
2330
  this.appStateSubscription = AppState.addEventListener("change", (state) => {
2055
2331
  if (state === "active") {
2332
+ if (!this.checkIntervalId) {
2333
+ this.checkIntervalId = setInterval(() => {
2334
+ void this.checkConnectivity();
2335
+ }, this.config.checkInterval);
2336
+ }
2056
2337
  void this.checkConnectivity();
2338
+ } else if (state === "background" || state === "inactive") {
2339
+ if (this.checkIntervalId) {
2340
+ clearInterval(this.checkIntervalId);
2341
+ this.checkIntervalId = null;
2342
+ }
2057
2343
  }
2058
2344
  });
2059
2345
  }
@@ -2135,7 +2421,6 @@ var NetworkMonitorClass = class {
2135
2421
  var networkMonitor = new NetworkMonitorClass();
2136
2422
 
2137
2423
  // src/core/queue/OfflineQueue.ts
2138
- import AsyncStorage2 from "@react-native-async-storage/async-storage";
2139
2424
  init_uuid();
2140
2425
 
2141
2426
  // src/core/queue/deduplication.ts
@@ -2197,7 +2482,7 @@ var OfflineQueueClass = class {
2197
2482
  }
2198
2483
  async loadFromStorage() {
2199
2484
  try {
2200
- const stored = await AsyncStorage2.getItem(this.config.storageKey);
2485
+ const stored = await nativeStorage.get(this.config.storageKey);
2201
2486
  if (stored) {
2202
2487
  this.queue = JSON.parse(stored);
2203
2488
  }
@@ -2207,7 +2492,7 @@ var OfflineQueueClass = class {
2207
2492
  }
2208
2493
  async saveToStorage() {
2209
2494
  try {
2210
- await AsyncStorage2.setItem(this.config.storageKey, JSON.stringify(this.queue));
2495
+ await nativeStorage.set(this.config.storageKey, JSON.stringify(this.queue));
2211
2496
  return true;
2212
2497
  } catch (error) {
2213
2498
  this.log("Failed to save queue to storage", error);
@@ -2222,7 +2507,7 @@ var OfflineQueueClass = class {
2222
2507
  cleanupExpired() {
2223
2508
  const now = Date.now();
2224
2509
  const before = this.queue.length;
2225
- this.queue = this.queue.filter((item) => Date.now() - item.timestamp < this.config.maxAge);
2510
+ this.queue = this.queue.filter((item) => now - item.timestamp < this.config.maxAge);
2226
2511
  if (before !== this.queue.length) {
2227
2512
  void this.saveToStorage();
2228
2513
  }
@@ -2245,10 +2530,16 @@ var OfflineQueueClass = class {
2245
2530
  return this.queue[existingIndex];
2246
2531
  }
2247
2532
  if (this.queue.length >= this.config.maxItems) {
2248
- const removed = this.queue.shift();
2249
- if (removed) {
2250
- this.emit({ type: "item:evicted", item: removed, queueSize: this.queue.length });
2533
+ let evictIndex = 0;
2534
+ for (let i = 1; i < this.queue.length; i++) {
2535
+ const candidate = this.queue[i];
2536
+ const current = this.queue[evictIndex];
2537
+ if (candidate.attempts > current.attempts || candidate.attempts === current.attempts && candidate.timestamp < current.timestamp) {
2538
+ evictIndex = i;
2539
+ }
2251
2540
  }
2541
+ const [removed] = this.queue.splice(evictIndex, 1);
2542
+ this.emit({ type: "item:evicted", item: removed, queueSize: this.queue.length });
2252
2543
  }
2253
2544
  this.queue.push(item);
2254
2545
  await this.saveToStorage();
@@ -2528,7 +2819,6 @@ var QueueProcessorClass = class {
2528
2819
  id: item.id,
2529
2820
  url: item.url,
2530
2821
  method: item.method,
2531
- headers: item.headers,
2532
2822
  attempts: item.attempts
2533
2823
  });
2534
2824
  const response = await this.httpClient.request(item.url, {
@@ -2546,7 +2836,7 @@ var QueueProcessorClass = class {
2546
2836
  id: item.id,
2547
2837
  url: item.url,
2548
2838
  status: response.status,
2549
- appKeyInHeaders: item.headers?.["X-App-Key"]
2839
+ hasAppKey: item.headers?.["X-App-Key"] != null
2550
2840
  });
2551
2841
  return { success: false, permanent: true, status: response.status };
2552
2842
  }
@@ -2668,15 +2958,96 @@ var ApiCache = class {
2668
2958
  }
2669
2959
  };
2670
2960
 
2961
+ // src/core/EventBatcher.ts
2962
+ import { Platform as Platform4 } from "react-native";
2963
+ var BATCH_MAX_SIZE = 10;
2964
+ var BATCH_FLUSH_MS = 5e3;
2965
+ var EventBatcher = class {
2966
+ constructor(post, debug = false) {
2967
+ this.batch = [];
2968
+ this.timer = null;
2969
+ this.post = post;
2970
+ this.debug = debug;
2971
+ }
2972
+ async track(eventName, distinctId, environment, properties, timestamp) {
2973
+ const EVENT_NAME_REGEX = /^(\$[a-zA-Z0-9_]+|[a-z][a-z0-9_]*)$/;
2974
+ if (!EVENT_NAME_REGEX.test(eventName)) {
2975
+ if (this.debug) console.warn(`[Paywallo] Invalid event name "${eventName}". Must be snake_case or start with $`);
2976
+ }
2977
+ const event = {
2978
+ eventName,
2979
+ distinctId,
2980
+ properties: { ...properties, platform: Platform4.OS },
2981
+ timestamp: timestamp ?? Date.now(),
2982
+ environment: environment.toLowerCase()
2983
+ };
2984
+ this.batch.push(event);
2985
+ if (this.batch.length >= BATCH_MAX_SIZE) {
2986
+ if (this.timer !== null) {
2987
+ clearTimeout(this.timer);
2988
+ this.timer = null;
2989
+ }
2990
+ await this.flush();
2991
+ return;
2992
+ }
2993
+ if (this.timer !== null) clearTimeout(this.timer);
2994
+ this.timer = setTimeout(() => {
2995
+ this.timer = null;
2996
+ void this.flush();
2997
+ }, BATCH_FLUSH_MS);
2998
+ }
2999
+ async flush() {
3000
+ if (this.batch.length === 0) return;
3001
+ const batch = this.batch.splice(0, this.batch.length);
3002
+ if (this.timer !== null) {
3003
+ clearTimeout(this.timer);
3004
+ this.timer = null;
3005
+ }
3006
+ try {
3007
+ await this.post("/api/v1/events/batch", { events: batch }, `event_batch:${batch.length}`);
3008
+ } catch {
3009
+ for (const event of batch) {
3010
+ await this.post("/api/v1/events", event, `event:${event.eventName}`);
3011
+ }
3012
+ }
3013
+ }
3014
+ dispose() {
3015
+ if (this.timer !== null) {
3016
+ clearTimeout(this.timer);
3017
+ this.timer = null;
3018
+ }
3019
+ this.batch = [];
3020
+ }
3021
+ };
3022
+
3023
+ // src/core/FlagStorage.ts
3024
+ var FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
3025
+ async function readFlagFromStorage(flagKey, distinctId) {
3026
+ try {
3027
+ const raw = await secureStorage.get(`flag:${flagKey}:${distinctId}`);
3028
+ if (!raw) return null;
3029
+ const parsed = JSON.parse(raw);
3030
+ if (typeof parsed !== "object" || parsed === null || !("variant" in parsed) || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") return null;
3031
+ const { variant, payload, cachedAt } = parsed;
3032
+ if (Date.now() - cachedAt > FLAG_MAX_AGE_MS) return null;
3033
+ return { variant, payload };
3034
+ } catch {
3035
+ return null;
3036
+ }
3037
+ }
3038
+ async function writeFlagToStorage(flagKey, distinctId, flag) {
3039
+ try {
3040
+ const value = JSON.stringify({ variant: flag.variant, payload: flag.payload, cachedAt: Date.now() });
3041
+ await secureStorage.set(`flag:${flagKey}:${distinctId}`, value);
3042
+ } catch {
3043
+ }
3044
+ }
3045
+
2671
3046
  // src/core/ApiClient.ts
2672
- var SDK_VERSION = "1.5.0";
2673
- var _ApiClient = class _ApiClient {
3047
+ var SDK_VERSION = "1.5.2";
3048
+ var ApiClient = class {
2674
3049
  constructor(appKey, apiUrl, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
2675
3050
  this.cache = new ApiCache();
2676
- this.eventBatch = [];
2677
- this.batchTimer = null;
2678
- this.BATCH_MAX_SIZE = 10;
2679
- this.BATCH_FLUSH_MS = 5e3;
2680
3051
  this.appKey = appKey;
2681
3052
  this.baseUrl = apiUrl;
2682
3053
  this.webUrl = deriveWebUrl(this.baseUrl);
@@ -2684,6 +3055,7 @@ var _ApiClient = class _ApiClient {
2684
3055
  this.environment = environment;
2685
3056
  this.offlineQueueEnabled = offlineQueueEnabled;
2686
3057
  this.httpClient = new HttpClient(this.baseUrl, { timeout, debug });
3058
+ this.batcher = new EventBatcher((url, payload, label) => this.postWithQueue(url, payload, label), debug);
2687
3059
  }
2688
3060
  getHttpClient() {
2689
3061
  return this.httpClient;
@@ -2694,6 +3066,9 @@ var _ApiClient = class _ApiClient {
2694
3066
  getWebUrl() {
2695
3067
  return this.webUrl;
2696
3068
  }
3069
+ getAppKey() {
3070
+ return this.appKey;
3071
+ }
2697
3072
  getEnvironment() {
2698
3073
  return this.environment;
2699
3074
  }
@@ -2729,79 +3104,33 @@ var _ApiClient = class _ApiClient {
2729
3104
  message,
2730
3105
  context,
2731
3106
  sdkVersion: SDK_VERSION,
2732
- platform: Platform4.OS
3107
+ platform: Platform5.OS
2733
3108
  }, true);
2734
3109
  } catch (err) {
2735
- if (this.debug) console.warn("[Paywallo:ApiClient] reportError failed", err);
3110
+ if (this.debug) console.log("[Paywallo:ApiClient] reportError failed", err);
2736
3111
  }
2737
3112
  }
2738
3113
  async trackEvent(eventName, distinctId, properties, timestamp) {
2739
- const EVENT_NAME_REGEX = /^(\$[a-zA-Z0-9_]+|[a-z][a-z0-9_]*)$/;
2740
- if (!EVENT_NAME_REGEX.test(eventName)) {
2741
- if (this.debug) {
2742
- console.warn(`[Paywallo] Invalid event name "${eventName}". Must be snake_case or start with $`);
2743
- }
2744
- }
2745
- const event = {
2746
- eventName,
2747
- distinctId,
2748
- properties: { ...properties, platform: Platform4.OS },
2749
- timestamp: timestamp ?? Date.now(),
2750
- environment: this.environment.toLowerCase()
2751
- };
2752
- this.eventBatch.push(event);
2753
- if (this.eventBatch.length >= this.BATCH_MAX_SIZE) {
2754
- if (this.batchTimer !== null) {
2755
- clearTimeout(this.batchTimer);
2756
- this.batchTimer = null;
2757
- }
2758
- await this.flushEventBatch();
2759
- return;
2760
- }
2761
- if (this.batchTimer !== null) {
2762
- clearTimeout(this.batchTimer);
2763
- }
2764
- this.batchTimer = setTimeout(() => {
2765
- this.batchTimer = null;
2766
- void this.flushEventBatch();
2767
- }, this.BATCH_FLUSH_MS);
3114
+ await this.batcher.track(eventName, distinctId, this.environment, properties, timestamp);
2768
3115
  }
2769
3116
  async flushEventBatch() {
2770
- if (this.eventBatch.length === 0) return;
2771
- const batch = this.eventBatch.splice(0, this.eventBatch.length);
2772
- if (this.batchTimer !== null) {
2773
- clearTimeout(this.batchTimer);
2774
- this.batchTimer = null;
2775
- }
2776
- try {
2777
- await this.postWithQueue(
2778
- "/api/v1/events/batch",
2779
- { events: batch },
2780
- `event_batch:${batch.length}`
2781
- );
2782
- } catch (error) {
2783
- this.log("Batch send failed, falling back to individual sends:", error);
2784
- for (const event of batch) {
2785
- await this.postWithQueue(
2786
- "/api/v1/events",
2787
- event,
2788
- `event:${event.eventName}`
2789
- );
2790
- }
2791
- }
3117
+ await this.batcher.flush();
3118
+ }
3119
+ dispose() {
3120
+ this.batcher.dispose();
2792
3121
  }
2793
3122
  async identify(distinctId, properties, email, deviceId) {
2794
- await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: Platform4.OS, ...deviceId && { deviceId } }, "identify");
3123
+ await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: Platform5.OS, ...deviceId && { deviceId } }, "identify");
2795
3124
  }
2796
3125
  async startSession(distinctId, sessionId, platform, appVersion, deviceModel, osVersion) {
2797
3126
  await this.postWithQueue("/api/v1/sessions/start", {
2798
3127
  distinctId,
2799
3128
  sessionId,
2800
- platform: platform ?? Platform4.OS,
3129
+ platform: platform ?? Platform5.OS,
2801
3130
  environment: this.environment.toLowerCase(),
2802
3131
  appVersion: appVersion ?? "unknown",
2803
3132
  deviceModel: deviceModel ?? "unknown",
2804
- osVersion: osVersion ?? String(Platform4.Version)
3133
+ osVersion: osVersion ?? String(Platform5.Version)
2805
3134
  }, `session.start:${sessionId}`);
2806
3135
  return { success: true };
2807
3136
  }
@@ -2813,7 +3142,7 @@ var _ApiClient = class _ApiClient {
2813
3142
  const key = `${flagKey}:${distinctId}`;
2814
3143
  const hit = this.cache.getVariant(key);
2815
3144
  if (hit !== void 0) return hit;
2816
- const persisted = await this.readFlagFromStorage(flagKey, distinctId);
3145
+ const persisted = await readFlagFromStorage(flagKey, distinctId);
2817
3146
  if (persisted !== null) {
2818
3147
  this.cache.setVariant(key, persisted);
2819
3148
  return persisted;
@@ -2821,25 +3150,20 @@ var _ApiClient = class _ApiClient {
2821
3150
  try {
2822
3151
  const res = await this.get(`/api/v1/flags/${flagKey}?distinctId=${encodeURIComponent(distinctId)}`);
2823
3152
  if (res.status === 404) {
2824
- this.log("Flag not found (404):", flagKey);
2825
3153
  const v = { variant: null };
2826
3154
  this.cache.setVariant(key, v, this.cache.nullTTL);
2827
3155
  return v;
2828
3156
  }
2829
3157
  if (!res.ok) {
2830
- this.log("Non-OK response for variant:", flagKey, res.status);
2831
3158
  const stale = this.cache.getStaleVariant(key);
2832
- if (stale !== void 0) return stale;
2833
- return { variant: null };
3159
+ return stale !== void 0 ? stale : { variant: null };
2834
3160
  }
2835
3161
  this.cache.setVariant(key, res.data);
2836
- await this.writeFlagToStorage(flagKey, distinctId, res.data);
3162
+ await writeFlagToStorage(flagKey, distinctId, res.data);
2837
3163
  return res.data;
2838
- } catch (error) {
2839
- this.log("Failed to get variant:", flagKey, error);
3164
+ } catch {
2840
3165
  const stale = this.cache.getStaleVariant(key);
2841
- if (stale !== void 0) return stale;
2842
- return { variant: null };
3166
+ return stale !== void 0 ? stale : { variant: null };
2843
3167
  }
2844
3168
  }
2845
3169
  async getPaywall(placement) {
@@ -2848,29 +3172,22 @@ var _ApiClient = class _ApiClient {
2848
3172
  try {
2849
3173
  const res = await this.get(`/api/v1/paywalls/${placement}`);
2850
3174
  if (res.status === 404) {
2851
- this.log("Paywall not found (404):", placement);
2852
3175
  this.cache.setPaywall(placement, null, this.cache.nullTTL);
2853
3176
  return null;
2854
3177
  }
2855
3178
  if (!res.ok) {
2856
- this.log("Non-OK response for paywall:", placement, res.status);
2857
3179
  const stale = this.cache.getStalePaywall(placement);
2858
- if (stale !== void 0) return stale;
2859
- return null;
3180
+ return stale !== void 0 ? stale : null;
2860
3181
  }
2861
3182
  this.cache.setPaywall(placement, res.data);
2862
3183
  return res.data;
2863
- } catch (error) {
2864
- this.log("Failed to get paywall:", placement, error);
3184
+ } catch {
2865
3185
  const stale = this.cache.getStalePaywall(placement);
2866
- if (stale !== void 0) return stale;
2867
- return null;
3186
+ return stale !== void 0 ? stale : null;
2868
3187
  }
2869
3188
  }
2870
3189
  async getEmergencyPaywall() {
2871
- const result = await getEmergencyPaywall(this);
2872
- this.log("Got emergency paywall:", result.enabled);
2873
- return result;
3190
+ return getEmergencyPaywall(this);
2874
3191
  }
2875
3192
  async getCampaign(placement, distinctId, context) {
2876
3193
  const key = `${placement}:${distinctId}`;
@@ -2879,18 +3196,14 @@ var _ApiClient = class _ApiClient {
2879
3196
  try {
2880
3197
  const result = await getCampaignData(this, this.baseUrl, placement, distinctId, context);
2881
3198
  if (!result) {
2882
- this.log("No campaign found for placement:", placement);
2883
3199
  this.cache.setCampaign(key, null, this.cache.nullTTL);
2884
3200
  return null;
2885
3201
  }
2886
- this.log("Got campaign:", placement, result.variantKey);
2887
3202
  this.cache.setCampaign(key, result);
2888
3203
  return result;
2889
- } catch (error) {
2890
- this.log("Failed to get campaign:", placement, error);
3204
+ } catch {
2891
3205
  const stale = this.cache.getStaleCampaign(key);
2892
- if (stale !== void 0) return stale;
2893
- return null;
3206
+ return stale !== void 0 ? stale : null;
2894
3207
  }
2895
3208
  }
2896
3209
  async getPrimaryCampaign(distinctId) {
@@ -2900,63 +3213,34 @@ var _ApiClient = class _ApiClient {
2900
3213
  try {
2901
3214
  const res = await this.get(`/campaigns/public/primary?distinctId=${encodeURIComponent(distinctId)}`);
2902
3215
  if (res.status === 404) {
2903
- this.log("Primary campaign not found (404)");
2904
3216
  this.cache.setCampaign(key, null, this.cache.nullTTL);
2905
3217
  return null;
2906
3218
  }
2907
3219
  if (!res.ok) {
2908
- this.log("Non-OK response for primary campaign:", res.status);
2909
3220
  const stale = this.cache.getStaleCampaign(key);
2910
- if (stale !== void 0) return stale;
2911
- return null;
3221
+ return stale !== void 0 ? stale : null;
2912
3222
  }
2913
- this.log("Got primary campaign:", res.data?.campaignId);
2914
3223
  this.cache.setCampaign(key, res.data);
2915
3224
  return res.data;
2916
- } catch (error) {
2917
- this.log("Failed to get primary campaign:", error);
3225
+ } catch {
2918
3226
  const stale = this.cache.getStaleCampaign(key);
2919
- if (stale !== void 0) return stale;
2920
- return null;
3227
+ return stale !== void 0 ? stale : null;
2921
3228
  }
2922
3229
  }
2923
3230
  async validatePurchase(platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
2924
- const result = await validatePurchase(
2925
- this,
2926
- this.appKey,
2927
- platform,
2928
- receipt,
2929
- productId,
2930
- transactionId,
2931
- priceLocal,
2932
- currency,
2933
- country,
2934
- distinctId,
2935
- paywallPlacement,
2936
- variantKey
2937
- );
2938
- this.log("Purchase validated:", result.success);
2939
- return result;
3231
+ return validatePurchase(this, this.appKey, platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey);
2940
3232
  }
2941
3233
  async getSubscriptionStatus(distinctId) {
2942
- const result = await getSubscriptionStatus(this, this.baseUrl, this.appKey, distinctId);
2943
- this.log("Subscription status:", result.hasActiveSubscription);
2944
- return result;
3234
+ return getSubscriptionStatus(this, this.baseUrl, this.appKey, distinctId);
2945
3235
  }
2946
3236
  async evaluateFlags(keys, distinctId) {
2947
3237
  const query = keys.map(encodeURIComponent).join(",");
2948
- const path = `/api/v1/flags/evaluate?keys=${query}`;
2949
3238
  try {
2950
- const res = await this.httpClient.get(path, {
3239
+ const res = await this.httpClient.get(`/api/v1/flags/evaluate?keys=${query}`, {
2951
3240
  headers: { "X-App-Key": this.appKey, "x-distinct-id": distinctId }
2952
3241
  });
2953
- if (!res.ok) {
2954
- this.log("evaluateFlags failed:", res.status, keys);
2955
- return {};
2956
- }
2957
- const raw = res.data;
2958
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
2959
- const record = raw;
3242
+ if (!res.ok || typeof res.data !== "object" || res.data === null || Array.isArray(res.data)) return {};
3243
+ const record = res.data;
2960
3244
  const result = {};
2961
3245
  for (const key of Object.keys(record)) {
2962
3246
  const value = record[key];
@@ -2964,11 +3248,10 @@ var _ApiClient = class _ApiClient {
2964
3248
  result[key] = variant;
2965
3249
  const flagVariant = { variant };
2966
3250
  this.cache.setVariant(`${key}:${distinctId}`, flagVariant);
2967
- await this.writeFlagToStorage(key, distinctId, flagVariant);
3251
+ await writeFlagToStorage(key, distinctId, flagVariant);
2968
3252
  }
2969
3253
  return result;
2970
- } catch (error) {
2971
- this.log("Failed to evaluateFlags:", error);
3254
+ } catch {
2972
3255
  return {};
2973
3256
  }
2974
3257
  }
@@ -2976,15 +3259,9 @@ var _ApiClient = class _ApiClient {
2976
3259
  try {
2977
3260
  const url = buildConditionalFlagUrl(this.baseUrl, flagKey, context);
2978
3261
  const response = await this.get(url);
2979
- if (response.status === 404) return { value: false, flagKey };
2980
- if (!response.ok) {
2981
- this.log("Non-OK response for conditional flag:", flagKey, response.status);
2982
- return { value: false, flagKey };
2983
- }
2984
- this.log("Got conditional flag:", flagKey, response.data.value);
3262
+ if (response.status === 404 || !response.ok) return { value: false, flagKey };
2985
3263
  return { value: response.data.value, flagKey };
2986
3264
  } catch {
2987
- this.log("Failed to get conditional flag:", flagKey);
2988
3265
  return { value: false, flagKey };
2989
3266
  }
2990
3267
  }
@@ -2992,34 +3269,13 @@ var _ApiClient = class _ApiClient {
2992
3269
  const key = `${flagKey}:${distinctId}`;
2993
3270
  const hit = this.cache.getVariant(key);
2994
3271
  if (hit !== void 0) return hit;
2995
- const persisted = await this.readFlagFromStorage(flagKey, distinctId);
3272
+ const persisted = await readFlagFromStorage(flagKey, distinctId);
2996
3273
  if (persisted !== null) {
2997
3274
  this.cache.setVariant(key, persisted);
2998
3275
  return persisted;
2999
3276
  }
3000
3277
  return null;
3001
3278
  }
3002
- async readFlagFromStorage(flagKey, distinctId) {
3003
- try {
3004
- const raw = await secureStorage.get(`flag:${flagKey}:${distinctId}`);
3005
- if (!raw) return null;
3006
- const parsed = JSON.parse(raw);
3007
- if (typeof parsed !== "object" || parsed === null || !("variant" in parsed) || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") return null;
3008
- const { variant, payload, cachedAt } = parsed;
3009
- if (Date.now() - cachedAt > _ApiClient.FLAG_MAX_AGE_MS) return null;
3010
- return { variant, payload };
3011
- } catch {
3012
- return null;
3013
- }
3014
- }
3015
- async writeFlagToStorage(flagKey, distinctId, flag) {
3016
- try {
3017
- const value = JSON.stringify({ variant: flag.variant, payload: flag.payload, cachedAt: Date.now() });
3018
- await secureStorage.set(`flag:${flagKey}:${distinctId}`, value);
3019
- } catch (err) {
3020
- this.log("writeFlagToStorage failed", err);
3021
- }
3022
- }
3023
3279
  async postWithQueue(url, payload, label) {
3024
3280
  if (this.offlineQueueEnabled && !networkMonitor.isOnline()) {
3025
3281
  await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
@@ -3032,7 +3288,6 @@ var _ApiClient = class _ApiClient {
3032
3288
  this.log("Request failed:", label, error);
3033
3289
  if (this.offlineQueueEnabled) {
3034
3290
  await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
3035
- this.log("Queued after failure:", label);
3036
3291
  return;
3037
3292
  }
3038
3293
  throw error;
@@ -3042,8 +3297,6 @@ var _ApiClient = class _ApiClient {
3042
3297
  if (this.debug) console.log("[Paywallo:ApiClient]", ...args);
3043
3298
  }
3044
3299
  };
3045
- _ApiClient.FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
3046
- var ApiClient = _ApiClient;
3047
3300
 
3048
3301
  // src/domains/campaign/CampaignError.ts
3049
3302
  var CampaignError = class extends PaywalloError {
@@ -3068,6 +3321,7 @@ var CampaignGateService = class {
3068
3321
  constructor() {
3069
3322
  this.preloadedCampaigns = /* @__PURE__ */ new Map();
3070
3323
  this.pendingPreloads = /* @__PURE__ */ new Set();
3324
+ this.activePreloadPromises = /* @__PURE__ */ new Map();
3071
3325
  }
3072
3326
  async getCampaign(apiClient, placement, context) {
3073
3327
  const distinctId = identityManager.getDistinctId();
@@ -3107,12 +3361,23 @@ var CampaignGateService = class {
3107
3361
  }
3108
3362
  async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
3109
3363
  const isActive = await hasActive();
3110
- if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo] requireSubscription \u2014 hasActive: ${isActive}`);
3364
+ if (PaywalloClient.getConfig()?.debug) console.log(`[Paywallo] requireSubscription \u2014 hasActive: ${isActive}`);
3111
3365
  if (isActive) return true;
3112
3366
  const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, { ...context, forceShow: true });
3113
3367
  return r.purchased || r.restored;
3114
3368
  }
3115
3369
  async preloadCampaign(apiClient, placement, context) {
3370
+ const existing = this.activePreloadPromises.get(placement);
3371
+ if (existing) return existing;
3372
+ const promise = this._doPreload(apiClient, placement, context);
3373
+ this.activePreloadPromises.set(placement, promise);
3374
+ try {
3375
+ return await promise;
3376
+ } finally {
3377
+ this.activePreloadPromises.delete(placement);
3378
+ }
3379
+ }
3380
+ async _doPreload(apiClient, placement, context) {
3116
3381
  this.pendingPreloads.add(placement);
3117
3382
  try {
3118
3383
  const campaign = await this.getCampaign(apiClient, placement, context);
@@ -3121,7 +3386,7 @@ var CampaignGateService = class {
3121
3386
  }
3122
3387
  this.preloadedCampaigns.set(placement, { campaign, preloadedAt: Date.now() });
3123
3388
  if (PaywalloClient.getConfig()?.debug) {
3124
- console.info(`[Paywallo PRELOAD] data cached \u2014 placement: ${placement}`);
3389
+ console.log(`[Paywallo PRELOAD] data cached \u2014 placement: ${placement}`);
3125
3390
  }
3126
3391
  return { success: true };
3127
3392
  } catch (error) {
@@ -3147,7 +3412,7 @@ var CampaignGateService = class {
3147
3412
  var campaignGateService = new CampaignGateService();
3148
3413
 
3149
3414
  // src/domains/identity/AdvertisingIdManager.ts
3150
- import { Platform as Platform5 } from "react-native";
3415
+ import { Platform as Platform6 } from "react-native";
3151
3416
  var ZERO_IDFA = "00000000-0000-0000-0000-000000000000";
3152
3417
  var AdvertisingIdManager = class {
3153
3418
  constructor() {
@@ -3158,7 +3423,7 @@ var AdvertisingIdManager = class {
3158
3423
  const fallback = { idfv: null, idfa: null, gaid: null, attStatus: "unavailable" };
3159
3424
  try {
3160
3425
  const tracking = await import("expo-tracking-transparency");
3161
- if (Platform5.OS === "ios") {
3426
+ if (Platform6.OS === "ios") {
3162
3427
  if (!tracking.isAvailable()) {
3163
3428
  const idfv2 = await this.collectIdfv();
3164
3429
  this.cached = { ...fallback, idfv: idfv2 };
@@ -3177,10 +3442,8 @@ var AdvertisingIdManager = class {
3177
3442
  this.cached = { idfv, idfa, gaid: null, attStatus: status };
3178
3443
  return this.cached;
3179
3444
  }
3180
- if (Platform5.OS === "android") {
3181
- const androidStatus = await tracking.getTrackingPermissionsAsync();
3182
- const optedIn = androidStatus.granted;
3183
- const rawGaid = optedIn ? tracking.getAdvertisingId() : null;
3445
+ if (Platform6.OS === "android") {
3446
+ const rawGaid = tracking.getAdvertisingId();
3184
3447
  const gaid = this.sanitizeId(rawGaid);
3185
3448
  this.cached = { idfv: null, idfa: null, gaid, attStatus: "unavailable" };
3186
3449
  return this.cached;
@@ -3194,9 +3457,8 @@ var AdvertisingIdManager = class {
3194
3457
  }
3195
3458
  async collectIdfv() {
3196
3459
  try {
3197
- const app = await import("expo-application");
3198
- const idfv = await app.getIosIdForVendorAsync();
3199
- return this.sanitizeId(idfv);
3460
+ const info = await nativeDeviceInfo.getDeviceInfo();
3461
+ return this.sanitizeId(info.idfv);
3200
3462
  } catch {
3201
3463
  return null;
3202
3464
  }
@@ -3209,13 +3471,11 @@ var AdvertisingIdManager = class {
3209
3471
 
3210
3472
  // src/domains/identity/InstallTracker.ts
3211
3473
  import { Dimensions as Dimensions3, Platform as Platform8 } from "react-native";
3212
- import AsyncStorage3 from "@react-native-async-storage/async-storage";
3213
- import RNDeviceInfo from "react-native-device-info";
3214
3474
 
3215
3475
  // src/domains/identity/MetaBridge.ts
3216
- import { NativeModules as NativeModules4 } from "react-native";
3476
+ import { NativeModules as NativeModules6 } from "react-native";
3217
3477
  var TIMEOUT_MS = 2e3;
3218
- var NativeBridge = NativeModules4.PaywalloFBBridge ?? null;
3478
+ var NativeBridge = NativeModules6.PaywalloFBBridge ?? null;
3219
3479
  var MetaBridge = class {
3220
3480
  async getAnonymousID() {
3221
3481
  if (!NativeBridge) return null;
@@ -3263,11 +3523,11 @@ var MetaBridge = class {
3263
3523
  var metaBridge = new MetaBridge();
3264
3524
 
3265
3525
  // src/domains/identity/InstallReferrerManager.ts
3266
- import { Platform as Platform6 } from "react-native";
3526
+ import { Platform as Platform7 } from "react-native";
3267
3527
  var REFERRER_TIMEOUT_MS = 5e3;
3268
3528
  var InstallReferrerManager = class {
3269
3529
  async getReferrer() {
3270
- if (Platform6.OS !== "android") {
3530
+ if (Platform7.OS !== "android") {
3271
3531
  return this.emptyResult();
3272
3532
  }
3273
3533
  let timerId;
@@ -3281,8 +3541,7 @@ var InstallReferrerManager = class {
3281
3541
  }
3282
3542
  async fetchReferrer() {
3283
3543
  try {
3284
- const Application = await import("expo-application");
3285
- const referrer = await Application.getInstallReferrerAsync();
3544
+ const referrer = await nativeDeviceInfo.getInstallReferrer();
3286
3545
  if (!referrer) return this.emptyResult();
3287
3546
  const capped = referrer.slice(0, 2048);
3288
3547
  const params = this.parseReferrer(capped);
@@ -3331,47 +3590,12 @@ import { AppState as AppState2 } from "react-native";
3331
3590
  init_uuid();
3332
3591
 
3333
3592
  // src/utils/deviceInfo.ts
3334
- import { Platform as Platform7 } from "react-native";
3335
- async function getAppVersion() {
3336
- try {
3337
- const app = await import("expo-application");
3338
- if (app.nativeApplicationVersion) {
3339
- return app.nativeApplicationVersion;
3340
- }
3341
- } catch {
3342
- }
3343
- try {
3344
- const DeviceInfo2 = (await import("react-native-device-info")).default;
3345
- const v = DeviceInfo2.getVersion();
3346
- if (v) return v;
3347
- } catch {
3348
- }
3349
- return "unknown";
3350
- }
3351
- async function getDeviceModel() {
3352
- try {
3353
- const DeviceInfo2 = (await import("react-native-device-info")).default;
3354
- const model = DeviceInfo2.getModel();
3355
- if (model) return model;
3356
- } catch {
3357
- }
3358
- return "unknown";
3359
- }
3360
- function getOsVersion() {
3361
- const v = Platform7.Version;
3362
- if (typeof v === "number") return String(v);
3363
- if (typeof v === "string" && v.length > 0) return v;
3364
- return "unknown";
3365
- }
3366
3593
  async function collectDeviceInfo() {
3367
- const [appVersion, deviceModel] = await Promise.all([
3368
- getAppVersion(),
3369
- getDeviceModel()
3370
- ]);
3594
+ const result = await nativeDeviceInfo.getDeviceInfo();
3371
3595
  return {
3372
- appVersion,
3373
- deviceModel,
3374
- osVersion: getOsVersion()
3596
+ appVersion: result.appVersion,
3597
+ deviceModel: result.model,
3598
+ osVersion: result.systemVersion
3375
3599
  };
3376
3600
  }
3377
3601
 
@@ -3635,16 +3859,26 @@ var sessionManager = new SessionManager();
3635
3859
  // src/domains/identity/InstallTracker.ts
3636
3860
  var InstallTracker = class {
3637
3861
  constructor(debug = false, advertisingIdManager) {
3862
+ this._trackingInProgress = false;
3638
3863
  this.debug = debug;
3639
3864
  this.advertisingIdManager = advertisingIdManager ?? new AdvertisingIdManager();
3640
3865
  }
3641
3866
  async trackIfNeeded(apiClient, requestATT = false) {
3867
+ if (this._trackingInProgress) return;
3868
+ this._trackingInProgress = true;
3869
+ try {
3870
+ await this._doTrack(apiClient, requestATT);
3871
+ } finally {
3872
+ this._trackingInProgress = false;
3873
+ }
3874
+ }
3875
+ async _doTrack(apiClient, requestATT) {
3642
3876
  const storage = new SecureStorage(this.debug);
3643
3877
  const alreadyTracked = await storage.get("@panel:install_tracked");
3644
3878
  if (alreadyTracked) return;
3645
3879
  let fallbackTracked = null;
3646
3880
  try {
3647
- fallbackTracked = await AsyncStorage3.getItem("@paywallo:install_tracked");
3881
+ fallbackTracked = await nativeStorage.get("@paywallo:install_tracked");
3648
3882
  } catch {
3649
3883
  }
3650
3884
  if (fallbackTracked) return;
@@ -3652,7 +3886,6 @@ var InstallTracker = class {
3652
3886
  const sessionId = sessionManager.getSessionId();
3653
3887
  const installedAt = Date.now();
3654
3888
  if (!distinctId) return;
3655
- const deviceInfo = RNDeviceInfo;
3656
3889
  let deviceData = {};
3657
3890
  try {
3658
3891
  const screen = Dimensions3.get("screen");
@@ -3660,39 +3893,40 @@ var InstallTracker = class {
3660
3893
  const screenHeight = Math.round(screen.height ?? 0);
3661
3894
  const locale = Intl.DateTimeFormat().resolvedOptions().locale ?? "unknown";
3662
3895
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "unknown";
3663
- const deviceModel = deviceInfo?.getModel() || "unknown";
3664
- const osVersion = deviceInfo?.getSystemVersion() || "unknown";
3665
- const appVersion = deviceInfo?.getVersion() || "unknown";
3666
- const buildNumber = deviceInfo?.getBuildNumber() || "0";
3896
+ const info = await nativeDeviceInfo.getDeviceInfo();
3897
+ const deviceModel = info.modelId || info.model || "unknown";
3898
+ const osVersion = info.systemVersion || "unknown";
3899
+ const appVersion = info.appVersion || "unknown";
3900
+ const buildNumber = info.buildNumber || "0";
3667
3901
  let carrier = "unknown";
3668
3902
  try {
3669
- carrier = await deviceInfo?.getCarrier?.() ?? "unknown";
3903
+ carrier = info.carrier ?? "unknown";
3670
3904
  } catch {
3671
3905
  }
3672
3906
  let totalDisk = 0;
3673
3907
  try {
3674
- totalDisk = Number(await deviceInfo?.getTotalDiskCapacity?.()) || 0;
3908
+ totalDisk = Number(info.totalDisk) || 0;
3675
3909
  } catch {
3676
3910
  }
3677
3911
  let freeDisk = 0;
3678
3912
  try {
3679
- freeDisk = Number(await deviceInfo?.getFreeDiskStorage?.()) || 0;
3913
+ freeDisk = Number(info.freeDisk) || 0;
3680
3914
  } catch {
3681
3915
  }
3682
3916
  let totalRam = 0;
3683
3917
  try {
3684
- totalRam = Number(await deviceInfo?.getTotalMemory?.()) || 0;
3918
+ totalRam = Number(info.totalRam) || 0;
3685
3919
  } catch {
3686
3920
  }
3687
3921
  let brand = "unknown";
3688
3922
  try {
3689
- brand = deviceInfo?.getBrand?.() ?? "unknown";
3923
+ brand = info.brand ?? "unknown";
3690
3924
  } catch {
3691
3925
  }
3692
3926
  let installerPackage = null;
3693
3927
  try {
3694
3928
  if (Platform8.OS === "android") {
3695
- installerPackage = await deviceInfo?.getInstallerPackageName?.() ?? null;
3929
+ installerPackage = info.installerPackage ?? null;
3696
3930
  }
3697
3931
  } catch {
3698
3932
  }
@@ -3731,37 +3965,34 @@ var InstallTracker = class {
3731
3965
  });
3732
3966
  }
3733
3967
  }
3734
- try {
3735
- await apiClient.trackEvent("$app_installed", distinctId, {
3736
- installedAt,
3737
- platform: Platform8.OS,
3738
- ...sessionId && { sessionId },
3739
- ...deviceData,
3740
- ...effectiveAnonId && { fbAnonId: effectiveAnonId },
3741
- ...referrer.rawReferrer && { installReferrer: referrer.rawReferrer },
3742
- ...referrer.trackingId && { referrerTrackingId: referrer.trackingId },
3743
- ...referrer.fbclid && { referrerFbclid: referrer.fbclid },
3744
- ...referrer.utmSource && { referrerUtmSource: referrer.utmSource },
3745
- ...referrer.utmMedium && { referrerUtmMedium: referrer.utmMedium },
3746
- ...referrer.utmCampaign && { referrerUtmCampaign: referrer.utmCampaign },
3747
- ...adIds.idfa && { idfa: adIds.idfa },
3748
- ...adIds.idfv && { idfv: adIds.idfv },
3749
- ...adIds.gaid && { gaid: adIds.gaid },
3750
- attStatus: adIds.attStatus
3968
+ await apiClient.trackEvent("$app_installed", distinctId, {
3969
+ installedAt,
3970
+ platform: Platform8.OS,
3971
+ ...sessionId && { sessionId },
3972
+ ...deviceData,
3973
+ ...effectiveAnonId && { fbAnonId: effectiveAnonId },
3974
+ ...referrer.rawReferrer && { installReferrer: referrer.rawReferrer },
3975
+ ...referrer.trackingId && { referrerTrackingId: referrer.trackingId },
3976
+ ...referrer.fbclid && { referrerFbclid: referrer.fbclid },
3977
+ ...referrer.utmSource && { referrerUtmSource: referrer.utmSource },
3978
+ ...referrer.utmMedium && { referrerUtmMedium: referrer.utmMedium },
3979
+ ...referrer.utmCampaign && { referrerUtmCampaign: referrer.utmCampaign },
3980
+ ...adIds.idfa && { idfa: adIds.idfa },
3981
+ ...adIds.idfv && { idfv: adIds.idfv },
3982
+ ...adIds.gaid && { gaid: adIds.gaid },
3983
+ attStatus: adIds.attStatus
3984
+ });
3985
+ if (Platform8.OS === "ios") {
3986
+ void this.callDeferredMatch(apiClient, deviceData, adIds, effectiveAnonId, installedAt).catch(() => {
3751
3987
  });
3752
- if (Platform8.OS === "ios") {
3753
- void this.callDeferredMatch(apiClient, deviceData, adIds, effectiveAnonId).catch(() => {
3754
- });
3755
- }
3756
- await storage.set("@panel:install_tracked", String(installedAt));
3757
- try {
3758
- await AsyncStorage3.setItem("@paywallo:install_tracked", String(installedAt));
3759
- } catch {
3760
- }
3988
+ }
3989
+ await storage.set("@panel:install_tracked", String(installedAt));
3990
+ try {
3991
+ await nativeStorage.set("@paywallo:install_tracked", String(installedAt));
3761
3992
  } catch {
3762
3993
  }
3763
3994
  }
3764
- async callDeferredMatch(apiClient, deviceData, adIds, anonId) {
3995
+ async callDeferredMatch(apiClient, deviceData, adIds, anonId, installedAt) {
3765
3996
  const storage = new SecureStorage(this.debug);
3766
3997
  const alreadyMatched = await storage.get("@panel:deferred_match_done");
3767
3998
  if (alreadyMatched) return;
@@ -3769,7 +4000,7 @@ var InstallTracker = class {
3769
4000
  const timer = setTimeout(() => controller.abort(), 5e3);
3770
4001
  try {
3771
4002
  const baseUrl = apiClient.getBaseUrl();
3772
- const appKey = apiClient.appKey;
4003
+ const appKey = apiClient.getAppKey();
3773
4004
  await fetch(`${baseUrl}/api/v1/attribution/deferred-match`, {
3774
4005
  method: "POST",
3775
4006
  headers: { "Content-Type": "application/json", "X-App-Key": appKey },
@@ -3781,7 +4012,8 @@ var InstallTracker = class {
3781
4012
  screenHeight: deviceData.screenHeight,
3782
4013
  timezone: deviceData.timezone,
3783
4014
  locale: deviceData.locale,
3784
- fbAnonId: anonId
4015
+ fbAnonId: anonId,
4016
+ installTimestamp: new Date(installedAt).toISOString()
3785
4017
  }),
3786
4018
  signal: controller.signal
3787
4019
  });
@@ -3906,7 +4138,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3906
4138
  error: error instanceof Error ? error.message : String(error)
3907
4139
  });
3908
4140
  } catch (err) {
3909
- if (config.debug) console.warn("[Paywallo] reportInitFailure error", err);
4141
+ if (config.debug) console.error("[Paywallo] reportInitFailure error", err);
3910
4142
  }
3911
4143
  }
3912
4144
  reportInitSuccess(config, attempts) {
@@ -3915,7 +4147,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3915
4147
  void this.apiClient.reportError("INIT_RECOVERED", `Succeeded after ${attempts} retries`, { attempts });
3916
4148
  }
3917
4149
  } catch (err) {
3918
- if (config.debug) console.warn("[Paywallo] reportInitSuccess error", err);
4150
+ if (config.debug) console.error("[Paywallo] reportInitSuccess error", err);
3919
4151
  }
3920
4152
  }
3921
4153
  async resolveSessionFlags(keys, apiClient, distinctId) {
@@ -3962,24 +4194,26 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3962
4194
  if (config.sessionFlags && config.sessionFlags.length > 0) {
3963
4195
  await this.resolveSessionFlags(config.sessionFlags, this.apiClient, distinctId);
3964
4196
  }
4197
+ setNativeStorageDebug(config.debug ?? false);
4198
+ setNativeDeviceInfoDebug(config.debug ?? false);
3965
4199
  this.config = config;
3966
4200
  if (this.resolveReady) {
3967
4201
  this.resolveReady();
3968
4202
  this.resolveReady = null;
3969
4203
  }
3970
- if (config.debug) console.info("[Paywallo INIT] complete");
4204
+ if (config.debug) console.log("[Paywallo INIT] complete");
3971
4205
  if (distinctId) {
3972
4206
  this.apiClient.getSubscriptionStatus(distinctId).then(() => {
3973
- if (config.debug) console.info("[Paywallo SUB] preloaded on boot");
4207
+ if (config.debug) console.log("[Paywallo SUB] preloaded on boot");
3974
4208
  }).catch(() => {
3975
4209
  });
3976
4210
  }
3977
- if (this.apiClient) {
4211
+ {
3978
4212
  const apiClientRef = this.apiClient;
3979
4213
  if (config.autoPreloadCampaign) {
3980
4214
  this.autoPreloadedPlacement = config.autoPreloadCampaign;
3981
4215
  this.autoPreloadPlacementPromise = Promise.resolve(config.autoPreloadCampaign);
3982
- if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${config.autoPreloadCampaign}`);
4216
+ if (config.debug) console.log(`[Paywallo PRELOAD] started \u2014 placement: ${config.autoPreloadCampaign}`);
3983
4217
  void campaignGateService.preloadCampaign(apiClientRef, config.autoPreloadCampaign).catch(() => {
3984
4218
  });
3985
4219
  } else {
@@ -3987,7 +4221,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3987
4221
  const placement = primary?.placement ?? primary?.paywall?.placement;
3988
4222
  if (placement) {
3989
4223
  this.autoPreloadedPlacement = placement;
3990
- if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${placement}`);
4224
+ if (config.debug) console.log(`[Paywallo PRELOAD] started \u2014 placement: ${placement}`);
3991
4225
  void campaignGateService.preloadCampaign(apiClientRef, placement).catch(() => {
3992
4226
  });
3993
4227
  return placement;
@@ -3999,21 +4233,21 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3999
4233
  }
4000
4234
  async identify(options) {
4001
4235
  this.ensureInitialized();
4002
- if (this.config?.debug) console.info("[Paywallo IDENTIFY]", options);
4236
+ if (this.config?.debug) console.log("[Paywallo IDENTIFY]", options ? { hasEmail: "email" in options && !!options.email, hasProperties: "properties" in options } : void 0);
4003
4237
  await identityManager.identify(options);
4004
4238
  }
4005
4239
  async track(eventName, options) {
4006
4240
  const apiClient = this.getApiClientOrThrow();
4007
4241
  const distinctId = identityManager.getDistinctId();
4008
4242
  if (!distinctId) {
4009
- if (this.config?.debug) console.info(`[Paywallo TRACK] ${eventName} skipped (no distinctId)`);
4243
+ if (this.config?.debug) console.log(`[Paywallo TRACK] ${eventName} skipped (no distinctId)`);
4010
4244
  return;
4011
4245
  }
4012
4246
  if (!isValidEventName(eventName)) {
4013
4247
  const msg = `Invalid event name: "${eventName}". Use snake_case or $reserved_name.`;
4014
4248
  throw new ClientError(CLIENT_ERROR_CODES.INVALID_EVENT_NAME, msg);
4015
4249
  }
4016
- if (this.config?.debug && eventName.startsWith("$purchase")) console.info(`[Paywallo TRACK] ${eventName}`, options?.properties ?? {});
4250
+ if (this.config?.debug && eventName.startsWith("$purchase")) console.log(`[Paywallo TRACK] ${eventName}`, options?.properties ?? {});
4017
4251
  const sessionId = sessionManager.getSessionId();
4018
4252
  await apiClient.trackEvent(eventName, distinctId, {
4019
4253
  ...identityManager.getProperties(),
@@ -4072,14 +4306,14 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4072
4306
  try {
4073
4307
  const distinctId = identityManager.getDistinctId();
4074
4308
  if (!distinctId) {
4075
- if (this.config?.debug) console.info(`[Paywallo FLAG] ${flagKey} \u2192 null (no distinctId)`);
4309
+ if (this.config?.debug) console.log(`[Paywallo FLAG] ${flagKey} \u2192 null (no distinctId)`);
4076
4310
  return { variant: null };
4077
4311
  }
4078
4312
  const result = await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
4079
- if (this.config?.debug) console.info(`[Paywallo FLAG] ${flagKey} \u2192`, result.variant);
4313
+ if (this.config?.debug) console.log(`[Paywallo FLAG] ${flagKey} \u2192`, result.variant);
4080
4314
  return result;
4081
4315
  } catch (error) {
4082
- if (this.config?.debug) console.warn(`[Paywallo FLAG] ${flagKey} \u2192 error`, error);
4316
+ if (this.config?.debug) console.log(`[Paywallo FLAG] ${flagKey} \u2192 error`, error);
4083
4317
  if (this.apiClient) void this.apiClient.reportError("GET_VARIANT_FAILED", error instanceof Error ? error.message : String(error));
4084
4318
  return { variant: null };
4085
4319
  }
@@ -4112,11 +4346,11 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4112
4346
  }
4113
4347
  async presentCampaign(placement, context) {
4114
4348
  this.ensureInitialized();
4115
- if (this.config?.debug) console.info(`[Paywallo CAMPAIGN] presenting ${placement}`);
4349
+ if (this.config?.debug) console.log(`[Paywallo CAMPAIGN] presenting ${placement}`);
4116
4350
  const result = await campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, () => this.hasActiveSubscription(), this.paywallPresenter, this.campaignPresenter, context);
4117
4351
  if (this.config?.debug) {
4118
- if (result.error) console.warn(`[Paywallo CAMPAIGN] ${placement} error:`, result.error.message);
4119
- else console.info(`[Paywallo CAMPAIGN] ${placement} result:`, { purchased: result.purchased, cancelled: result.cancelled, restored: result.restored, skippedReason: result.skippedReason });
4352
+ if (result.error) console.log(`[Paywallo CAMPAIGN] ${placement} error:`, result.error.message);
4353
+ else console.log(`[Paywallo CAMPAIGN] ${placement} result:`, { purchased: result.purchased, cancelled: result.cancelled, restored: result.restored, skippedReason: result.skippedReason });
4120
4354
  }
4121
4355
  return result;
4122
4356
  }
@@ -4126,20 +4360,20 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4126
4360
  if (this.activeChecker) {
4127
4361
  const result = await this.activeChecker();
4128
4362
  const isActive2 = result === true;
4129
- if (this.config?.debug) console.info("[Paywallo SUB] hasActiveSubscription (checker):", isActive2);
4363
+ if (this.config?.debug) console.log("[Paywallo SUB] hasActiveSubscription (checker):", isActive2);
4130
4364
  return isActive2;
4131
4365
  }
4132
4366
  const distinctId = identityManager.getDistinctId();
4133
4367
  if (!distinctId || !this.apiClient) {
4134
- if (this.config?.debug) console.info("[Paywallo SUB] hasActiveSubscription: false (no distinctId/apiClient)");
4368
+ if (this.config?.debug) console.log("[Paywallo SUB] hasActiveSubscription: false (no distinctId/apiClient)");
4135
4369
  return false;
4136
4370
  }
4137
4371
  const status = await this.apiClient.getSubscriptionStatus(distinctId);
4138
4372
  const isActive = status.hasActiveSubscription === true;
4139
- if (this.config?.debug) console.info("[Paywallo SUB] hasActiveSubscription (server):", isActive);
4373
+ if (this.config?.debug) console.log("[Paywallo SUB] hasActiveSubscription (server):", isActive);
4140
4374
  return isActive;
4141
4375
  } catch (error) {
4142
- if (this.config?.debug) console.warn("[Paywallo SUB] hasActiveSubscription error", error);
4376
+ if (this.config?.debug) console.error("[Paywallo SUB] hasActiveSubscription error", error);
4143
4377
  if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", error instanceof Error ? error.message : "hasActiveSubscription failed");
4144
4378
  return false;
4145
4379
  }
@@ -4160,9 +4394,9 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4160
4394
  async restorePurchases() {
4161
4395
  this.ensureInitialized();
4162
4396
  if (!this.restoreHandler) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
4163
- if (this.config?.debug) console.info("[Paywallo RESTORE] starting");
4397
+ if (this.config?.debug) console.log("[Paywallo RESTORE] starting");
4164
4398
  const result = await this.restoreHandler();
4165
- if (this.config?.debug) console.info("[Paywallo RESTORE] result:", { success: result.success, restoredProducts: result.restoredProducts?.length ?? 0 });
4399
+ if (this.config?.debug) console.log("[Paywallo RESTORE] result:", { success: result.success, restoredProducts: result.restoredProducts?.length ?? 0 });
4166
4400
  return result;
4167
4401
  }
4168
4402
  async reset() {
@@ -4172,6 +4406,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4172
4406
  async fullReset() {
4173
4407
  const wasDebug = this.config?.debug ?? false;
4174
4408
  await sessionManager.endSession();
4409
+ sessionManager.destroy();
4175
4410
  await identityManager.reset();
4176
4411
  await subscriptionCache.invalidateAll();
4177
4412
  queueProcessor.dispose();
@@ -4180,6 +4415,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4180
4415
  campaignGateService.clearPreloaded();
4181
4416
  this.cleanupNetworkRecovery();
4182
4417
  this.sessionFlagsMap.clear();
4418
+ this.apiClient?.dispose();
4183
4419
  this.apiClient = null;
4184
4420
  this.config = null;
4185
4421
  this.initPromise = null;
@@ -4516,6 +4752,7 @@ function usePurchase() {
4516
4752
 
4517
4753
  // src/hooks/useSubscription.ts
4518
4754
  import { useCallback as useCallback8, useEffect as useEffect6, useState as useState8 } from "react";
4755
+ var EMPTY_ENTITLEMENTS = [];
4519
4756
  function useSubscription() {
4520
4757
  const [status, setStatus] = useState8(null);
4521
4758
  const [isLoading, setIsLoading] = useState8(true);
@@ -4551,7 +4788,8 @@ function useSubscription() {
4551
4788
  setStatus(s);
4552
4789
  setIsLoading(false);
4553
4790
  }).catch((err) => {
4554
- setError(err instanceof Error ? err : new Error(String(err)));
4791
+ const e = err instanceof PurchaseError ? err : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, String(err));
4792
+ setError(e);
4555
4793
  setIsLoading(false);
4556
4794
  });
4557
4795
  const removeListener = subscriptionManager.addListener((newStatus) => {
@@ -4566,7 +4804,7 @@ function useSubscription() {
4566
4804
  isActive: status?.hasActiveSubscription ?? false,
4567
4805
  isLoading,
4568
4806
  error,
4569
- entitlements: status?.entitlements ?? [],
4807
+ entitlements: status?.entitlements ?? EMPTY_ENTITLEMENTS,
4570
4808
  refresh,
4571
4809
  restore
4572
4810
  };
@@ -4698,7 +4936,7 @@ function useCampaignPreload() {
4698
4936
  campaignId: campaign.campaignId,
4699
4937
  variantKey: campaign.variantKey
4700
4938
  });
4701
- if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo WEBVIEW] mounting hidden \u2014 placement: ${placement}`);
4939
+ if (PaywalloClient.getConfig()?.debug) console.log(`[Paywallo WEBVIEW] mounting hidden \u2014 placement: ${placement}`);
4702
4940
  return { success: true };
4703
4941
  } catch (error) {
4704
4942
  return {
@@ -4733,7 +4971,7 @@ function useProductLoader() {
4733
4971
  for (const p of loaded) map.set(p.productId, p);
4734
4972
  setProducts(map);
4735
4973
  } catch (err) {
4736
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] refreshProducts failed", err);
4974
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] refreshProducts failed", err);
4737
4975
  } finally {
4738
4976
  setIsLoadingProducts(false);
4739
4977
  }
@@ -4773,7 +5011,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4773
5011
  clearPreloadedWebView();
4774
5012
  resolvePreloadedPaywall({ presented: true, purchased: true, cancelled: false, restored: false });
4775
5013
  } catch (error) {
4776
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] preloaded purchase failed", error);
5014
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] preloaded purchase failed", error);
4777
5015
  setShowingPreloadedWebView(false);
4778
5016
  clearPreloadedWebView();
4779
5017
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
@@ -4794,7 +5032,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4794
5032
  clearPreloadedWebView();
4795
5033
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: true });
4796
5034
  } catch (error) {
4797
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] preloaded restore failed", error);
5035
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] preloaded restore failed", error);
4798
5036
  setShowingPreloadedWebView(false);
4799
5037
  clearPreloadedWebView();
4800
5038
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
@@ -4807,11 +5045,28 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4807
5045
  import { useCallback as useCallback12, useRef as useRef7, useState as useState11 } from "react";
4808
5046
  var CACHE_TTL_MS2 = 5 * 60 * 1e3;
4809
5047
  var ACTIVE_STATUSES = ["active", "in_grace_period"];
5048
+ var PUBLIC_TO_DOMAIN_STATUS = {
5049
+ active: "active",
5050
+ expired: "expired",
5051
+ in_billing_retry: "billing_retry",
5052
+ in_grace_period: "grace_period",
5053
+ revoked: "unknown",
5054
+ cancelled: "canceled"
5055
+ };
5056
+ var DOMAIN_TO_PUBLIC_STATUS = {
5057
+ active: "active",
5058
+ expired: "expired",
5059
+ billing_retry: "in_billing_retry",
5060
+ grace_period: "in_grace_period",
5061
+ canceled: "cancelled",
5062
+ paused: "expired",
5063
+ unknown: "expired"
5064
+ };
4810
5065
  function toDomainSubscriptionInfo(sub) {
4811
5066
  return {
4812
5067
  id: "",
4813
5068
  productId: sub.productId,
4814
- status: sub.status,
5069
+ status: PUBLIC_TO_DOMAIN_STATUS[sub.status] ?? "unknown",
4815
5070
  expiresAt: sub.expiresAt ?? /* @__PURE__ */ new Date(0),
4816
5071
  originalPurchaseDate: /* @__PURE__ */ new Date(0),
4817
5072
  latestPurchaseDate: /* @__PURE__ */ new Date(0),
@@ -4834,6 +5089,10 @@ function isSubscriptionStillActive(sub) {
4834
5089
  if (sub.expiresAt && sub.expiresAt.getTime() !== 0 && sub.expiresAt < /* @__PURE__ */ new Date()) return false;
4835
5090
  return true;
4836
5091
  }
5092
+ function subscriptionInfoToSub(s) {
5093
+ const status = DOMAIN_TO_PUBLIC_STATUS[s.status] ?? "expired";
5094
+ return { productId: s.productId, status, expiresAt: s.expiresAt ?? null, platform: s.platform, autoRenewEnabled: s.willRenew, inGracePeriod: status === "in_grace_period" };
5095
+ }
4837
5096
  async function resolveOfflineFromCache(distinctId) {
4838
5097
  try {
4839
5098
  const cached = await subscriptionCache.get(distinctId);
@@ -4874,24 +5133,12 @@ function useSubscriptionSync() {
4874
5133
  const distinctId = PaywalloClient.getDistinctId();
4875
5134
  if (!apiClient || !distinctId) return false;
4876
5135
  const cached = cacheRef.current;
4877
- if (cached && cached.expiresAt > Date.now()) {
4878
- return isActiveCacheEntry(cached);
4879
- }
5136
+ if (cached && cached.expiresAt > Date.now()) return isActiveCacheEntry(cached);
4880
5137
  try {
4881
5138
  const persisted = await subscriptionCache.get(distinctId);
4882
5139
  if (persisted && !persisted.isStale) {
4883
- const persistedSub = persisted.data.subscription;
4884
- const persistedStatus = persistedSub?.status;
4885
- const sub = persistedSub ? {
4886
- productId: persistedSub.productId,
4887
- status: persistedStatus,
4888
- expiresAt: persistedSub.expiresAt ?? null,
4889
- platform: persistedSub.platform,
4890
- autoRenewEnabled: persistedSub.willRenew,
4891
- inGracePeriod: persistedStatus === "in_grace_period"
4892
- } : null;
4893
- const stillActive = isSubscriptionStillActive(sub);
4894
- if (stillActive) {
5140
+ const sub = persisted.data.subscription ? subscriptionInfoToSub(persisted.data.subscription) : null;
5141
+ if (isSubscriptionStillActive(sub)) {
4895
5142
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4896
5143
  setSubscription(sub);
4897
5144
  return true;
@@ -4900,17 +5147,14 @@ function useSubscriptionSync() {
4900
5147
  } catch {
4901
5148
  }
4902
5149
  try {
4903
- const status = await apiClient.getSubscriptionStatus(distinctId);
4904
- const sub = status.subscription ? {
4905
- ...status.subscription,
4906
- expiresAt: status.subscription.expiresAt ? new Date(status.subscription.expiresAt) : null
4907
- } : null;
5150
+ const apiStatus = await apiClient.getSubscriptionStatus(distinctId);
5151
+ const sub = apiStatus.subscription ? { ...apiStatus.subscription, expiresAt: apiStatus.subscription.expiresAt ? new Date(apiStatus.subscription.expiresAt) : null } : null;
4908
5152
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4909
5153
  setSubscription(sub);
4910
- void subscriptionCache.set(distinctId, toDomainResponse(status));
4911
- return status.hasActiveSubscription === true;
5154
+ void subscriptionCache.set(distinctId, toDomainResponse(apiStatus));
5155
+ return apiStatus.hasActiveSubscription === true;
4912
5156
  } catch {
4913
- return await resolveOfflineFromCache(distinctId);
5157
+ return resolveOfflineFromCache(distinctId);
4914
5158
  }
4915
5159
  }, []);
4916
5160
  const getSubscription = useCallback12(async () => {
@@ -4925,16 +5169,7 @@ function useSubscriptionSync() {
4925
5169
  try {
4926
5170
  const persisted = await subscriptionCache.get(distinctId);
4927
5171
  if (!persisted?.data.subscription) return null;
4928
- const s = persisted.data.subscription;
4929
- const status = s.status;
4930
- return {
4931
- productId: s.productId,
4932
- status,
4933
- expiresAt: s.expiresAt ?? null,
4934
- platform: s.platform,
4935
- autoRenewEnabled: s.willRenew,
4936
- inGracePeriod: status === "in_grace_period"
4937
- };
5172
+ return subscriptionInfoToSub(persisted.data.subscription);
4938
5173
  } catch {
4939
5174
  return null;
4940
5175
  }
@@ -4956,6 +5191,8 @@ function PaywalloProvider({ children, config }) {
4956
5191
  const clearPreloadedWebView = useCallback13(() => setPreloadedWebView(null), [setPreloadedWebView]);
4957
5192
  const autoPreloadTriggeredRef = useRef8(false);
4958
5193
  const preloadedWebViewRef = useRef8(preloadedWebView);
5194
+ const pollTimerRef = useRef8(null);
5195
+ const pollCancelledRef = useRef8(false);
4959
5196
  useEffect7(() => {
4960
5197
  preloadedWebViewRef.current = preloadedWebView;
4961
5198
  }, [preloadedWebView]);
@@ -4964,21 +5201,21 @@ function PaywalloProvider({ children, config }) {
4964
5201
  });
4965
5202
  }, []);
4966
5203
  const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady: baseHandlePreloadedWebViewReady, handlePreloadedClose: baseHandlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, setPreloadedWebView);
4967
- const SCREEN_HEIGHT3 = useMemo2(() => Dimensions4.get("window").height, []);
4968
- const slideAnim = useRef8(new Animated3.Value(SCREEN_HEIGHT3)).current;
5204
+ const SCREEN_HEIGHT = useMemo2(() => Dimensions4.get("window").height, []);
5205
+ const slideAnim = useRef8(new Animated3.Value(SCREEN_HEIGHT)).current;
4969
5206
  const handlePreloadedWebViewReady = useCallback13(() => {
4970
5207
  if (PaywalloClient.getConfig()?.debug) {
4971
- console.info(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
5208
+ console.log(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
4972
5209
  }
4973
5210
  baseHandlePreloadedWebViewReady();
4974
- }, [config?.debug, preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
5211
+ }, [preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
4975
5212
  const handlePreloadedClose = useCallback13(() => {
4976
5213
  const placement = preloadedWebView?.placement;
4977
- Animated3.timing(slideAnim, { toValue: SCREEN_HEIGHT3, duration: 200, easing: Easing2.in(Easing2.cubic), useNativeDriver: true }).start(() => {
5214
+ Animated3.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 200, easing: Easing2.in(Easing2.cubic), useNativeDriver: true }).start(() => {
4978
5215
  baseHandlePreloadedClose();
4979
5216
  if (placement) triggerRepreload(placement);
4980
5217
  });
4981
- }, [baseHandlePreloadedClose, preloadedWebView, triggerRepreload, slideAnim, SCREEN_HEIGHT3]);
5218
+ }, [baseHandlePreloadedClose, preloadedWebView, triggerRepreload, slideAnim, SCREEN_HEIGHT]);
4982
5219
  const { handlePreloadedPurchase: rawPreloadedPurchase, handlePreloadedRestore } = usePurchaseOrchestrator(
4983
5220
  preloadedWebView,
4984
5221
  resolvePreloadedPaywall,
@@ -4996,21 +5233,28 @@ function PaywalloProvider({ children, config }) {
4996
5233
  setIsPreloadPurchasing(false);
4997
5234
  }
4998
5235
  }, [rawPreloadedPurchase]);
5236
+ const configRef = useRef8(config);
4999
5237
  useEffect7(() => {
5238
+ let mounted = true;
5000
5239
  initLocalization({ detectDevice: true });
5001
- PaywalloClient.init(config).then(async () => {
5240
+ PaywalloClient.init(configRef.current).then(async () => {
5241
+ if (!mounted) return;
5002
5242
  setDistinctId(PaywalloClient.getDistinctId());
5003
5243
  setIsInitialized(true);
5004
5244
  if (!autoPreloadTriggeredRef.current) {
5005
5245
  autoPreloadTriggeredRef.current = true;
5006
5246
  const placement = await PaywalloClient.waitForAutoPreloadedPlacement();
5007
- if (placement) void preloadCampaign(placement).catch(() => {
5247
+ if (mounted && placement) void preloadCampaign(placement).catch(() => {
5008
5248
  });
5009
5249
  }
5010
5250
  }).catch((err) => {
5251
+ if (!mounted) return;
5011
5252
  setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
5012
5253
  });
5013
- }, [config, preloadCampaign]);
5254
+ return () => {
5255
+ mounted = false;
5256
+ };
5257
+ }, [preloadCampaign]);
5014
5258
  const getPaywallConfig = useCallback13(async (p) => {
5015
5259
  const api = PaywalloClient.getApiClient();
5016
5260
  return api ? api.getPaywall(p) : null;
@@ -5111,15 +5355,17 @@ function PaywalloProvider({ children, config }) {
5111
5355
  }, [activePaywall]);
5112
5356
  const bridgePaywallPresenter = useCallback13((placement) => {
5113
5357
  if (PaywalloClient.getConfig()?.debug) {
5114
- console.info(`[Paywallo PRESENT] called \u2014 preloaded: ${preloadedWebViewRef.current?.loaded === true} placement: ${placement}`);
5358
+ console.log(`[Paywallo PRESENT] called \u2014 preloaded: ${preloadedWebViewRef.current?.loaded === true} placement: ${placement}`);
5115
5359
  }
5116
5360
  if (preloadedWebViewRef.current?.placement === placement) {
5117
5361
  if (preloadedWebViewRef.current.loaded) {
5118
5362
  return presentPreloadedWebView();
5119
5363
  }
5364
+ pollCancelledRef.current = false;
5120
5365
  return new Promise((resolve) => {
5121
5366
  let elapsed = 0;
5122
5367
  const poll = () => {
5368
+ if (pollCancelledRef.current) return;
5123
5369
  if (preloadedWebViewRef.current?.loaded) {
5124
5370
  resolve(presentPreloadedWebView());
5125
5371
  return;
@@ -5129,9 +5375,9 @@ function PaywalloProvider({ children, config }) {
5129
5375
  resolve(presentCampaign(placement));
5130
5376
  return;
5131
5377
  }
5132
- setTimeout(poll, 100);
5378
+ pollTimerRef.current = setTimeout(poll, 100);
5133
5379
  };
5134
- setTimeout(poll, 100);
5380
+ pollTimerRef.current = setTimeout(poll, 100);
5135
5381
  });
5136
5382
  }
5137
5383
  return presentCampaign(placement);
@@ -5149,6 +5395,18 @@ function PaywalloProvider({ children, config }) {
5149
5395
  PaywalloClient.registerActiveChecker(hasActiveSubscription);
5150
5396
  PaywalloClient.registerRestoreHandler(restorePurchases);
5151
5397
  PaywalloClient.registerEmergencyPaywallHandler(onEmergency);
5398
+ return () => {
5399
+ pollCancelledRef.current = true;
5400
+ if (pollTimerRef.current !== null) {
5401
+ clearTimeout(pollTimerRef.current);
5402
+ pollTimerRef.current = null;
5403
+ }
5404
+ PaywalloClient.registerPaywallPresenter(null);
5405
+ PaywalloClient.registerSubscriptionGetter(null);
5406
+ PaywalloClient.registerActiveChecker(null);
5407
+ PaywalloClient.registerRestoreHandler(null);
5408
+ PaywalloClient.registerEmergencyPaywallHandler(null);
5409
+ };
5152
5410
  }, [isInitialized, bridgePaywallPresenter, getSubscription, hasActiveSubscription, restorePurchases]);
5153
5411
  const isReady = isInitialized && !isLoadingProducts;
5154
5412
  const noOp = useCallback13(() => {
@@ -5189,10 +5447,10 @@ function PaywalloProvider({ children, config }) {
5189
5447
  ]);
5190
5448
  useLayoutEffect(() => {
5191
5449
  if (showingPreloadedWebView) {
5192
- slideAnim.setValue(SCREEN_HEIGHT3);
5450
+ slideAnim.setValue(SCREEN_HEIGHT);
5193
5451
  Animated3.timing(slideAnim, { toValue: 0, duration: 200, easing: Easing2.out(Easing2.cubic), useNativeDriver: true }).start();
5194
5452
  }
5195
- }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT3]);
5453
+ }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT]);
5196
5454
  const preloadStyle = [
5197
5455
  styles3.preloadOverlay,
5198
5456
  showingPreloadedWebView ? styles3.visible : styles3.hidden,