@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.js CHANGED
@@ -144,19 +144,20 @@ var CLIENT_ERROR_CODES = {
144
144
  MISSING_APP_KEY: "CLIENT_MISSING_APP_KEY",
145
145
  INVALID_EVENT_NAME: "CLIENT_INVALID_EVENT_NAME",
146
146
  PROVIDER_MISSING: "CLIENT_PROVIDER_MISSING",
147
+ INSECURE_REQUEST: "CLIENT_INSECURE_REQUEST",
147
148
  UNKNOWN: "CLIENT_UNKNOWN"
148
149
  };
149
150
 
150
151
  // src/domains/paywall/PaywallModal.tsx
151
152
  var import_react5 = require("react");
152
- var import_react_native6 = require("react-native");
153
+ var import_react_native8 = require("react-native");
153
154
 
154
155
  // src/domains/paywall/usePaywallActions.ts
155
156
  var import_react2 = require("react");
156
- var import_react_native3 = require("react-native");
157
+ var import_react_native5 = require("react-native");
157
158
 
158
159
  // src/domains/iap/IAPService.ts
159
- var import_react_native2 = require("react-native");
160
+ var import_react_native4 = require("react-native");
160
161
 
161
162
  // src/domains/iap/PurchaseError.ts
162
163
  var PurchaseError = class extends PaywalloError {
@@ -201,10 +202,7 @@ function createPurchaseError(code, message) {
201
202
  }
202
203
 
203
204
  // src/domains/iap/NativeStoreKit.ts
204
- var import_react_native = require("react-native");
205
-
206
- // src/domains/identity/IdentityManager.ts
207
- var import_react_native_device_info = __toESM(require("react-native-device-info"));
205
+ var import_react_native3 = require("react-native");
208
206
 
209
207
  // src/domains/identity/IdentityError.ts
210
208
  var IdentityError = class extends PaywalloError {
@@ -224,13 +222,141 @@ var IDENTITY_ERROR_CODES = {
224
222
  // src/domains/identity/IdentityManager.ts
225
223
  init_uuid();
226
224
 
225
+ // src/utils/NativeDeviceInfo.ts
226
+ var import_react_native = require("react-native");
227
+ var FALLBACK = {
228
+ deviceId: "unknown",
229
+ model: "unknown",
230
+ modelId: "unknown",
231
+ systemVersion: "0.0.0",
232
+ appVersion: "0.0.0",
233
+ buildNumber: "0",
234
+ bundleId: "unknown",
235
+ brand: "unknown",
236
+ systemName: "unknown",
237
+ totalDisk: 0,
238
+ freeDisk: 0,
239
+ totalRam: 0,
240
+ carrier: "unknown",
241
+ installerPackage: null,
242
+ idfv: null
243
+ };
244
+ var _debug = false;
245
+ var warnedOnce = false;
246
+ var cachedDeviceInfo = null;
247
+ function setNativeDeviceInfoDebug(debug) {
248
+ _debug = debug;
249
+ }
250
+ function isDeviceInfoResult(value) {
251
+ if (typeof value !== "object" || value === null) return false;
252
+ const v = value;
253
+ return typeof v["deviceId"] === "string" && typeof v["model"] === "string" && typeof v["systemVersion"] === "string" && typeof v["appVersion"] === "string" && typeof v["bundleId"] === "string";
254
+ }
255
+ function getNativeModule() {
256
+ const mod = import_react_native.NativeModules.PaywalloDevice;
257
+ if (!mod) {
258
+ if (!warnedOnce) {
259
+ warnedOnce = true;
260
+ console.warn("[Paywallo] NativeModules.PaywalloDevice not found \u2014 falling back to defaults");
261
+ }
262
+ return null;
263
+ }
264
+ return mod;
265
+ }
266
+ async function getDeviceInfo() {
267
+ if (cachedDeviceInfo !== null) return cachedDeviceInfo;
268
+ const mod = getNativeModule();
269
+ if (!mod) return FALLBACK;
270
+ const raw = await mod.getDeviceInfo();
271
+ if (!isDeviceInfoResult(raw)) return FALLBACK;
272
+ cachedDeviceInfo = raw;
273
+ return raw;
274
+ }
275
+ async function getInstallReferrer() {
276
+ const mod = getNativeModule();
277
+ if (!mod) return null;
278
+ try {
279
+ return await mod.getInstallReferrer();
280
+ } catch {
281
+ return null;
282
+ }
283
+ }
284
+ function isAvailable() {
285
+ return import_react_native.NativeModules.PaywalloDevice != null;
286
+ }
287
+ function clearCache() {
288
+ cachedDeviceInfo = null;
289
+ }
290
+ var nativeDeviceInfo = {
291
+ getDeviceInfo,
292
+ getInstallReferrer,
293
+ isAvailable,
294
+ clearCache
295
+ };
296
+
297
+ // src/core/storage/NativeStorage.ts
298
+ var import_react_native2 = require("react-native");
299
+ var NativeModule = import_react_native2.NativeModules.PaywalloStorage;
300
+ var _debug2 = false;
301
+ var _warnedUnavailable = false;
302
+ function setNativeStorageDebug(debug) {
303
+ _debug2 = debug;
304
+ }
305
+ function isAvailable2() {
306
+ return NativeModule != null;
307
+ }
308
+ function warnIfUnavailable() {
309
+ if (NativeModule != null) return true;
310
+ if (!_warnedUnavailable) {
311
+ _warnedUnavailable = true;
312
+ if (_debug2) console.warn("[Paywallo] PaywalloStorage native module not available \u2014 storage will not persist");
313
+ }
314
+ return false;
315
+ }
316
+ function getModule() {
317
+ if (!warnIfUnavailable()) return null;
318
+ return NativeModule;
319
+ }
320
+ async function get(key) {
321
+ return getModule()?.get(key) ?? null;
322
+ }
323
+ async function set(key, value) {
324
+ return getModule()?.set(key, value) ?? false;
325
+ }
326
+ async function remove(key) {
327
+ return getModule()?.remove(key) ?? false;
328
+ }
329
+ async function secureGet(key) {
330
+ return getModule()?.secureGet(key) ?? null;
331
+ }
332
+ async function secureSet(key, value) {
333
+ return getModule()?.secureSet(key, value) ?? false;
334
+ }
335
+ async function secureRemove(key) {
336
+ return getModule()?.secureRemove(key) ?? false;
337
+ }
338
+ async function legacySecureGet(key) {
339
+ const mod = getModule();
340
+ if (!mod?.legacySecureGet) return null;
341
+ return mod.legacySecureGet(key);
342
+ }
343
+ var nativeStorage = {
344
+ isAvailable: isAvailable2,
345
+ get,
346
+ set,
347
+ remove,
348
+ secureGet,
349
+ secureSet,
350
+ secureRemove,
351
+ legacySecureGet
352
+ };
353
+
227
354
  // src/core/storage/SecureStorage.ts
228
- var import_async_storage = __toESM(require("@react-native-async-storage/async-storage"));
229
- var import_react_native_keychain = __toESM(require("react-native-keychain"));
230
- var KEYCHAIN_SERVICE_PREFIX = "com.paywallo.sdk.";
355
+ var KEYCHAIN_KEY_PREFIX = "com.paywallo.sdk.";
356
+ var REGULAR_KEY_PREFIX = "@paywallo:";
231
357
  var SecureStorage = class {
232
358
  // eslint-disable-next-line @typescript-eslint/no-useless-constructor
233
- constructor(_debug = false) {
359
+ constructor(_debug3 = false) {
234
360
  }
235
361
  async get(key) {
236
362
  const keychainValue = await this.getFromKeychain(key);
@@ -238,9 +364,10 @@ var SecureStorage = class {
238
364
  return keychainValue;
239
365
  }
240
366
  try {
241
- const value = await import_async_storage.default.getItem(`@paywallo:${key}`);
367
+ const value = await nativeStorage.get(`${REGULAR_KEY_PREFIX}${key}`);
242
368
  if (value) {
243
369
  await this.setToKeychain(key, value);
370
+ nativeStorage.remove(`${REGULAR_KEY_PREFIX}${key}`).catch(() => void 0);
244
371
  }
245
372
  return value;
246
373
  } catch {
@@ -248,20 +375,13 @@ var SecureStorage = class {
248
375
  }
249
376
  }
250
377
  async set(key, value) {
251
- try {
252
- await Promise.all([
253
- import_async_storage.default.setItem(`@paywallo:${key}`, value),
254
- this.setToKeychain(key, value)
255
- ]);
256
- return true;
257
- } catch {
258
- return false;
259
- }
378
+ return this.setToKeychain(key, value);
260
379
  }
261
380
  async remove(key) {
262
381
  try {
263
382
  await Promise.all([
264
- import_async_storage.default.removeItem(`@paywallo:${key}`),
383
+ // Also remove the legacy plaintext copy if it exists (migration cleanup).
384
+ nativeStorage.remove(`${REGULAR_KEY_PREFIX}${key}`).catch(() => void 0),
265
385
  this.removeFromKeychain(key)
266
386
  ]);
267
387
  return true;
@@ -270,31 +390,25 @@ var SecureStorage = class {
270
390
  }
271
391
  }
272
392
  hasKeychainSupport() {
273
- return true;
393
+ return nativeStorage.isAvailable();
274
394
  }
275
395
  async getFromKeychain(key) {
276
396
  try {
277
- const result = await import_react_native_keychain.default.getGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
278
- if (result && result.password) {
279
- return result.password;
280
- }
281
- return null;
397
+ return await nativeStorage.secureGet(`${KEYCHAIN_KEY_PREFIX}${key}`);
282
398
  } catch {
283
399
  return null;
284
400
  }
285
401
  }
286
402
  async setToKeychain(key, value) {
287
403
  try {
288
- await import_react_native_keychain.default.setGenericPassword("panel", value, { service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
289
- return true;
404
+ return await nativeStorage.secureSet(`${KEYCHAIN_KEY_PREFIX}${key}`, value);
290
405
  } catch {
291
406
  return false;
292
407
  }
293
408
  }
294
409
  async removeFromKeychain(key) {
295
410
  try {
296
- await import_react_native_keychain.default.resetGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
297
- return true;
411
+ return await nativeStorage.secureRemove(`${KEYCHAIN_KEY_PREFIX}${key}`);
298
412
  } catch {
299
413
  return false;
300
414
  }
@@ -302,6 +416,57 @@ var SecureStorage = class {
302
416
  };
303
417
  var secureStorage = new SecureStorage();
304
418
 
419
+ // src/core/storage/StorageMigration.ts
420
+ var MIGRATION_FLAG_KEY = "@paywallo:_migration_v152_done";
421
+ var LEGACY_SECURE_KEYS = [
422
+ "device_id",
423
+ "device_id_fallback",
424
+ "anon_id",
425
+ "user_email",
426
+ "user_properties"
427
+ ];
428
+ var KEYCHAIN_KEY_PREFIX2 = "com.paywallo.sdk.";
429
+ async function runStorageMigrationIfNeeded() {
430
+ if (!nativeStorage.isAvailable()) return;
431
+ const alreadyDone = await nativeStorage.get(MIGRATION_FLAG_KEY);
432
+ if (alreadyDone === "1") return;
433
+ await migrateAsyncStorage();
434
+ await migrateKeychainLegacy();
435
+ await nativeStorage.set(MIGRATION_FLAG_KEY, "1");
436
+ }
437
+ async function migrateAsyncStorage() {
438
+ try {
439
+ const mod = await import("@react-native-async-storage/async-storage");
440
+ const AsyncStorage = mod.default;
441
+ const allKeys = await AsyncStorage.getAllKeys();
442
+ const paywallo = allKeys.filter((k) => k.startsWith("@paywallo:"));
443
+ if (paywallo.length === 0) return;
444
+ const pairs = await AsyncStorage.multiGet(paywallo);
445
+ for (const [key, value] of pairs) {
446
+ if (!value) continue;
447
+ const existing = await nativeStorage.get(key);
448
+ if (!existing) {
449
+ await nativeStorage.set(key, value);
450
+ }
451
+ }
452
+ } catch {
453
+ }
454
+ }
455
+ async function migrateKeychainLegacy() {
456
+ for (const key of LEGACY_SECURE_KEYS) {
457
+ try {
458
+ const legacyValue = await nativeStorage.legacySecureGet(`${KEYCHAIN_KEY_PREFIX2}${key}`);
459
+ if (!legacyValue) continue;
460
+ const newSlotKey = `${KEYCHAIN_KEY_PREFIX2}${key}`;
461
+ const existing = await nativeStorage.secureGet(newSlotKey);
462
+ if (!existing) {
463
+ await nativeStorage.secureSet(newSlotKey, legacyValue);
464
+ }
465
+ } catch {
466
+ }
467
+ }
468
+ }
469
+
305
470
  // src/domains/identity/IdentityManager.ts
306
471
  var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
307
472
  var DEVICE_ID_KEY = "device_id";
@@ -318,18 +483,31 @@ var IdentityManager = class {
318
483
  this.secureStorage = null;
319
484
  this.debug = false;
320
485
  this.initialized = false;
486
+ this.initPromise = null;
321
487
  }
322
488
  async initialize(apiClient, debug = false) {
323
489
  if (this.initialized) {
324
490
  return this.deviceId ?? "";
325
491
  }
492
+ if (this.initPromise) {
493
+ return this.initPromise;
494
+ }
495
+ this.initPromise = this._doInitialize(apiClient, debug).finally(() => {
496
+ this.initPromise = null;
497
+ });
498
+ return this.initPromise;
499
+ }
500
+ async _doInitialize(apiClient, debug) {
326
501
  this.apiClient = apiClient;
327
502
  this.debug = debug;
328
503
  this.secureStorage = new SecureStorage(debug);
504
+ await runStorageMigrationIfNeeded();
329
505
  await this.loadPersistedState();
330
506
  if (!this.deviceId) {
331
507
  try {
332
- this.deviceId = await import_react_native_device_info.default.getUniqueId();
508
+ const info = await nativeDeviceInfo.getDeviceInfo();
509
+ const deviceId = info.deviceId;
510
+ this.deviceId = !deviceId || deviceId === "unknown" ? generateUUID() : deviceId;
333
511
  } catch {
334
512
  this.deviceId = generateUUID();
335
513
  }
@@ -361,9 +539,7 @@ var IdentityManager = class {
361
539
  }
362
540
  if (email) {
363
541
  if (!EMAIL_REGEX.test(email)) {
364
- if (this.debug) {
365
- console.warn("[Paywallo] Invalid email format, skipping email field");
366
- }
542
+ if (this.debug) console.warn("[Paywallo] Invalid email format, skipping email field");
367
543
  email = void 0;
368
544
  } else {
369
545
  this.email = email;
@@ -472,8 +648,8 @@ var IdentityManager = class {
472
648
  var identityManager = new IdentityManager();
473
649
 
474
650
  // src/domains/iap/NativeStoreKit.ts
475
- var PaywalloStoreKitNative = import_react_native.NativeModules.PaywalloStoreKit ?? null;
476
- function isAvailable() {
651
+ var PaywalloStoreKitNative = import_react_native3.NativeModules.PaywalloStoreKit ?? null;
652
+ function isAvailable3() {
477
653
  return PaywalloStoreKitNative != null;
478
654
  }
479
655
  function mapNativeProduct(native) {
@@ -545,11 +721,11 @@ function mapNativePurchase(native) {
545
721
  transactionId: native.transactionId,
546
722
  transactionDate: native.transactionDate,
547
723
  receipt: native.receipt,
548
- platform: import_react_native.Platform.OS
724
+ platform: import_react_native3.Platform.OS
549
725
  };
550
726
  }
551
727
  var nativeStoreKit = {
552
- isAvailable,
728
+ isAvailable: isAvailable3,
553
729
  async getProducts(productIds) {
554
730
  if (!PaywalloStoreKitNative) {
555
731
  return [];
@@ -566,8 +742,8 @@ var nativeStoreKit = {
566
742
  }
567
743
  const distinctId = identityManager.getDistinctId();
568
744
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
569
- if (distinctId && !uuidRegex.test(distinctId) && PaywalloClient.getConfig()?.debug) {
570
- console.warn("[Paywallo] distinctId is not a valid UUID for appAccountToken:", distinctId);
745
+ if (distinctId && !uuidRegex.test(distinctId)) {
746
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] distinctId is not a valid UUID for appAccountToken:", distinctId);
571
747
  }
572
748
  try {
573
749
  const result = await PaywalloStoreKitNative.purchase(productId, distinctId ?? null);
@@ -588,7 +764,7 @@ var nativeStoreKit = {
588
764
  try {
589
765
  await PaywalloStoreKitNative.finishTransaction(transactionId);
590
766
  } catch (err) {
591
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction native error", err);
767
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] finishTransaction native error", err);
592
768
  }
593
769
  },
594
770
  async getActiveTransactions() {
@@ -597,7 +773,7 @@ var nativeStoreKit = {
597
773
  const results = await PaywalloStoreKitNative.getActiveTransactions();
598
774
  return results.map(mapNativePurchase);
599
775
  } catch (err) {
600
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] getActiveTransactions failed", err);
776
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] getActiveTransactions failed", err);
601
777
  return [];
602
778
  }
603
779
  }
@@ -608,6 +784,7 @@ var IAPService = class {
608
784
  constructor(apiClient) {
609
785
  this.productsCache = /* @__PURE__ */ new Map();
610
786
  this.apiClient = null;
787
+ this.purchaseInFlight = null;
611
788
  this.apiClient = apiClient ?? null;
612
789
  }
613
790
  get debug() {
@@ -643,7 +820,7 @@ var IAPService = class {
643
820
  }
644
821
  return products;
645
822
  } catch (err) {
646
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] loadProducts failed", err);
823
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] loadProducts failed", err);
647
824
  return [];
648
825
  }
649
826
  }
@@ -668,7 +845,7 @@ var IAPService = class {
668
845
  async validateWithServer(purchase, productId, options) {
669
846
  const apiClient = this.getApiClient();
670
847
  const product = this.productsCache.get(productId);
671
- const platform = import_react_native2.Platform.OS;
848
+ const platform = import_react_native4.Platform.OS;
672
849
  const distinctId = PaywalloClient.getDistinctId();
673
850
  return this.validateWithRetry(
674
851
  () => apiClient.validatePurchase(
@@ -689,7 +866,7 @@ var IAPService = class {
689
866
  try {
690
867
  await nativeStoreKit.finishTransaction(transactionId);
691
868
  } catch (err) {
692
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction failed", err);
869
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] finishTransaction failed", err);
693
870
  }
694
871
  }
695
872
  async purchase(productId, options) {
@@ -699,27 +876,36 @@ var IAPService = class {
699
876
  error: new PurchaseError(PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE, "In-app purchases are not available on this device")
700
877
  };
701
878
  }
879
+ if (this.purchaseInFlight) {
880
+ return {
881
+ success: false,
882
+ error: new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, "A purchase is already in progress")
883
+ };
884
+ }
885
+ this.purchaseInFlight = productId;
702
886
  const debug = PaywalloClient.getConfig()?.debug;
703
- if (debug) console.info(`[Paywallo PURCHASE] starting ${productId}`);
887
+ if (debug) console.log(`[Paywallo PURCHASE] starting ${productId}`);
704
888
  try {
705
889
  const purchase = await nativeStoreKit.purchase(productId);
706
890
  if (!purchase) {
707
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} cancelled`);
891
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} cancelled`);
708
892
  return { success: false };
709
893
  }
710
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} approved`, { transactionId: purchase.transactionId });
894
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} approved`, { transactionId: purchase.transactionId });
711
895
  await this.finishTransaction(purchase.transactionId);
712
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} finished`);
896
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} finished`);
713
897
  this.validateWithServer(purchase, productId, options).then(() => {
714
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} server validated`);
898
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} server validated`);
715
899
  }).catch((err) => {
716
- if (debug) console.warn("[Paywallo PURCHASE] background validation failed", err);
900
+ if (debug) console.error("[Paywallo PURCHASE] background validation failed", err);
717
901
  });
718
902
  return { success: true, purchase };
719
903
  } catch (error) {
720
- if (debug) console.warn(`[Paywallo PURCHASE] ${productId} failed`, error);
904
+ if (debug) console.error(`[Paywallo PURCHASE] ${productId} failed`, error);
721
905
  const e = error instanceof PurchaseError ? error : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, error instanceof Error ? error.message : String(error));
722
906
  return { success: false, error: e };
907
+ } finally {
908
+ this.purchaseInFlight = null;
723
909
  }
724
910
  }
725
911
  async restore() {
@@ -730,7 +916,7 @@ var IAPService = class {
730
916
  const transactions = await nativeStoreKit.getActiveTransactions();
731
917
  const apiClient = this.getApiClient();
732
918
  const distinctId = PaywalloClient.getDistinctId();
733
- const platform = import_react_native2.Platform.OS;
919
+ const platform = import_react_native4.Platform.OS;
734
920
  const results = await Promise.allSettled(
735
921
  transactions.map((tx) => {
736
922
  const product = this.productsCache.get(tx.productId);
@@ -754,14 +940,14 @@ var IAPService = class {
754
940
  try {
755
941
  await nativeStoreKit.finishTransaction(tx.transactionId);
756
942
  } catch (err) {
757
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction failed", err);
943
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] finishTransaction failed", err);
758
944
  }
759
945
  validated.push(tx);
760
946
  }
761
947
  }
762
948
  return validated;
763
949
  } catch (err) {
764
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] restore failed", err);
950
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] restore failed", err);
765
951
  return [];
766
952
  }
767
953
  }
@@ -792,7 +978,7 @@ function usePaywallTracking() {
792
978
  ...sessionId && { sessionId }
793
979
  });
794
980
  } catch (err) {
795
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackDismissed failed", err);
981
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] trackDismissed failed", err);
796
982
  }
797
983
  }, []);
798
984
  const trackProductSelected = (0, import_react.useCallback)((opts) => {
@@ -807,7 +993,7 @@ function usePaywallTracking() {
807
993
  ...opts.campaignId && { campaignId: opts.campaignId },
808
994
  ...sessionId && { sessionId }
809
995
  }).catch((err) => {
810
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackProductSelected failed", err);
996
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] trackProductSelected failed", err);
811
997
  });
812
998
  }, []);
813
999
  const trackPurchased = (0, import_react.useCallback)(async (opts) => {
@@ -824,25 +1010,24 @@ function usePaywallTracking() {
824
1010
  ...sessionId && { sessionId }
825
1011
  });
826
1012
  } catch (err) {
827
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackPurchased failed", err);
1013
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] trackPurchased failed", err);
828
1014
  }
829
1015
  }, []);
830
1016
  return { trackDismissed, trackProductSelected, trackPurchased };
831
1017
  }
832
1018
 
833
1019
  // src/domains/paywall/usePaywallActions.ts
834
- var SCREEN_HEIGHT = import_react_native3.Dimensions.get("window").height;
835
1020
  function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId, openAnim) {
836
1021
  const [isPurchasing, setIsPurchasing] = (0, import_react2.useState)(false);
837
1022
  const [isClosing, setIsClosing] = (0, import_react2.useState)(false);
838
- const slideAnim = (0, import_react2.useRef)(new import_react_native3.Animated.Value(0)).current;
1023
+ const slideAnim = (0, import_react2.useRef)(new import_react_native5.Animated.Value(0)).current;
839
1024
  const presentedAtRef = (0, import_react2.useRef)(Date.now());
840
1025
  const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
841
1026
  const handleClose = (0, import_react2.useCallback)(() => {
842
1027
  if (isClosing) return;
843
1028
  setIsClosing(true);
844
1029
  const animTarget = openAnim ?? slideAnim;
845
- import_react_native3.Animated.timing(animTarget, { toValue: SCREEN_HEIGHT, duration: 350, useNativeDriver: true }).start(() => {
1030
+ import_react_native5.Animated.timing(animTarget, { toValue: import_react_native5.Dimensions.get("window").height, duration: 350, useNativeDriver: true }).start(() => {
846
1031
  if (paywallConfig) {
847
1032
  const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
848
1033
  void trackDismissed({ placement, paywallId: paywallConfig.id, durationSeconds, action: "close", variantKey, campaignId });
@@ -931,6 +1116,7 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
931
1116
  trackingDoneRef.current = false;
932
1117
  return;
933
1118
  }
1119
+ let cancelled = false;
934
1120
  const load = async () => {
935
1121
  setError(null);
936
1122
  try {
@@ -951,27 +1137,30 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
951
1137
  if (!fetched) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_FOUND, `Paywall not found for placement: ${placement}`);
952
1138
  config = fetched;
953
1139
  }
1140
+ if (cancelled) return;
954
1141
  setPaywallConfig(config);
955
1142
  const productIds = [];
956
1143
  if (config.primaryProductId) productIds.push(config.primaryProductId);
957
1144
  if (config.secondaryProductId) productIds.push(config.secondaryProductId);
958
1145
  const snapProducts = preloadedProductsRef.current;
959
1146
  if (snapProducts && snapProducts.size > 0) {
960
- setProducts(snapProducts);
1147
+ if (!cancelled) setProducts(snapProducts);
961
1148
  } else if (productIds.length > 0) {
962
1149
  try {
963
1150
  const storeProducts = await getIAPService().loadProducts(productIds);
964
- const map = /* @__PURE__ */ new Map();
965
- for (const p of storeProducts) map.set(p.productId, p);
966
- setProducts(map);
1151
+ if (!cancelled) {
1152
+ const map = /* @__PURE__ */ new Map();
1153
+ for (const p of storeProducts) map.set(p.productId, p);
1154
+ setProducts(map);
1155
+ }
967
1156
  } catch (err) {
968
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] loadProducts in paywall failed", err);
969
- setProducts(/* @__PURE__ */ new Map());
1157
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] loadProducts in paywall failed", err);
1158
+ if (!cancelled) setProducts(/* @__PURE__ */ new Map());
970
1159
  }
971
1160
  } else {
972
- setProducts(/* @__PURE__ */ new Map());
1161
+ if (!cancelled) setProducts(/* @__PURE__ */ new Map());
973
1162
  }
974
- if (!trackingDoneRef.current) {
1163
+ if (!trackingDoneRef.current && !cancelled) {
975
1164
  trackingDoneRef.current = true;
976
1165
  const sessionId = PaywalloClient.getSessionId();
977
1166
  await apiClient.trackEvent("$paywall_viewed", PaywalloClient.getDistinctId(), {
@@ -983,17 +1172,21 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
983
1172
  });
984
1173
  }
985
1174
  } catch (err) {
1175
+ if (cancelled) return;
986
1176
  const e = err instanceof Error ? err : new PaywallError(PAYWALL_ERROR_CODES.LOAD_FAILED, String(err));
987
1177
  setError(e);
988
1178
  }
989
1179
  };
990
1180
  void load();
1181
+ return () => {
1182
+ cancelled = true;
1183
+ };
991
1184
  }, [placement, visible, variantKey, campaignId]);
992
1185
  return { paywallConfig, products, error };
993
1186
  }
994
1187
 
995
1188
  // src/utils/localization.ts
996
- var import_react_native4 = require("react-native");
1189
+ var import_react_native6 = require("react-native");
997
1190
  var currentLanguage = "pt-BR";
998
1191
  var defaultLanguage = "pt-BR";
999
1192
  function setCurrentLanguage(language) {
@@ -1015,11 +1208,11 @@ function getLocalizedString(text) {
1015
1208
  }
1016
1209
  function detectDeviceLanguage() {
1017
1210
  try {
1018
- if (import_react_native4.Platform.OS === "ios") {
1019
- const settings = import_react_native4.NativeModules.SettingsManager?.settings;
1211
+ if (import_react_native6.Platform.OS === "ios") {
1212
+ const settings = import_react_native6.NativeModules.SettingsManager?.settings;
1020
1213
  return settings?.AppleLocale ?? settings?.AppleLanguages?.[0] ?? "en-US";
1021
1214
  }
1022
- return import_react_native4.NativeModules.I18nManager?.localeIdentifier ?? "en-US";
1215
+ return import_react_native6.NativeModules.I18nManager?.localeIdentifier ?? "en-US";
1023
1216
  } catch {
1024
1217
  return "en-US";
1025
1218
  }
@@ -1055,9 +1248,8 @@ var SDK_STRINGS = {
1055
1248
  };
1056
1249
  function getSdkString(key) {
1057
1250
  const map = SDK_STRINGS[key];
1058
- const lang = currentLanguage;
1059
- if (lang in map) return map[lang];
1060
- const base = lang.split("-")[0];
1251
+ if (currentLanguage in map) return map[currentLanguage];
1252
+ const base = currentLanguage.split("-")[0];
1061
1253
  if (base && base in map) return map[base];
1062
1254
  if (defaultLanguage in map) return map[defaultLanguage];
1063
1255
  return Object.values(map)[0];
@@ -1074,9 +1266,17 @@ function initLocalization(options) {
1074
1266
 
1075
1267
  // src/domains/paywall/PaywallWebView.tsx
1076
1268
  var import_react4 = require("react");
1077
- var import_react_native5 = require("react-native");
1269
+ var import_react_native7 = require("react-native");
1078
1270
 
1079
1271
  // src/domains/paywall/parseWebViewMessage.ts
1272
+ var ALLOWED_MESSAGE_TYPES = /* @__PURE__ */ new Set([
1273
+ "purchase",
1274
+ "close",
1275
+ "restore",
1276
+ "select-product",
1277
+ "open-url",
1278
+ "ready"
1279
+ ]);
1080
1280
  function deriveMessageId(message) {
1081
1281
  if (message.id) return message.id;
1082
1282
  const payloadKey = message.payload?.productId ?? message.payload?.url ?? (message.payload?.timestamp != null ? String(message.payload.timestamp) : "");
@@ -1085,10 +1285,20 @@ function deriveMessageId(message) {
1085
1285
  function parseWebViewMessage(raw) {
1086
1286
  try {
1087
1287
  const parsed = JSON.parse(raw);
1088
- if (typeof parsed?.type !== "string") {
1288
+ if (typeof parsed?.type !== "string" || !ALLOWED_MESSAGE_TYPES.has(parsed.type)) {
1089
1289
  return null;
1090
1290
  }
1091
- return parsed;
1291
+ const payload = parsed.payload;
1292
+ const safePayload = payload !== null && typeof payload === "object" ? {
1293
+ productId: typeof payload.productId === "string" ? String(payload.productId) : void 0,
1294
+ url: typeof payload.url === "string" ? String(payload.url) : void 0,
1295
+ timestamp: typeof payload.timestamp === "number" ? Number(payload.timestamp) : void 0
1296
+ } : void 0;
1297
+ return {
1298
+ type: parsed.type,
1299
+ id: typeof parsed.id === "string" ? parsed.id : void 0,
1300
+ payload: safePayload
1301
+ };
1092
1302
  } catch {
1093
1303
  return null;
1094
1304
  }
@@ -1116,14 +1326,14 @@ var import_jsx_runtime = require("react/jsx-runtime");
1116
1326
  var NativeWebViewComponent = null;
1117
1327
  function getNativeWebView() {
1118
1328
  if (!NativeWebViewComponent) {
1119
- NativeWebViewComponent = (0, import_react_native5.requireNativeComponent)("PaywalloWebView");
1329
+ NativeWebViewComponent = (0, import_react_native7.requireNativeComponent)("PaywalloWebView");
1120
1330
  }
1121
1331
  return NativeWebViewComponent;
1122
1332
  }
1123
1333
  var webViewEmitter = null;
1124
1334
  function getWebViewEmitter() {
1125
1335
  if (!webViewEmitter) {
1126
- webViewEmitter = new import_react_native5.NativeEventEmitter(import_react_native5.NativeModules.PaywalloWebViewModule);
1336
+ webViewEmitter = new import_react_native7.NativeEventEmitter(import_react_native7.NativeModules.PaywalloWebViewModule);
1127
1337
  }
1128
1338
  return webViewEmitter;
1129
1339
  }
@@ -1198,12 +1408,24 @@ function PaywallWebView({
1198
1408
  setScriptToInject(script);
1199
1409
  }, [craftData, products, primaryProductId, secondaryProductId]);
1200
1410
  const processedIdsRef = (0, import_react4.useRef)(/* @__PURE__ */ new Set());
1411
+ const dedupeTimersRef = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
1412
+ (0, import_react4.useEffect)(() => {
1413
+ return () => {
1414
+ for (const timerId of dedupeTimersRef.current.values()) clearTimeout(timerId);
1415
+ dedupeTimersRef.current.clear();
1416
+ processedIdsRef.current.clear();
1417
+ };
1418
+ }, []);
1201
1419
  const handleParsedMessage = (0, import_react4.useCallback)(
1202
1420
  (message) => {
1203
1421
  const msgId = deriveMessageId(message);
1204
1422
  if (processedIdsRef.current.has(msgId)) return;
1205
1423
  processedIdsRef.current.add(msgId);
1206
- setTimeout(() => processedIdsRef.current.delete(msgId), 5e3);
1424
+ const timerId = setTimeout(() => {
1425
+ processedIdsRef.current.delete(msgId);
1426
+ dedupeTimersRef.current.delete(msgId);
1427
+ }, 5e3);
1428
+ dedupeTimersRef.current.set(msgId, timerId);
1207
1429
  switch (message.type) {
1208
1430
  case "ready":
1209
1431
  if (!paywallDataSent.current) {
@@ -1224,8 +1446,8 @@ function PaywallWebView({
1224
1446
  case "open-url":
1225
1447
  if (message.payload?.url) {
1226
1448
  const url = message.payload.url;
1227
- if (/^https?:\/\//i.test(url) || /^mailto:/i.test(url)) {
1228
- void import_react_native5.Linking.openURL(url);
1449
+ if (/^https?:\/\//i.test(url) || /^mailto:/i.test(url) || /^itms-apps:/i.test(url) || /^itms-services:/i.test(url) || /^market:/i.test(url)) {
1450
+ void import_react_native7.Linking.openURL(url);
1229
1451
  }
1230
1452
  }
1231
1453
  break;
@@ -1303,7 +1525,7 @@ function PaywallWebView({
1303
1525
  }, [isPurchasing]);
1304
1526
  if (!resolvedWebUrl) return null;
1305
1527
  const paywallUrl = `${resolvedWebUrl}/paywall/${paywallId}`;
1306
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native5.View, { style: styles.container, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1528
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native7.View, { style: styles.container, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1307
1529
  NativeWebView,
1308
1530
  {
1309
1531
  sourceUrl: paywallUrl,
@@ -1315,24 +1537,23 @@ function PaywallWebView({
1315
1537
  }
1316
1538
  ) });
1317
1539
  }
1318
- var styles = import_react_native5.StyleSheet.create({
1540
+ var styles = import_react_native7.StyleSheet.create({
1319
1541
  container: { flex: 1 },
1320
1542
  webview: { flex: 1, backgroundColor: "transparent" }
1321
1543
  });
1322
1544
 
1323
1545
  // src/domains/paywall/PaywallModal.tsx
1324
1546
  var import_jsx_runtime2 = require("react/jsx-runtime");
1325
- var SCREEN_HEIGHT2 = import_react_native6.Dimensions.get("window").height;
1326
1547
  function ErrorFallback({ description, onRetry, onClose }) {
1327
1548
  const overrides = PaywalloClient.getConfig()?.errorStrings;
1328
1549
  const title = overrides?.title ?? getSdkString("paywallErrorTitle");
1329
1550
  const retryLabel = overrides?.retry ?? getSdkString("paywallRetryButton");
1330
1551
  const closeLabel = overrides?.close ?? getSdkString("paywallCloseButton");
1331
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.View, { style: styles2.fallbackContainer, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native6.View, { style: styles2.fallbackCard, children: [
1332
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Text, { style: styles2.fallbackTitle, children: title }),
1333
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Text, { style: styles2.fallbackDescription, children: description }),
1334
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Pressable, { style: styles2.retryButton, onPress: onRetry, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Text, { style: styles2.retryButtonText, children: retryLabel }) }),
1335
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Pressable, { style: styles2.closeButton, onPress: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Text, { style: styles2.closeButtonText, children: closeLabel }) })
1552
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native8.View, { style: styles2.fallbackContainer, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native8.View, { style: styles2.fallbackCard, children: [
1553
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native8.Text, { style: styles2.fallbackTitle, children: title }),
1554
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native8.Text, { style: styles2.fallbackDescription, children: description }),
1555
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native8.Pressable, { style: styles2.retryButton, onPress: onRetry, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native8.Text, { style: styles2.retryButtonText, children: retryLabel }) }),
1556
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native8.Pressable, { style: styles2.closeButton, onPress: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native8.Text, { style: styles2.closeButtonText, children: closeLabel }) })
1336
1557
  ] }) });
1337
1558
  }
1338
1559
  function PaywallModal({
@@ -1352,7 +1573,8 @@ function PaywallModal({
1352
1573
  variantKey,
1353
1574
  campaignId
1354
1575
  );
1355
- const openAnim = (0, import_react5.useRef)(new import_react_native6.Animated.Value(SCREEN_HEIGHT2)).current;
1576
+ const screenHeight = import_react_native8.Dimensions.get("window").height;
1577
+ const openAnim = (0, import_react5.useRef)(new import_react_native8.Animated.Value(screenHeight)).current;
1356
1578
  const [modalVisible, setModalVisible] = (0, import_react5.useState)(false);
1357
1579
  const handleResult = (0, import_react5.useCallback)((result) => {
1358
1580
  setModalVisible(false);
@@ -1381,18 +1603,18 @@ function PaywallModal({
1381
1603
  (0, import_react5.useEffect)(() => {
1382
1604
  if (visible) {
1383
1605
  setModalVisible(true);
1384
- openAnim.setValue(SCREEN_HEIGHT2);
1385
- import_react_native6.Animated.timing(openAnim, {
1606
+ openAnim.setValue(import_react_native8.Dimensions.get("window").height);
1607
+ import_react_native8.Animated.timing(openAnim, {
1386
1608
  toValue: 0,
1387
1609
  duration: 550,
1388
- easing: import_react_native6.Easing.out(import_react_native6.Easing.cubic),
1610
+ easing: import_react_native8.Easing.out(import_react_native8.Easing.cubic),
1389
1611
  useNativeDriver: true
1390
1612
  }).start();
1391
1613
  }
1392
1614
  }, [visible, openAnim]);
1393
1615
  if (!modalVisible && !visible) return null;
1394
1616
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1395
- import_react_native6.Modal,
1617
+ import_react_native8.Modal,
1396
1618
  {
1397
1619
  visible: modalVisible,
1398
1620
  animationType: "none",
@@ -1400,8 +1622,8 @@ function PaywallModal({
1400
1622
  onRequestClose: handleClose,
1401
1623
  transparent: true,
1402
1624
  statusBarTranslucent: true,
1403
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.View, { style: styles2.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Animated.View, { style: [styles2.animatedContainer, { transform: [{ translateY: openAnim }] }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native6.View, { style: styles2.container, children: [
1404
- error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.View, { style: styles2.errorContainer, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native6.Text, { style: styles2.errorText, children: error.message }) }),
1625
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native8.View, { style: styles2.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native8.Animated.View, { style: [styles2.animatedContainer, { transform: [{ translateY: openAnim }] }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native8.View, { style: styles2.container, children: [
1626
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native8.View, { style: styles2.errorContainer, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native8.Text, { style: styles2.errorText, children: error.message }) }),
1405
1627
  !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: webViewError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1406
1628
  ErrorFallback,
1407
1629
  {
@@ -1429,7 +1651,7 @@ function PaywallModal({
1429
1651
  }
1430
1652
  );
1431
1653
  }
1432
- var styles2 = import_react_native6.StyleSheet.create({
1654
+ var styles2 = import_react_native8.StyleSheet.create({
1433
1655
  overlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.3)" },
1434
1656
  animatedContainer: { flex: 1, backgroundColor: "#fff" },
1435
1657
  container: { flex: 1, backgroundColor: "#fff" },
@@ -1531,6 +1753,7 @@ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
1531
1753
  resolverRef.current({ presented: false, purchased: false, cancelled: true, restored: false });
1532
1754
  resolverRef.current = null;
1533
1755
  }
1756
+ setShowingPreloadedWebView(false);
1534
1757
  };
1535
1758
  }, []);
1536
1759
  const presentPreloadedWebView = (0, import_react8.useCallback)(() => {
@@ -1576,8 +1799,9 @@ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
1576
1799
  }
1577
1800
 
1578
1801
  // src/domains/subscription/SubscriptionCache.ts
1579
- var LEGACY_CACHE_KEY = "subscription_cache";
1802
+ var LEGACY_CACHE_KEY = "@panel:subscription_cache";
1580
1803
  var CACHE_KEY_PREFIX = "subscription_cache:";
1804
+ var CACHE_INDEX_KEY = "subscription_cache:__index__";
1581
1805
  var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
1582
1806
  function keyFor(distinctId) {
1583
1807
  return `${CACHE_KEY_PREFIX}${distinctId}`;
@@ -1657,9 +1881,30 @@ var SubscriptionCache = class {
1657
1881
  this.memoryCache.set(distinctId, cached);
1658
1882
  try {
1659
1883
  await this.storage.set(keyFor(distinctId), JSON.stringify(cached));
1884
+ await this.addToIndex(distinctId);
1885
+ } catch {
1886
+ }
1887
+ }
1888
+ async addToIndex(distinctId) {
1889
+ try {
1890
+ const raw = await this.storage.get(CACHE_INDEX_KEY);
1891
+ const ids = raw ? JSON.parse(raw) : [];
1892
+ if (!ids.includes(distinctId)) {
1893
+ ids.push(distinctId);
1894
+ await this.storage.set(CACHE_INDEX_KEY, JSON.stringify(ids));
1895
+ }
1660
1896
  } catch {
1661
1897
  }
1662
1898
  }
1899
+ async readIndex() {
1900
+ try {
1901
+ const raw = await this.storage.get(CACHE_INDEX_KEY);
1902
+ if (!raw) return [];
1903
+ return JSON.parse(raw);
1904
+ } catch {
1905
+ return [];
1906
+ }
1907
+ }
1663
1908
  async invalidate(distinctId) {
1664
1909
  if (!distinctId) return;
1665
1910
  this.memoryCache.delete(distinctId);
@@ -1669,11 +1914,15 @@ var SubscriptionCache = class {
1669
1914
  }
1670
1915
  }
1671
1916
  async invalidateAll() {
1917
+ const memKeys = Array.from(this.memoryCache.keys());
1918
+ const indexKeys = await this.readIndex();
1919
+ const allKeys = Array.from(/* @__PURE__ */ new Set([...memKeys, ...indexKeys]));
1672
1920
  this.memoryCache.clear();
1673
- try {
1674
- await this.storage.remove(LEGACY_CACHE_KEY);
1675
- } catch {
1676
- }
1921
+ await Promise.allSettled([
1922
+ this.storage.remove(LEGACY_CACHE_KEY),
1923
+ this.storage.remove(CACHE_INDEX_KEY),
1924
+ ...allKeys.map((k) => this.storage.remove(keyFor(k)))
1925
+ ]);
1677
1926
  }
1678
1927
  isExpired(cached) {
1679
1928
  return Date.now() - cached.cachedAt > this.ttl;
@@ -1829,7 +2078,7 @@ var SubscriptionManagerClass = class {
1829
2078
  var subscriptionManager = new SubscriptionManagerClass();
1830
2079
 
1831
2080
  // src/core/ApiClient.ts
1832
- var import_react_native8 = require("react-native");
2081
+ var import_react_native11 = require("react-native");
1833
2082
 
1834
2083
  // src/core/apiUrlUtils.ts
1835
2084
  var DEFAULT_WEB_URL = "https://paywallo.com.br";
@@ -1877,7 +2126,7 @@ async function validatePurchase(client, appKey, platform, receipt, productId, tr
1877
2126
  false
1878
2127
  );
1879
2128
  if (!response.ok) {
1880
- throw new Error(`Server validation failed: HTTP ${response.status}`);
2129
+ throw new PurchaseError(PURCHASE_ERROR_CODES.VALIDATION_FAILED, `Server validation failed: HTTP ${response.status}`);
1881
2130
  }
1882
2131
  return response.data;
1883
2132
  }
@@ -1886,7 +2135,7 @@ async function getSubscriptionStatus(client, baseUrl, appKey, distinctId) {
1886
2135
  url.searchParams.set("distinctId", distinctId);
1887
2136
  const response = await client.get(url.toString());
1888
2137
  if (!response.ok) {
1889
- throw new Error(`Subscription status check failed: HTTP ${response.status}`);
2138
+ throw new PurchaseError(PURCHASE_ERROR_CODES.NETWORK_ERROR, `Subscription status check failed: HTTP ${response.status}`);
1890
2139
  }
1891
2140
  return response.data;
1892
2141
  }
@@ -1928,7 +2177,7 @@ var HttpClient = class {
1928
2177
  constructor(baseUrl, config) {
1929
2178
  this.config = {
1930
2179
  baseUrl,
1931
- timeout: config?.timeout ?? DEFAULT_HTTP_CONFIG.timeout,
2180
+ timeout: config?.timeout ?? DEFAULT_HTTP_CONFIG.timeout ?? 1e4,
1932
2181
  retry: { ...DEFAULT_RETRY_CONFIG, ...config?.retry },
1933
2182
  debug: config?.debug ?? false
1934
2183
  };
@@ -1958,7 +2207,7 @@ var HttpClient = class {
1958
2207
  if (this.shouldRetry(response.status, retryConfig)) {
1959
2208
  if (state.attempt < retryConfig.maxRetries) {
1960
2209
  state.attempt++;
1961
- const delay = this.calculateDelay(state.attempt, retryConfig);
2210
+ const delay = this.calculateDelayForResponse(state.attempt, response.headers, retryConfig);
1962
2211
  await this.sleep(delay);
1963
2212
  continue;
1964
2213
  }
@@ -2012,7 +2261,7 @@ var HttpClient = class {
2012
2261
  };
2013
2262
  } catch (error) {
2014
2263
  this.log("Request failed", {
2015
- url,
2264
+ url: this.redactUrl(url),
2016
2265
  error: error instanceof Error ? error.message : String(error)
2017
2266
  });
2018
2267
  throw error;
@@ -2025,6 +2274,14 @@ var HttpClient = class {
2025
2274
  }
2026
2275
  buildUrl(path) {
2027
2276
  if (path.startsWith("http://") || path.startsWith("https://")) {
2277
+ if (path.startsWith("http://")) {
2278
+ const isLocalDev = /^http:\/\/(localhost|127\.0\.0\.1|192\.168\.\d{1,3}\.\d{1,3})/.test(path);
2279
+ if (!isLocalDev) {
2280
+ const sanitizedPath = path.split("?")[0];
2281
+ this.log("Blocked http:// request \u2014 only HTTPS is allowed in production:", sanitizedPath);
2282
+ throw new ClientError(CLIENT_ERROR_CODES.INSECURE_REQUEST, `Insecure HTTP request blocked: ${sanitizedPath}`);
2283
+ }
2284
+ }
2028
2285
  return path;
2029
2286
  }
2030
2287
  return `${this.config.baseUrl}${path}`;
@@ -2064,9 +2321,28 @@ var HttpClient = class {
2064
2321
  const jitter = Math.random() * 0.3 * exponentialDelay;
2065
2322
  return Math.min(exponentialDelay + jitter, config.maxDelay);
2066
2323
  }
2324
+ calculateDelayForResponse(attempt, headers, config) {
2325
+ const retryAfter = headers["retry-after"];
2326
+ if (retryAfter) {
2327
+ const seconds = Number(retryAfter);
2328
+ if (!Number.isNaN(seconds) && seconds > 0) {
2329
+ return Math.min(seconds * 1e3, config.maxDelay);
2330
+ }
2331
+ const date = new Date(retryAfter);
2332
+ if (!Number.isNaN(date.getTime())) {
2333
+ const ms = date.getTime() - Date.now();
2334
+ if (ms > 0) return Math.min(ms, config.maxDelay);
2335
+ }
2336
+ }
2337
+ return this.calculateDelay(attempt, config);
2338
+ }
2067
2339
  sleep(ms) {
2068
2340
  return new Promise((resolve) => setTimeout(resolve, ms));
2069
2341
  }
2342
+ /** Strips path segments that look like app keys (alphanumeric, 20–64 chars) from URLs before logging. */
2343
+ redactUrl(url) {
2344
+ return url.replace(/\/([a-zA-Z0-9_-]{20,64})(\?|$|\/)/g, "/[REDACTED]$2");
2345
+ }
2070
2346
  log(...args) {
2071
2347
  if (this.config.debug) console.log("[Paywallo:Http]", ...args);
2072
2348
  }
@@ -2079,7 +2355,7 @@ var HttpClient = class {
2079
2355
  };
2080
2356
 
2081
2357
  // src/core/network/NetworkMonitor.ts
2082
- var import_react_native7 = require("react-native");
2358
+ var import_react_native9 = require("react-native");
2083
2359
 
2084
2360
  // src/core/network/types.ts
2085
2361
  var DEFAULT_NETWORK_CONFIG = {
@@ -2116,9 +2392,19 @@ var NetworkMonitorClass = class {
2116
2392
  }, this.config.checkInterval);
2117
2393
  }
2118
2394
  setupAppStateListener() {
2119
- this.appStateSubscription = import_react_native7.AppState.addEventListener("change", (state) => {
2395
+ this.appStateSubscription = import_react_native9.AppState.addEventListener("change", (state) => {
2120
2396
  if (state === "active") {
2397
+ if (!this.checkIntervalId) {
2398
+ this.checkIntervalId = setInterval(() => {
2399
+ void this.checkConnectivity();
2400
+ }, this.config.checkInterval);
2401
+ }
2121
2402
  void this.checkConnectivity();
2403
+ } else if (state === "background" || state === "inactive") {
2404
+ if (this.checkIntervalId) {
2405
+ clearInterval(this.checkIntervalId);
2406
+ this.checkIntervalId = null;
2407
+ }
2122
2408
  }
2123
2409
  });
2124
2410
  }
@@ -2200,7 +2486,6 @@ var NetworkMonitorClass = class {
2200
2486
  var networkMonitor = new NetworkMonitorClass();
2201
2487
 
2202
2488
  // src/core/queue/OfflineQueue.ts
2203
- var import_async_storage2 = __toESM(require("@react-native-async-storage/async-storage"));
2204
2489
  init_uuid();
2205
2490
 
2206
2491
  // src/core/queue/deduplication.ts
@@ -2262,7 +2547,7 @@ var OfflineQueueClass = class {
2262
2547
  }
2263
2548
  async loadFromStorage() {
2264
2549
  try {
2265
- const stored = await import_async_storage2.default.getItem(this.config.storageKey);
2550
+ const stored = await nativeStorage.get(this.config.storageKey);
2266
2551
  if (stored) {
2267
2552
  this.queue = JSON.parse(stored);
2268
2553
  }
@@ -2272,7 +2557,7 @@ var OfflineQueueClass = class {
2272
2557
  }
2273
2558
  async saveToStorage() {
2274
2559
  try {
2275
- await import_async_storage2.default.setItem(this.config.storageKey, JSON.stringify(this.queue));
2560
+ await nativeStorage.set(this.config.storageKey, JSON.stringify(this.queue));
2276
2561
  return true;
2277
2562
  } catch (error) {
2278
2563
  this.log("Failed to save queue to storage", error);
@@ -2287,7 +2572,7 @@ var OfflineQueueClass = class {
2287
2572
  cleanupExpired() {
2288
2573
  const now = Date.now();
2289
2574
  const before = this.queue.length;
2290
- this.queue = this.queue.filter((item) => Date.now() - item.timestamp < this.config.maxAge);
2575
+ this.queue = this.queue.filter((item) => now - item.timestamp < this.config.maxAge);
2291
2576
  if (before !== this.queue.length) {
2292
2577
  void this.saveToStorage();
2293
2578
  }
@@ -2310,10 +2595,16 @@ var OfflineQueueClass = class {
2310
2595
  return this.queue[existingIndex];
2311
2596
  }
2312
2597
  if (this.queue.length >= this.config.maxItems) {
2313
- const removed = this.queue.shift();
2314
- if (removed) {
2315
- this.emit({ type: "item:evicted", item: removed, queueSize: this.queue.length });
2598
+ let evictIndex = 0;
2599
+ for (let i = 1; i < this.queue.length; i++) {
2600
+ const candidate = this.queue[i];
2601
+ const current = this.queue[evictIndex];
2602
+ if (candidate.attempts > current.attempts || candidate.attempts === current.attempts && candidate.timestamp < current.timestamp) {
2603
+ evictIndex = i;
2604
+ }
2316
2605
  }
2606
+ const [removed] = this.queue.splice(evictIndex, 1);
2607
+ this.emit({ type: "item:evicted", item: removed, queueSize: this.queue.length });
2317
2608
  }
2318
2609
  this.queue.push(item);
2319
2610
  await this.saveToStorage();
@@ -2593,7 +2884,6 @@ var QueueProcessorClass = class {
2593
2884
  id: item.id,
2594
2885
  url: item.url,
2595
2886
  method: item.method,
2596
- headers: item.headers,
2597
2887
  attempts: item.attempts
2598
2888
  });
2599
2889
  const response = await this.httpClient.request(item.url, {
@@ -2611,7 +2901,7 @@ var QueueProcessorClass = class {
2611
2901
  id: item.id,
2612
2902
  url: item.url,
2613
2903
  status: response.status,
2614
- appKeyInHeaders: item.headers?.["X-App-Key"]
2904
+ hasAppKey: item.headers?.["X-App-Key"] != null
2615
2905
  });
2616
2906
  return { success: false, permanent: true, status: response.status };
2617
2907
  }
@@ -2733,15 +3023,96 @@ var ApiCache = class {
2733
3023
  }
2734
3024
  };
2735
3025
 
3026
+ // src/core/EventBatcher.ts
3027
+ var import_react_native10 = require("react-native");
3028
+ var BATCH_MAX_SIZE = 10;
3029
+ var BATCH_FLUSH_MS = 5e3;
3030
+ var EventBatcher = class {
3031
+ constructor(post, debug = false) {
3032
+ this.batch = [];
3033
+ this.timer = null;
3034
+ this.post = post;
3035
+ this.debug = debug;
3036
+ }
3037
+ async track(eventName, distinctId, environment, properties, timestamp) {
3038
+ const EVENT_NAME_REGEX = /^(\$[a-zA-Z0-9_]+|[a-z][a-z0-9_]*)$/;
3039
+ if (!EVENT_NAME_REGEX.test(eventName)) {
3040
+ if (this.debug) console.warn(`[Paywallo] Invalid event name "${eventName}". Must be snake_case or start with $`);
3041
+ }
3042
+ const event = {
3043
+ eventName,
3044
+ distinctId,
3045
+ properties: { ...properties, platform: import_react_native10.Platform.OS },
3046
+ timestamp: timestamp ?? Date.now(),
3047
+ environment: environment.toLowerCase()
3048
+ };
3049
+ this.batch.push(event);
3050
+ if (this.batch.length >= BATCH_MAX_SIZE) {
3051
+ if (this.timer !== null) {
3052
+ clearTimeout(this.timer);
3053
+ this.timer = null;
3054
+ }
3055
+ await this.flush();
3056
+ return;
3057
+ }
3058
+ if (this.timer !== null) clearTimeout(this.timer);
3059
+ this.timer = setTimeout(() => {
3060
+ this.timer = null;
3061
+ void this.flush();
3062
+ }, BATCH_FLUSH_MS);
3063
+ }
3064
+ async flush() {
3065
+ if (this.batch.length === 0) return;
3066
+ const batch = this.batch.splice(0, this.batch.length);
3067
+ if (this.timer !== null) {
3068
+ clearTimeout(this.timer);
3069
+ this.timer = null;
3070
+ }
3071
+ try {
3072
+ await this.post("/api/v1/events/batch", { events: batch }, `event_batch:${batch.length}`);
3073
+ } catch {
3074
+ for (const event of batch) {
3075
+ await this.post("/api/v1/events", event, `event:${event.eventName}`);
3076
+ }
3077
+ }
3078
+ }
3079
+ dispose() {
3080
+ if (this.timer !== null) {
3081
+ clearTimeout(this.timer);
3082
+ this.timer = null;
3083
+ }
3084
+ this.batch = [];
3085
+ }
3086
+ };
3087
+
3088
+ // src/core/FlagStorage.ts
3089
+ var FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
3090
+ async function readFlagFromStorage(flagKey, distinctId) {
3091
+ try {
3092
+ const raw = await secureStorage.get(`flag:${flagKey}:${distinctId}`);
3093
+ if (!raw) return null;
3094
+ const parsed = JSON.parse(raw);
3095
+ if (typeof parsed !== "object" || parsed === null || !("variant" in parsed) || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") return null;
3096
+ const { variant, payload, cachedAt } = parsed;
3097
+ if (Date.now() - cachedAt > FLAG_MAX_AGE_MS) return null;
3098
+ return { variant, payload };
3099
+ } catch {
3100
+ return null;
3101
+ }
3102
+ }
3103
+ async function writeFlagToStorage(flagKey, distinctId, flag) {
3104
+ try {
3105
+ const value = JSON.stringify({ variant: flag.variant, payload: flag.payload, cachedAt: Date.now() });
3106
+ await secureStorage.set(`flag:${flagKey}:${distinctId}`, value);
3107
+ } catch {
3108
+ }
3109
+ }
3110
+
2736
3111
  // src/core/ApiClient.ts
2737
- var SDK_VERSION = "1.5.0";
2738
- var _ApiClient = class _ApiClient {
3112
+ var SDK_VERSION = "1.5.2";
3113
+ var ApiClient = class {
2739
3114
  constructor(appKey, apiUrl, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
2740
3115
  this.cache = new ApiCache();
2741
- this.eventBatch = [];
2742
- this.batchTimer = null;
2743
- this.BATCH_MAX_SIZE = 10;
2744
- this.BATCH_FLUSH_MS = 5e3;
2745
3116
  this.appKey = appKey;
2746
3117
  this.baseUrl = apiUrl;
2747
3118
  this.webUrl = deriveWebUrl(this.baseUrl);
@@ -2749,6 +3120,7 @@ var _ApiClient = class _ApiClient {
2749
3120
  this.environment = environment;
2750
3121
  this.offlineQueueEnabled = offlineQueueEnabled;
2751
3122
  this.httpClient = new HttpClient(this.baseUrl, { timeout, debug });
3123
+ this.batcher = new EventBatcher((url, payload, label) => this.postWithQueue(url, payload, label), debug);
2752
3124
  }
2753
3125
  getHttpClient() {
2754
3126
  return this.httpClient;
@@ -2759,6 +3131,9 @@ var _ApiClient = class _ApiClient {
2759
3131
  getWebUrl() {
2760
3132
  return this.webUrl;
2761
3133
  }
3134
+ getAppKey() {
3135
+ return this.appKey;
3136
+ }
2762
3137
  getEnvironment() {
2763
3138
  return this.environment;
2764
3139
  }
@@ -2794,79 +3169,33 @@ var _ApiClient = class _ApiClient {
2794
3169
  message,
2795
3170
  context,
2796
3171
  sdkVersion: SDK_VERSION,
2797
- platform: import_react_native8.Platform.OS
3172
+ platform: import_react_native11.Platform.OS
2798
3173
  }, true);
2799
3174
  } catch (err) {
2800
- if (this.debug) console.warn("[Paywallo:ApiClient] reportError failed", err);
3175
+ if (this.debug) console.log("[Paywallo:ApiClient] reportError failed", err);
2801
3176
  }
2802
3177
  }
2803
3178
  async trackEvent(eventName, distinctId, properties, timestamp) {
2804
- const EVENT_NAME_REGEX = /^(\$[a-zA-Z0-9_]+|[a-z][a-z0-9_]*)$/;
2805
- if (!EVENT_NAME_REGEX.test(eventName)) {
2806
- if (this.debug) {
2807
- console.warn(`[Paywallo] Invalid event name "${eventName}". Must be snake_case or start with $`);
2808
- }
2809
- }
2810
- const event = {
2811
- eventName,
2812
- distinctId,
2813
- properties: { ...properties, platform: import_react_native8.Platform.OS },
2814
- timestamp: timestamp ?? Date.now(),
2815
- environment: this.environment.toLowerCase()
2816
- };
2817
- this.eventBatch.push(event);
2818
- if (this.eventBatch.length >= this.BATCH_MAX_SIZE) {
2819
- if (this.batchTimer !== null) {
2820
- clearTimeout(this.batchTimer);
2821
- this.batchTimer = null;
2822
- }
2823
- await this.flushEventBatch();
2824
- return;
2825
- }
2826
- if (this.batchTimer !== null) {
2827
- clearTimeout(this.batchTimer);
2828
- }
2829
- this.batchTimer = setTimeout(() => {
2830
- this.batchTimer = null;
2831
- void this.flushEventBatch();
2832
- }, this.BATCH_FLUSH_MS);
3179
+ await this.batcher.track(eventName, distinctId, this.environment, properties, timestamp);
2833
3180
  }
2834
3181
  async flushEventBatch() {
2835
- if (this.eventBatch.length === 0) return;
2836
- const batch = this.eventBatch.splice(0, this.eventBatch.length);
2837
- if (this.batchTimer !== null) {
2838
- clearTimeout(this.batchTimer);
2839
- this.batchTimer = null;
2840
- }
2841
- try {
2842
- await this.postWithQueue(
2843
- "/api/v1/events/batch",
2844
- { events: batch },
2845
- `event_batch:${batch.length}`
2846
- );
2847
- } catch (error) {
2848
- this.log("Batch send failed, falling back to individual sends:", error);
2849
- for (const event of batch) {
2850
- await this.postWithQueue(
2851
- "/api/v1/events",
2852
- event,
2853
- `event:${event.eventName}`
2854
- );
2855
- }
2856
- }
3182
+ await this.batcher.flush();
3183
+ }
3184
+ dispose() {
3185
+ this.batcher.dispose();
2857
3186
  }
2858
3187
  async identify(distinctId, properties, email, deviceId) {
2859
- await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: import_react_native8.Platform.OS, ...deviceId && { deviceId } }, "identify");
3188
+ await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: import_react_native11.Platform.OS, ...deviceId && { deviceId } }, "identify");
2860
3189
  }
2861
3190
  async startSession(distinctId, sessionId, platform, appVersion, deviceModel, osVersion) {
2862
3191
  await this.postWithQueue("/api/v1/sessions/start", {
2863
3192
  distinctId,
2864
3193
  sessionId,
2865
- platform: platform ?? import_react_native8.Platform.OS,
3194
+ platform: platform ?? import_react_native11.Platform.OS,
2866
3195
  environment: this.environment.toLowerCase(),
2867
3196
  appVersion: appVersion ?? "unknown",
2868
3197
  deviceModel: deviceModel ?? "unknown",
2869
- osVersion: osVersion ?? String(import_react_native8.Platform.Version)
3198
+ osVersion: osVersion ?? String(import_react_native11.Platform.Version)
2870
3199
  }, `session.start:${sessionId}`);
2871
3200
  return { success: true };
2872
3201
  }
@@ -2878,7 +3207,7 @@ var _ApiClient = class _ApiClient {
2878
3207
  const key = `${flagKey}:${distinctId}`;
2879
3208
  const hit = this.cache.getVariant(key);
2880
3209
  if (hit !== void 0) return hit;
2881
- const persisted = await this.readFlagFromStorage(flagKey, distinctId);
3210
+ const persisted = await readFlagFromStorage(flagKey, distinctId);
2882
3211
  if (persisted !== null) {
2883
3212
  this.cache.setVariant(key, persisted);
2884
3213
  return persisted;
@@ -2886,25 +3215,20 @@ var _ApiClient = class _ApiClient {
2886
3215
  try {
2887
3216
  const res = await this.get(`/api/v1/flags/${flagKey}?distinctId=${encodeURIComponent(distinctId)}`);
2888
3217
  if (res.status === 404) {
2889
- this.log("Flag not found (404):", flagKey);
2890
3218
  const v = { variant: null };
2891
3219
  this.cache.setVariant(key, v, this.cache.nullTTL);
2892
3220
  return v;
2893
3221
  }
2894
3222
  if (!res.ok) {
2895
- this.log("Non-OK response for variant:", flagKey, res.status);
2896
3223
  const stale = this.cache.getStaleVariant(key);
2897
- if (stale !== void 0) return stale;
2898
- return { variant: null };
3224
+ return stale !== void 0 ? stale : { variant: null };
2899
3225
  }
2900
3226
  this.cache.setVariant(key, res.data);
2901
- await this.writeFlagToStorage(flagKey, distinctId, res.data);
3227
+ await writeFlagToStorage(flagKey, distinctId, res.data);
2902
3228
  return res.data;
2903
- } catch (error) {
2904
- this.log("Failed to get variant:", flagKey, error);
3229
+ } catch {
2905
3230
  const stale = this.cache.getStaleVariant(key);
2906
- if (stale !== void 0) return stale;
2907
- return { variant: null };
3231
+ return stale !== void 0 ? stale : { variant: null };
2908
3232
  }
2909
3233
  }
2910
3234
  async getPaywall(placement) {
@@ -2913,29 +3237,22 @@ var _ApiClient = class _ApiClient {
2913
3237
  try {
2914
3238
  const res = await this.get(`/api/v1/paywalls/${placement}`);
2915
3239
  if (res.status === 404) {
2916
- this.log("Paywall not found (404):", placement);
2917
3240
  this.cache.setPaywall(placement, null, this.cache.nullTTL);
2918
3241
  return null;
2919
3242
  }
2920
3243
  if (!res.ok) {
2921
- this.log("Non-OK response for paywall:", placement, res.status);
2922
3244
  const stale = this.cache.getStalePaywall(placement);
2923
- if (stale !== void 0) return stale;
2924
- return null;
3245
+ return stale !== void 0 ? stale : null;
2925
3246
  }
2926
3247
  this.cache.setPaywall(placement, res.data);
2927
3248
  return res.data;
2928
- } catch (error) {
2929
- this.log("Failed to get paywall:", placement, error);
3249
+ } catch {
2930
3250
  const stale = this.cache.getStalePaywall(placement);
2931
- if (stale !== void 0) return stale;
2932
- return null;
3251
+ return stale !== void 0 ? stale : null;
2933
3252
  }
2934
3253
  }
2935
3254
  async getEmergencyPaywall() {
2936
- const result = await getEmergencyPaywall(this);
2937
- this.log("Got emergency paywall:", result.enabled);
2938
- return result;
3255
+ return getEmergencyPaywall(this);
2939
3256
  }
2940
3257
  async getCampaign(placement, distinctId, context) {
2941
3258
  const key = `${placement}:${distinctId}`;
@@ -2944,18 +3261,14 @@ var _ApiClient = class _ApiClient {
2944
3261
  try {
2945
3262
  const result = await getCampaignData(this, this.baseUrl, placement, distinctId, context);
2946
3263
  if (!result) {
2947
- this.log("No campaign found for placement:", placement);
2948
3264
  this.cache.setCampaign(key, null, this.cache.nullTTL);
2949
3265
  return null;
2950
3266
  }
2951
- this.log("Got campaign:", placement, result.variantKey);
2952
3267
  this.cache.setCampaign(key, result);
2953
3268
  return result;
2954
- } catch (error) {
2955
- this.log("Failed to get campaign:", placement, error);
3269
+ } catch {
2956
3270
  const stale = this.cache.getStaleCampaign(key);
2957
- if (stale !== void 0) return stale;
2958
- return null;
3271
+ return stale !== void 0 ? stale : null;
2959
3272
  }
2960
3273
  }
2961
3274
  async getPrimaryCampaign(distinctId) {
@@ -2965,63 +3278,34 @@ var _ApiClient = class _ApiClient {
2965
3278
  try {
2966
3279
  const res = await this.get(`/campaigns/public/primary?distinctId=${encodeURIComponent(distinctId)}`);
2967
3280
  if (res.status === 404) {
2968
- this.log("Primary campaign not found (404)");
2969
3281
  this.cache.setCampaign(key, null, this.cache.nullTTL);
2970
3282
  return null;
2971
3283
  }
2972
3284
  if (!res.ok) {
2973
- this.log("Non-OK response for primary campaign:", res.status);
2974
3285
  const stale = this.cache.getStaleCampaign(key);
2975
- if (stale !== void 0) return stale;
2976
- return null;
3286
+ return stale !== void 0 ? stale : null;
2977
3287
  }
2978
- this.log("Got primary campaign:", res.data?.campaignId);
2979
3288
  this.cache.setCampaign(key, res.data);
2980
3289
  return res.data;
2981
- } catch (error) {
2982
- this.log("Failed to get primary campaign:", error);
3290
+ } catch {
2983
3291
  const stale = this.cache.getStaleCampaign(key);
2984
- if (stale !== void 0) return stale;
2985
- return null;
3292
+ return stale !== void 0 ? stale : null;
2986
3293
  }
2987
3294
  }
2988
3295
  async validatePurchase(platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
2989
- const result = await validatePurchase(
2990
- this,
2991
- this.appKey,
2992
- platform,
2993
- receipt,
2994
- productId,
2995
- transactionId,
2996
- priceLocal,
2997
- currency,
2998
- country,
2999
- distinctId,
3000
- paywallPlacement,
3001
- variantKey
3002
- );
3003
- this.log("Purchase validated:", result.success);
3004
- return result;
3296
+ return validatePurchase(this, this.appKey, platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey);
3005
3297
  }
3006
3298
  async getSubscriptionStatus(distinctId) {
3007
- const result = await getSubscriptionStatus(this, this.baseUrl, this.appKey, distinctId);
3008
- this.log("Subscription status:", result.hasActiveSubscription);
3009
- return result;
3299
+ return getSubscriptionStatus(this, this.baseUrl, this.appKey, distinctId);
3010
3300
  }
3011
3301
  async evaluateFlags(keys, distinctId) {
3012
3302
  const query = keys.map(encodeURIComponent).join(",");
3013
- const path = `/api/v1/flags/evaluate?keys=${query}`;
3014
3303
  try {
3015
- const res = await this.httpClient.get(path, {
3304
+ const res = await this.httpClient.get(`/api/v1/flags/evaluate?keys=${query}`, {
3016
3305
  headers: { "X-App-Key": this.appKey, "x-distinct-id": distinctId }
3017
3306
  });
3018
- if (!res.ok) {
3019
- this.log("evaluateFlags failed:", res.status, keys);
3020
- return {};
3021
- }
3022
- const raw = res.data;
3023
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
3024
- const record = raw;
3307
+ if (!res.ok || typeof res.data !== "object" || res.data === null || Array.isArray(res.data)) return {};
3308
+ const record = res.data;
3025
3309
  const result = {};
3026
3310
  for (const key of Object.keys(record)) {
3027
3311
  const value = record[key];
@@ -3029,11 +3313,10 @@ var _ApiClient = class _ApiClient {
3029
3313
  result[key] = variant;
3030
3314
  const flagVariant = { variant };
3031
3315
  this.cache.setVariant(`${key}:${distinctId}`, flagVariant);
3032
- await this.writeFlagToStorage(key, distinctId, flagVariant);
3316
+ await writeFlagToStorage(key, distinctId, flagVariant);
3033
3317
  }
3034
3318
  return result;
3035
- } catch (error) {
3036
- this.log("Failed to evaluateFlags:", error);
3319
+ } catch {
3037
3320
  return {};
3038
3321
  }
3039
3322
  }
@@ -3041,15 +3324,9 @@ var _ApiClient = class _ApiClient {
3041
3324
  try {
3042
3325
  const url = buildConditionalFlagUrl(this.baseUrl, flagKey, context);
3043
3326
  const response = await this.get(url);
3044
- if (response.status === 404) return { value: false, flagKey };
3045
- if (!response.ok) {
3046
- this.log("Non-OK response for conditional flag:", flagKey, response.status);
3047
- return { value: false, flagKey };
3048
- }
3049
- this.log("Got conditional flag:", flagKey, response.data.value);
3327
+ if (response.status === 404 || !response.ok) return { value: false, flagKey };
3050
3328
  return { value: response.data.value, flagKey };
3051
3329
  } catch {
3052
- this.log("Failed to get conditional flag:", flagKey);
3053
3330
  return { value: false, flagKey };
3054
3331
  }
3055
3332
  }
@@ -3057,34 +3334,13 @@ var _ApiClient = class _ApiClient {
3057
3334
  const key = `${flagKey}:${distinctId}`;
3058
3335
  const hit = this.cache.getVariant(key);
3059
3336
  if (hit !== void 0) return hit;
3060
- const persisted = await this.readFlagFromStorage(flagKey, distinctId);
3337
+ const persisted = await readFlagFromStorage(flagKey, distinctId);
3061
3338
  if (persisted !== null) {
3062
3339
  this.cache.setVariant(key, persisted);
3063
3340
  return persisted;
3064
3341
  }
3065
3342
  return null;
3066
3343
  }
3067
- async readFlagFromStorage(flagKey, distinctId) {
3068
- try {
3069
- const raw = await secureStorage.get(`flag:${flagKey}:${distinctId}`);
3070
- if (!raw) return null;
3071
- const parsed = JSON.parse(raw);
3072
- if (typeof parsed !== "object" || parsed === null || !("variant" in parsed) || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") return null;
3073
- const { variant, payload, cachedAt } = parsed;
3074
- if (Date.now() - cachedAt > _ApiClient.FLAG_MAX_AGE_MS) return null;
3075
- return { variant, payload };
3076
- } catch {
3077
- return null;
3078
- }
3079
- }
3080
- async writeFlagToStorage(flagKey, distinctId, flag) {
3081
- try {
3082
- const value = JSON.stringify({ variant: flag.variant, payload: flag.payload, cachedAt: Date.now() });
3083
- await secureStorage.set(`flag:${flagKey}:${distinctId}`, value);
3084
- } catch (err) {
3085
- this.log("writeFlagToStorage failed", err);
3086
- }
3087
- }
3088
3344
  async postWithQueue(url, payload, label) {
3089
3345
  if (this.offlineQueueEnabled && !networkMonitor.isOnline()) {
3090
3346
  await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
@@ -3097,7 +3353,6 @@ var _ApiClient = class _ApiClient {
3097
3353
  this.log("Request failed:", label, error);
3098
3354
  if (this.offlineQueueEnabled) {
3099
3355
  await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
3100
- this.log("Queued after failure:", label);
3101
3356
  return;
3102
3357
  }
3103
3358
  throw error;
@@ -3107,8 +3362,6 @@ var _ApiClient = class _ApiClient {
3107
3362
  if (this.debug) console.log("[Paywallo:ApiClient]", ...args);
3108
3363
  }
3109
3364
  };
3110
- _ApiClient.FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
3111
- var ApiClient = _ApiClient;
3112
3365
 
3113
3366
  // src/domains/campaign/CampaignError.ts
3114
3367
  var CampaignError = class extends PaywalloError {
@@ -3133,6 +3386,7 @@ var CampaignGateService = class {
3133
3386
  constructor() {
3134
3387
  this.preloadedCampaigns = /* @__PURE__ */ new Map();
3135
3388
  this.pendingPreloads = /* @__PURE__ */ new Set();
3389
+ this.activePreloadPromises = /* @__PURE__ */ new Map();
3136
3390
  }
3137
3391
  async getCampaign(apiClient, placement, context) {
3138
3392
  const distinctId = identityManager.getDistinctId();
@@ -3172,12 +3426,23 @@ var CampaignGateService = class {
3172
3426
  }
3173
3427
  async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
3174
3428
  const isActive = await hasActive();
3175
- if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo] requireSubscription \u2014 hasActive: ${isActive}`);
3429
+ if (PaywalloClient.getConfig()?.debug) console.log(`[Paywallo] requireSubscription \u2014 hasActive: ${isActive}`);
3176
3430
  if (isActive) return true;
3177
3431
  const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, { ...context, forceShow: true });
3178
3432
  return r.purchased || r.restored;
3179
3433
  }
3180
3434
  async preloadCampaign(apiClient, placement, context) {
3435
+ const existing = this.activePreloadPromises.get(placement);
3436
+ if (existing) return existing;
3437
+ const promise = this._doPreload(apiClient, placement, context);
3438
+ this.activePreloadPromises.set(placement, promise);
3439
+ try {
3440
+ return await promise;
3441
+ } finally {
3442
+ this.activePreloadPromises.delete(placement);
3443
+ }
3444
+ }
3445
+ async _doPreload(apiClient, placement, context) {
3181
3446
  this.pendingPreloads.add(placement);
3182
3447
  try {
3183
3448
  const campaign = await this.getCampaign(apiClient, placement, context);
@@ -3186,7 +3451,7 @@ var CampaignGateService = class {
3186
3451
  }
3187
3452
  this.preloadedCampaigns.set(placement, { campaign, preloadedAt: Date.now() });
3188
3453
  if (PaywalloClient.getConfig()?.debug) {
3189
- console.info(`[Paywallo PRELOAD] data cached \u2014 placement: ${placement}`);
3454
+ console.log(`[Paywallo PRELOAD] data cached \u2014 placement: ${placement}`);
3190
3455
  }
3191
3456
  return { success: true };
3192
3457
  } catch (error) {
@@ -3212,7 +3477,7 @@ var CampaignGateService = class {
3212
3477
  var campaignGateService = new CampaignGateService();
3213
3478
 
3214
3479
  // src/domains/identity/AdvertisingIdManager.ts
3215
- var import_react_native9 = require("react-native");
3480
+ var import_react_native12 = require("react-native");
3216
3481
  var ZERO_IDFA = "00000000-0000-0000-0000-000000000000";
3217
3482
  var AdvertisingIdManager = class {
3218
3483
  constructor() {
@@ -3223,7 +3488,7 @@ var AdvertisingIdManager = class {
3223
3488
  const fallback = { idfv: null, idfa: null, gaid: null, attStatus: "unavailable" };
3224
3489
  try {
3225
3490
  const tracking = await import("expo-tracking-transparency");
3226
- if (import_react_native9.Platform.OS === "ios") {
3491
+ if (import_react_native12.Platform.OS === "ios") {
3227
3492
  if (!tracking.isAvailable()) {
3228
3493
  const idfv2 = await this.collectIdfv();
3229
3494
  this.cached = { ...fallback, idfv: idfv2 };
@@ -3242,10 +3507,8 @@ var AdvertisingIdManager = class {
3242
3507
  this.cached = { idfv, idfa, gaid: null, attStatus: status };
3243
3508
  return this.cached;
3244
3509
  }
3245
- if (import_react_native9.Platform.OS === "android") {
3246
- const androidStatus = await tracking.getTrackingPermissionsAsync();
3247
- const optedIn = androidStatus.granted;
3248
- const rawGaid = optedIn ? tracking.getAdvertisingId() : null;
3510
+ if (import_react_native12.Platform.OS === "android") {
3511
+ const rawGaid = tracking.getAdvertisingId();
3249
3512
  const gaid = this.sanitizeId(rawGaid);
3250
3513
  this.cached = { idfv: null, idfa: null, gaid, attStatus: "unavailable" };
3251
3514
  return this.cached;
@@ -3259,9 +3522,8 @@ var AdvertisingIdManager = class {
3259
3522
  }
3260
3523
  async collectIdfv() {
3261
3524
  try {
3262
- const app = await import("expo-application");
3263
- const idfv = await app.getIosIdForVendorAsync();
3264
- return this.sanitizeId(idfv);
3525
+ const info = await nativeDeviceInfo.getDeviceInfo();
3526
+ return this.sanitizeId(info.idfv);
3265
3527
  } catch {
3266
3528
  return null;
3267
3529
  }
@@ -3273,14 +3535,12 @@ var AdvertisingIdManager = class {
3273
3535
  };
3274
3536
 
3275
3537
  // src/domains/identity/InstallTracker.ts
3276
- var import_react_native14 = require("react-native");
3277
- var import_async_storage3 = __toESM(require("@react-native-async-storage/async-storage"));
3278
- var import_react_native_device_info2 = __toESM(require("react-native-device-info"));
3538
+ var import_react_native16 = require("react-native");
3279
3539
 
3280
3540
  // src/domains/identity/MetaBridge.ts
3281
- var import_react_native10 = require("react-native");
3541
+ var import_react_native13 = require("react-native");
3282
3542
  var TIMEOUT_MS = 2e3;
3283
- var NativeBridge = import_react_native10.NativeModules.PaywalloFBBridge ?? null;
3543
+ var NativeBridge = import_react_native13.NativeModules.PaywalloFBBridge ?? null;
3284
3544
  var MetaBridge = class {
3285
3545
  async getAnonymousID() {
3286
3546
  if (!NativeBridge) return null;
@@ -3328,11 +3588,11 @@ var MetaBridge = class {
3328
3588
  var metaBridge = new MetaBridge();
3329
3589
 
3330
3590
  // src/domains/identity/InstallReferrerManager.ts
3331
- var import_react_native11 = require("react-native");
3591
+ var import_react_native14 = require("react-native");
3332
3592
  var REFERRER_TIMEOUT_MS = 5e3;
3333
3593
  var InstallReferrerManager = class {
3334
3594
  async getReferrer() {
3335
- if (import_react_native11.Platform.OS !== "android") {
3595
+ if (import_react_native14.Platform.OS !== "android") {
3336
3596
  return this.emptyResult();
3337
3597
  }
3338
3598
  let timerId;
@@ -3346,8 +3606,7 @@ var InstallReferrerManager = class {
3346
3606
  }
3347
3607
  async fetchReferrer() {
3348
3608
  try {
3349
- const Application = await import("expo-application");
3350
- const referrer = await Application.getInstallReferrerAsync();
3609
+ const referrer = await nativeDeviceInfo.getInstallReferrer();
3351
3610
  if (!referrer) return this.emptyResult();
3352
3611
  const capped = referrer.slice(0, 2048);
3353
3612
  const params = this.parseReferrer(capped);
@@ -3392,51 +3651,16 @@ var InstallReferrerManager = class {
3392
3651
  var installReferrerManager = new InstallReferrerManager();
3393
3652
 
3394
3653
  // src/domains/session/SessionManager.ts
3395
- var import_react_native13 = require("react-native");
3654
+ var import_react_native15 = require("react-native");
3396
3655
  init_uuid();
3397
3656
 
3398
3657
  // src/utils/deviceInfo.ts
3399
- var import_react_native12 = require("react-native");
3400
- async function getAppVersion() {
3401
- try {
3402
- const app = await import("expo-application");
3403
- if (app.nativeApplicationVersion) {
3404
- return app.nativeApplicationVersion;
3405
- }
3406
- } catch {
3407
- }
3408
- try {
3409
- const DeviceInfo2 = (await import("react-native-device-info")).default;
3410
- const v = DeviceInfo2.getVersion();
3411
- if (v) return v;
3412
- } catch {
3413
- }
3414
- return "unknown";
3415
- }
3416
- async function getDeviceModel() {
3417
- try {
3418
- const DeviceInfo2 = (await import("react-native-device-info")).default;
3419
- const model = DeviceInfo2.getModel();
3420
- if (model) return model;
3421
- } catch {
3422
- }
3423
- return "unknown";
3424
- }
3425
- function getOsVersion() {
3426
- const v = import_react_native12.Platform.Version;
3427
- if (typeof v === "number") return String(v);
3428
- if (typeof v === "string" && v.length > 0) return v;
3429
- return "unknown";
3430
- }
3431
3658
  async function collectDeviceInfo() {
3432
- const [appVersion, deviceModel] = await Promise.all([
3433
- getAppVersion(),
3434
- getDeviceModel()
3435
- ]);
3659
+ const result = await nativeDeviceInfo.getDeviceInfo();
3436
3660
  return {
3437
- appVersion,
3438
- deviceModel,
3439
- osVersion: getOsVersion()
3661
+ appVersion: result.appVersion,
3662
+ deviceModel: result.model,
3663
+ osVersion: result.systemVersion
3440
3664
  };
3441
3665
  }
3442
3666
 
@@ -3627,7 +3851,7 @@ var SessionManager = class {
3627
3851
  if (this.appStateSubscription) {
3628
3852
  this.appStateSubscription.remove();
3629
3853
  }
3630
- this.appStateSubscription = import_react_native13.AppState.addEventListener("change", this.handleAppStateChange);
3854
+ this.appStateSubscription = import_react_native15.AppState.addEventListener("change", this.handleAppStateChange);
3631
3855
  }
3632
3856
  async handleForeground() {
3633
3857
  if (this.backgroundTimestamp) {
@@ -3700,16 +3924,26 @@ var sessionManager = new SessionManager();
3700
3924
  // src/domains/identity/InstallTracker.ts
3701
3925
  var InstallTracker = class {
3702
3926
  constructor(debug = false, advertisingIdManager) {
3927
+ this._trackingInProgress = false;
3703
3928
  this.debug = debug;
3704
3929
  this.advertisingIdManager = advertisingIdManager ?? new AdvertisingIdManager();
3705
3930
  }
3706
3931
  async trackIfNeeded(apiClient, requestATT = false) {
3932
+ if (this._trackingInProgress) return;
3933
+ this._trackingInProgress = true;
3934
+ try {
3935
+ await this._doTrack(apiClient, requestATT);
3936
+ } finally {
3937
+ this._trackingInProgress = false;
3938
+ }
3939
+ }
3940
+ async _doTrack(apiClient, requestATT) {
3707
3941
  const storage = new SecureStorage(this.debug);
3708
3942
  const alreadyTracked = await storage.get("@panel:install_tracked");
3709
3943
  if (alreadyTracked) return;
3710
3944
  let fallbackTracked = null;
3711
3945
  try {
3712
- fallbackTracked = await import_async_storage3.default.getItem("@paywallo:install_tracked");
3946
+ fallbackTracked = await nativeStorage.get("@paywallo:install_tracked");
3713
3947
  } catch {
3714
3948
  }
3715
3949
  if (fallbackTracked) return;
@@ -3717,47 +3951,47 @@ var InstallTracker = class {
3717
3951
  const sessionId = sessionManager.getSessionId();
3718
3952
  const installedAt = Date.now();
3719
3953
  if (!distinctId) return;
3720
- const deviceInfo = import_react_native_device_info2.default;
3721
3954
  let deviceData = {};
3722
3955
  try {
3723
- const screen = import_react_native14.Dimensions.get("screen");
3956
+ const screen = import_react_native16.Dimensions.get("screen");
3724
3957
  const screenWidth = Math.round(screen.width ?? 0);
3725
3958
  const screenHeight = Math.round(screen.height ?? 0);
3726
3959
  const locale = Intl.DateTimeFormat().resolvedOptions().locale ?? "unknown";
3727
3960
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "unknown";
3728
- const deviceModel = deviceInfo?.getModel() || "unknown";
3729
- const osVersion = deviceInfo?.getSystemVersion() || "unknown";
3730
- const appVersion = deviceInfo?.getVersion() || "unknown";
3731
- const buildNumber = deviceInfo?.getBuildNumber() || "0";
3961
+ const info = await nativeDeviceInfo.getDeviceInfo();
3962
+ const deviceModel = info.modelId || info.model || "unknown";
3963
+ const osVersion = info.systemVersion || "unknown";
3964
+ const appVersion = info.appVersion || "unknown";
3965
+ const buildNumber = info.buildNumber || "0";
3732
3966
  let carrier = "unknown";
3733
3967
  try {
3734
- carrier = await deviceInfo?.getCarrier?.() ?? "unknown";
3968
+ carrier = info.carrier ?? "unknown";
3735
3969
  } catch {
3736
3970
  }
3737
3971
  let totalDisk = 0;
3738
3972
  try {
3739
- totalDisk = Number(await deviceInfo?.getTotalDiskCapacity?.()) || 0;
3973
+ totalDisk = Number(info.totalDisk) || 0;
3740
3974
  } catch {
3741
3975
  }
3742
3976
  let freeDisk = 0;
3743
3977
  try {
3744
- freeDisk = Number(await deviceInfo?.getFreeDiskStorage?.()) || 0;
3978
+ freeDisk = Number(info.freeDisk) || 0;
3745
3979
  } catch {
3746
3980
  }
3747
3981
  let totalRam = 0;
3748
3982
  try {
3749
- totalRam = Number(await deviceInfo?.getTotalMemory?.()) || 0;
3983
+ totalRam = Number(info.totalRam) || 0;
3750
3984
  } catch {
3751
3985
  }
3752
3986
  let brand = "unknown";
3753
3987
  try {
3754
- brand = deviceInfo?.getBrand?.() ?? "unknown";
3988
+ brand = info.brand ?? "unknown";
3755
3989
  } catch {
3756
3990
  }
3757
3991
  let installerPackage = null;
3758
3992
  try {
3759
- if (import_react_native14.Platform.OS === "android") {
3760
- installerPackage = await deviceInfo?.getInstallerPackageName?.() ?? null;
3993
+ if (import_react_native16.Platform.OS === "android") {
3994
+ installerPackage = info.installerPackage ?? null;
3761
3995
  }
3762
3996
  } catch {
3763
3997
  }
@@ -3796,37 +4030,34 @@ var InstallTracker = class {
3796
4030
  });
3797
4031
  }
3798
4032
  }
3799
- try {
3800
- await apiClient.trackEvent("$app_installed", distinctId, {
3801
- installedAt,
3802
- platform: import_react_native14.Platform.OS,
3803
- ...sessionId && { sessionId },
3804
- ...deviceData,
3805
- ...effectiveAnonId && { fbAnonId: effectiveAnonId },
3806
- ...referrer.rawReferrer && { installReferrer: referrer.rawReferrer },
3807
- ...referrer.trackingId && { referrerTrackingId: referrer.trackingId },
3808
- ...referrer.fbclid && { referrerFbclid: referrer.fbclid },
3809
- ...referrer.utmSource && { referrerUtmSource: referrer.utmSource },
3810
- ...referrer.utmMedium && { referrerUtmMedium: referrer.utmMedium },
3811
- ...referrer.utmCampaign && { referrerUtmCampaign: referrer.utmCampaign },
3812
- ...adIds.idfa && { idfa: adIds.idfa },
3813
- ...adIds.idfv && { idfv: adIds.idfv },
3814
- ...adIds.gaid && { gaid: adIds.gaid },
3815
- attStatus: adIds.attStatus
4033
+ await apiClient.trackEvent("$app_installed", distinctId, {
4034
+ installedAt,
4035
+ platform: import_react_native16.Platform.OS,
4036
+ ...sessionId && { sessionId },
4037
+ ...deviceData,
4038
+ ...effectiveAnonId && { fbAnonId: effectiveAnonId },
4039
+ ...referrer.rawReferrer && { installReferrer: referrer.rawReferrer },
4040
+ ...referrer.trackingId && { referrerTrackingId: referrer.trackingId },
4041
+ ...referrer.fbclid && { referrerFbclid: referrer.fbclid },
4042
+ ...referrer.utmSource && { referrerUtmSource: referrer.utmSource },
4043
+ ...referrer.utmMedium && { referrerUtmMedium: referrer.utmMedium },
4044
+ ...referrer.utmCampaign && { referrerUtmCampaign: referrer.utmCampaign },
4045
+ ...adIds.idfa && { idfa: adIds.idfa },
4046
+ ...adIds.idfv && { idfv: adIds.idfv },
4047
+ ...adIds.gaid && { gaid: adIds.gaid },
4048
+ attStatus: adIds.attStatus
4049
+ });
4050
+ if (import_react_native16.Platform.OS === "ios") {
4051
+ void this.callDeferredMatch(apiClient, deviceData, adIds, effectiveAnonId, installedAt).catch(() => {
3816
4052
  });
3817
- if (import_react_native14.Platform.OS === "ios") {
3818
- void this.callDeferredMatch(apiClient, deviceData, adIds, effectiveAnonId).catch(() => {
3819
- });
3820
- }
3821
- await storage.set("@panel:install_tracked", String(installedAt));
3822
- try {
3823
- await import_async_storage3.default.setItem("@paywallo:install_tracked", String(installedAt));
3824
- } catch {
3825
- }
4053
+ }
4054
+ await storage.set("@panel:install_tracked", String(installedAt));
4055
+ try {
4056
+ await nativeStorage.set("@paywallo:install_tracked", String(installedAt));
3826
4057
  } catch {
3827
4058
  }
3828
4059
  }
3829
- async callDeferredMatch(apiClient, deviceData, adIds, anonId) {
4060
+ async callDeferredMatch(apiClient, deviceData, adIds, anonId, installedAt) {
3830
4061
  const storage = new SecureStorage(this.debug);
3831
4062
  const alreadyMatched = await storage.get("@panel:deferred_match_done");
3832
4063
  if (alreadyMatched) return;
@@ -3834,7 +4065,7 @@ var InstallTracker = class {
3834
4065
  const timer = setTimeout(() => controller.abort(), 5e3);
3835
4066
  try {
3836
4067
  const baseUrl = apiClient.getBaseUrl();
3837
- const appKey = apiClient.appKey;
4068
+ const appKey = apiClient.getAppKey();
3838
4069
  await fetch(`${baseUrl}/api/v1/attribution/deferred-match`, {
3839
4070
  method: "POST",
3840
4071
  headers: { "Content-Type": "application/json", "X-App-Key": appKey },
@@ -3846,7 +4077,8 @@ var InstallTracker = class {
3846
4077
  screenHeight: deviceData.screenHeight,
3847
4078
  timezone: deviceData.timezone,
3848
4079
  locale: deviceData.locale,
3849
- fbAnonId: anonId
4080
+ fbAnonId: anonId,
4081
+ installTimestamp: new Date(installedAt).toISOString()
3850
4082
  }),
3851
4083
  signal: controller.signal
3852
4084
  });
@@ -3971,7 +4203,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3971
4203
  error: error instanceof Error ? error.message : String(error)
3972
4204
  });
3973
4205
  } catch (err) {
3974
- if (config.debug) console.warn("[Paywallo] reportInitFailure error", err);
4206
+ if (config.debug) console.error("[Paywallo] reportInitFailure error", err);
3975
4207
  }
3976
4208
  }
3977
4209
  reportInitSuccess(config, attempts) {
@@ -3980,7 +4212,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3980
4212
  void this.apiClient.reportError("INIT_RECOVERED", `Succeeded after ${attempts} retries`, { attempts });
3981
4213
  }
3982
4214
  } catch (err) {
3983
- if (config.debug) console.warn("[Paywallo] reportInitSuccess error", err);
4215
+ if (config.debug) console.error("[Paywallo] reportInitSuccess error", err);
3984
4216
  }
3985
4217
  }
3986
4218
  async resolveSessionFlags(keys, apiClient, distinctId) {
@@ -4027,24 +4259,26 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4027
4259
  if (config.sessionFlags && config.sessionFlags.length > 0) {
4028
4260
  await this.resolveSessionFlags(config.sessionFlags, this.apiClient, distinctId);
4029
4261
  }
4262
+ setNativeStorageDebug(config.debug ?? false);
4263
+ setNativeDeviceInfoDebug(config.debug ?? false);
4030
4264
  this.config = config;
4031
4265
  if (this.resolveReady) {
4032
4266
  this.resolveReady();
4033
4267
  this.resolveReady = null;
4034
4268
  }
4035
- if (config.debug) console.info("[Paywallo INIT] complete");
4269
+ if (config.debug) console.log("[Paywallo INIT] complete");
4036
4270
  if (distinctId) {
4037
4271
  this.apiClient.getSubscriptionStatus(distinctId).then(() => {
4038
- if (config.debug) console.info("[Paywallo SUB] preloaded on boot");
4272
+ if (config.debug) console.log("[Paywallo SUB] preloaded on boot");
4039
4273
  }).catch(() => {
4040
4274
  });
4041
4275
  }
4042
- if (this.apiClient) {
4276
+ {
4043
4277
  const apiClientRef = this.apiClient;
4044
4278
  if (config.autoPreloadCampaign) {
4045
4279
  this.autoPreloadedPlacement = config.autoPreloadCampaign;
4046
4280
  this.autoPreloadPlacementPromise = Promise.resolve(config.autoPreloadCampaign);
4047
- if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${config.autoPreloadCampaign}`);
4281
+ if (config.debug) console.log(`[Paywallo PRELOAD] started \u2014 placement: ${config.autoPreloadCampaign}`);
4048
4282
  void campaignGateService.preloadCampaign(apiClientRef, config.autoPreloadCampaign).catch(() => {
4049
4283
  });
4050
4284
  } else {
@@ -4052,7 +4286,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4052
4286
  const placement = primary?.placement ?? primary?.paywall?.placement;
4053
4287
  if (placement) {
4054
4288
  this.autoPreloadedPlacement = placement;
4055
- if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${placement}`);
4289
+ if (config.debug) console.log(`[Paywallo PRELOAD] started \u2014 placement: ${placement}`);
4056
4290
  void campaignGateService.preloadCampaign(apiClientRef, placement).catch(() => {
4057
4291
  });
4058
4292
  return placement;
@@ -4064,21 +4298,21 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4064
4298
  }
4065
4299
  async identify(options) {
4066
4300
  this.ensureInitialized();
4067
- if (this.config?.debug) console.info("[Paywallo IDENTIFY]", options);
4301
+ if (this.config?.debug) console.log("[Paywallo IDENTIFY]", options ? { hasEmail: "email" in options && !!options.email, hasProperties: "properties" in options } : void 0);
4068
4302
  await identityManager.identify(options);
4069
4303
  }
4070
4304
  async track(eventName, options) {
4071
4305
  const apiClient = this.getApiClientOrThrow();
4072
4306
  const distinctId = identityManager.getDistinctId();
4073
4307
  if (!distinctId) {
4074
- if (this.config?.debug) console.info(`[Paywallo TRACK] ${eventName} skipped (no distinctId)`);
4308
+ if (this.config?.debug) console.log(`[Paywallo TRACK] ${eventName} skipped (no distinctId)`);
4075
4309
  return;
4076
4310
  }
4077
4311
  if (!isValidEventName(eventName)) {
4078
4312
  const msg = `Invalid event name: "${eventName}". Use snake_case or $reserved_name.`;
4079
4313
  throw new ClientError(CLIENT_ERROR_CODES.INVALID_EVENT_NAME, msg);
4080
4314
  }
4081
- if (this.config?.debug && eventName.startsWith("$purchase")) console.info(`[Paywallo TRACK] ${eventName}`, options?.properties ?? {});
4315
+ if (this.config?.debug && eventName.startsWith("$purchase")) console.log(`[Paywallo TRACK] ${eventName}`, options?.properties ?? {});
4082
4316
  const sessionId = sessionManager.getSessionId();
4083
4317
  await apiClient.trackEvent(eventName, distinctId, {
4084
4318
  ...identityManager.getProperties(),
@@ -4137,14 +4371,14 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4137
4371
  try {
4138
4372
  const distinctId = identityManager.getDistinctId();
4139
4373
  if (!distinctId) {
4140
- if (this.config?.debug) console.info(`[Paywallo FLAG] ${flagKey} \u2192 null (no distinctId)`);
4374
+ if (this.config?.debug) console.log(`[Paywallo FLAG] ${flagKey} \u2192 null (no distinctId)`);
4141
4375
  return { variant: null };
4142
4376
  }
4143
4377
  const result = await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
4144
- if (this.config?.debug) console.info(`[Paywallo FLAG] ${flagKey} \u2192`, result.variant);
4378
+ if (this.config?.debug) console.log(`[Paywallo FLAG] ${flagKey} \u2192`, result.variant);
4145
4379
  return result;
4146
4380
  } catch (error) {
4147
- if (this.config?.debug) console.warn(`[Paywallo FLAG] ${flagKey} \u2192 error`, error);
4381
+ if (this.config?.debug) console.log(`[Paywallo FLAG] ${flagKey} \u2192 error`, error);
4148
4382
  if (this.apiClient) void this.apiClient.reportError("GET_VARIANT_FAILED", error instanceof Error ? error.message : String(error));
4149
4383
  return { variant: null };
4150
4384
  }
@@ -4177,11 +4411,11 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4177
4411
  }
4178
4412
  async presentCampaign(placement, context) {
4179
4413
  this.ensureInitialized();
4180
- if (this.config?.debug) console.info(`[Paywallo CAMPAIGN] presenting ${placement}`);
4414
+ if (this.config?.debug) console.log(`[Paywallo CAMPAIGN] presenting ${placement}`);
4181
4415
  const result = await campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, () => this.hasActiveSubscription(), this.paywallPresenter, this.campaignPresenter, context);
4182
4416
  if (this.config?.debug) {
4183
- if (result.error) console.warn(`[Paywallo CAMPAIGN] ${placement} error:`, result.error.message);
4184
- else console.info(`[Paywallo CAMPAIGN] ${placement} result:`, { purchased: result.purchased, cancelled: result.cancelled, restored: result.restored, skippedReason: result.skippedReason });
4417
+ if (result.error) console.log(`[Paywallo CAMPAIGN] ${placement} error:`, result.error.message);
4418
+ else console.log(`[Paywallo CAMPAIGN] ${placement} result:`, { purchased: result.purchased, cancelled: result.cancelled, restored: result.restored, skippedReason: result.skippedReason });
4185
4419
  }
4186
4420
  return result;
4187
4421
  }
@@ -4191,20 +4425,20 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4191
4425
  if (this.activeChecker) {
4192
4426
  const result = await this.activeChecker();
4193
4427
  const isActive2 = result === true;
4194
- if (this.config?.debug) console.info("[Paywallo SUB] hasActiveSubscription (checker):", isActive2);
4428
+ if (this.config?.debug) console.log("[Paywallo SUB] hasActiveSubscription (checker):", isActive2);
4195
4429
  return isActive2;
4196
4430
  }
4197
4431
  const distinctId = identityManager.getDistinctId();
4198
4432
  if (!distinctId || !this.apiClient) {
4199
- if (this.config?.debug) console.info("[Paywallo SUB] hasActiveSubscription: false (no distinctId/apiClient)");
4433
+ if (this.config?.debug) console.log("[Paywallo SUB] hasActiveSubscription: false (no distinctId/apiClient)");
4200
4434
  return false;
4201
4435
  }
4202
4436
  const status = await this.apiClient.getSubscriptionStatus(distinctId);
4203
4437
  const isActive = status.hasActiveSubscription === true;
4204
- if (this.config?.debug) console.info("[Paywallo SUB] hasActiveSubscription (server):", isActive);
4438
+ if (this.config?.debug) console.log("[Paywallo SUB] hasActiveSubscription (server):", isActive);
4205
4439
  return isActive;
4206
4440
  } catch (error) {
4207
- if (this.config?.debug) console.warn("[Paywallo SUB] hasActiveSubscription error", error);
4441
+ if (this.config?.debug) console.error("[Paywallo SUB] hasActiveSubscription error", error);
4208
4442
  if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", error instanceof Error ? error.message : "hasActiveSubscription failed");
4209
4443
  return false;
4210
4444
  }
@@ -4225,9 +4459,9 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4225
4459
  async restorePurchases() {
4226
4460
  this.ensureInitialized();
4227
4461
  if (!this.restoreHandler) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
4228
- if (this.config?.debug) console.info("[Paywallo RESTORE] starting");
4462
+ if (this.config?.debug) console.log("[Paywallo RESTORE] starting");
4229
4463
  const result = await this.restoreHandler();
4230
- if (this.config?.debug) console.info("[Paywallo RESTORE] result:", { success: result.success, restoredProducts: result.restoredProducts?.length ?? 0 });
4464
+ if (this.config?.debug) console.log("[Paywallo RESTORE] result:", { success: result.success, restoredProducts: result.restoredProducts?.length ?? 0 });
4231
4465
  return result;
4232
4466
  }
4233
4467
  async reset() {
@@ -4237,6 +4471,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4237
4471
  async fullReset() {
4238
4472
  const wasDebug = this.config?.debug ?? false;
4239
4473
  await sessionManager.endSession();
4474
+ sessionManager.destroy();
4240
4475
  await identityManager.reset();
4241
4476
  await subscriptionCache.invalidateAll();
4242
4477
  queueProcessor.dispose();
@@ -4245,6 +4480,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4245
4480
  campaignGateService.clearPreloaded();
4246
4481
  this.cleanupNetworkRecovery();
4247
4482
  this.sessionFlagsMap.clear();
4483
+ this.apiClient?.dispose();
4248
4484
  this.apiClient = null;
4249
4485
  this.config = null;
4250
4486
  this.initPromise = null;
@@ -4581,6 +4817,7 @@ function usePurchase() {
4581
4817
 
4582
4818
  // src/hooks/useSubscription.ts
4583
4819
  var import_react13 = require("react");
4820
+ var EMPTY_ENTITLEMENTS = [];
4584
4821
  function useSubscription() {
4585
4822
  const [status, setStatus] = (0, import_react13.useState)(null);
4586
4823
  const [isLoading, setIsLoading] = (0, import_react13.useState)(true);
@@ -4616,7 +4853,8 @@ function useSubscription() {
4616
4853
  setStatus(s);
4617
4854
  setIsLoading(false);
4618
4855
  }).catch((err) => {
4619
- setError(err instanceof Error ? err : new Error(String(err)));
4856
+ const e = err instanceof PurchaseError ? err : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, String(err));
4857
+ setError(e);
4620
4858
  setIsLoading(false);
4621
4859
  });
4622
4860
  const removeListener = subscriptionManager.addListener((newStatus) => {
@@ -4631,7 +4869,7 @@ function useSubscription() {
4631
4869
  isActive: status?.hasActiveSubscription ?? false,
4632
4870
  isLoading,
4633
4871
  error,
4634
- entitlements: status?.entitlements ?? [],
4872
+ entitlements: status?.entitlements ?? EMPTY_ENTITLEMENTS,
4635
4873
  refresh,
4636
4874
  restore
4637
4875
  };
@@ -4639,7 +4877,7 @@ function useSubscription() {
4639
4877
 
4640
4878
  // src/PaywalloProvider.tsx
4641
4879
  var import_react18 = require("react");
4642
- var import_react_native15 = require("react-native");
4880
+ var import_react_native17 = require("react-native");
4643
4881
 
4644
4882
  // src/hooks/useCampaignPreload.ts
4645
4883
  var import_react14 = require("react");
@@ -4763,7 +5001,7 @@ function useCampaignPreload() {
4763
5001
  campaignId: campaign.campaignId,
4764
5002
  variantKey: campaign.variantKey
4765
5003
  });
4766
- if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo WEBVIEW] mounting hidden \u2014 placement: ${placement}`);
5004
+ if (PaywalloClient.getConfig()?.debug) console.log(`[Paywallo WEBVIEW] mounting hidden \u2014 placement: ${placement}`);
4767
5005
  return { success: true };
4768
5006
  } catch (error) {
4769
5007
  return {
@@ -4798,7 +5036,7 @@ function useProductLoader() {
4798
5036
  for (const p of loaded) map.set(p.productId, p);
4799
5037
  setProducts(map);
4800
5038
  } catch (err) {
4801
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] refreshProducts failed", err);
5039
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] refreshProducts failed", err);
4802
5040
  } finally {
4803
5041
  setIsLoadingProducts(false);
4804
5042
  }
@@ -4838,7 +5076,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4838
5076
  clearPreloadedWebView();
4839
5077
  resolvePreloadedPaywall({ presented: true, purchased: true, cancelled: false, restored: false });
4840
5078
  } catch (error) {
4841
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] preloaded purchase failed", error);
5079
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] preloaded purchase failed", error);
4842
5080
  setShowingPreloadedWebView(false);
4843
5081
  clearPreloadedWebView();
4844
5082
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
@@ -4859,7 +5097,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4859
5097
  clearPreloadedWebView();
4860
5098
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: true });
4861
5099
  } catch (error) {
4862
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] preloaded restore failed", error);
5100
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] preloaded restore failed", error);
4863
5101
  setShowingPreloadedWebView(false);
4864
5102
  clearPreloadedWebView();
4865
5103
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
@@ -4872,11 +5110,28 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4872
5110
  var import_react17 = require("react");
4873
5111
  var CACHE_TTL_MS2 = 5 * 60 * 1e3;
4874
5112
  var ACTIVE_STATUSES = ["active", "in_grace_period"];
5113
+ var PUBLIC_TO_DOMAIN_STATUS = {
5114
+ active: "active",
5115
+ expired: "expired",
5116
+ in_billing_retry: "billing_retry",
5117
+ in_grace_period: "grace_period",
5118
+ revoked: "unknown",
5119
+ cancelled: "canceled"
5120
+ };
5121
+ var DOMAIN_TO_PUBLIC_STATUS = {
5122
+ active: "active",
5123
+ expired: "expired",
5124
+ billing_retry: "in_billing_retry",
5125
+ grace_period: "in_grace_period",
5126
+ canceled: "cancelled",
5127
+ paused: "expired",
5128
+ unknown: "expired"
5129
+ };
4875
5130
  function toDomainSubscriptionInfo(sub) {
4876
5131
  return {
4877
5132
  id: "",
4878
5133
  productId: sub.productId,
4879
- status: sub.status,
5134
+ status: PUBLIC_TO_DOMAIN_STATUS[sub.status] ?? "unknown",
4880
5135
  expiresAt: sub.expiresAt ?? /* @__PURE__ */ new Date(0),
4881
5136
  originalPurchaseDate: /* @__PURE__ */ new Date(0),
4882
5137
  latestPurchaseDate: /* @__PURE__ */ new Date(0),
@@ -4899,6 +5154,10 @@ function isSubscriptionStillActive(sub) {
4899
5154
  if (sub.expiresAt && sub.expiresAt.getTime() !== 0 && sub.expiresAt < /* @__PURE__ */ new Date()) return false;
4900
5155
  return true;
4901
5156
  }
5157
+ function subscriptionInfoToSub(s) {
5158
+ const status = DOMAIN_TO_PUBLIC_STATUS[s.status] ?? "expired";
5159
+ return { productId: s.productId, status, expiresAt: s.expiresAt ?? null, platform: s.platform, autoRenewEnabled: s.willRenew, inGracePeriod: status === "in_grace_period" };
5160
+ }
4902
5161
  async function resolveOfflineFromCache(distinctId) {
4903
5162
  try {
4904
5163
  const cached = await subscriptionCache.get(distinctId);
@@ -4939,24 +5198,12 @@ function useSubscriptionSync() {
4939
5198
  const distinctId = PaywalloClient.getDistinctId();
4940
5199
  if (!apiClient || !distinctId) return false;
4941
5200
  const cached = cacheRef.current;
4942
- if (cached && cached.expiresAt > Date.now()) {
4943
- return isActiveCacheEntry(cached);
4944
- }
5201
+ if (cached && cached.expiresAt > Date.now()) return isActiveCacheEntry(cached);
4945
5202
  try {
4946
5203
  const persisted = await subscriptionCache.get(distinctId);
4947
5204
  if (persisted && !persisted.isStale) {
4948
- const persistedSub = persisted.data.subscription;
4949
- const persistedStatus = persistedSub?.status;
4950
- const sub = persistedSub ? {
4951
- productId: persistedSub.productId,
4952
- status: persistedStatus,
4953
- expiresAt: persistedSub.expiresAt ?? null,
4954
- platform: persistedSub.platform,
4955
- autoRenewEnabled: persistedSub.willRenew,
4956
- inGracePeriod: persistedStatus === "in_grace_period"
4957
- } : null;
4958
- const stillActive = isSubscriptionStillActive(sub);
4959
- if (stillActive) {
5205
+ const sub = persisted.data.subscription ? subscriptionInfoToSub(persisted.data.subscription) : null;
5206
+ if (isSubscriptionStillActive(sub)) {
4960
5207
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4961
5208
  setSubscription(sub);
4962
5209
  return true;
@@ -4965,17 +5212,14 @@ function useSubscriptionSync() {
4965
5212
  } catch {
4966
5213
  }
4967
5214
  try {
4968
- const status = await apiClient.getSubscriptionStatus(distinctId);
4969
- const sub = status.subscription ? {
4970
- ...status.subscription,
4971
- expiresAt: status.subscription.expiresAt ? new Date(status.subscription.expiresAt) : null
4972
- } : null;
5215
+ const apiStatus = await apiClient.getSubscriptionStatus(distinctId);
5216
+ const sub = apiStatus.subscription ? { ...apiStatus.subscription, expiresAt: apiStatus.subscription.expiresAt ? new Date(apiStatus.subscription.expiresAt) : null } : null;
4973
5217
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4974
5218
  setSubscription(sub);
4975
- void subscriptionCache.set(distinctId, toDomainResponse(status));
4976
- return status.hasActiveSubscription === true;
5219
+ void subscriptionCache.set(distinctId, toDomainResponse(apiStatus));
5220
+ return apiStatus.hasActiveSubscription === true;
4977
5221
  } catch {
4978
- return await resolveOfflineFromCache(distinctId);
5222
+ return resolveOfflineFromCache(distinctId);
4979
5223
  }
4980
5224
  }, []);
4981
5225
  const getSubscription = (0, import_react17.useCallback)(async () => {
@@ -4990,16 +5234,7 @@ function useSubscriptionSync() {
4990
5234
  try {
4991
5235
  const persisted = await subscriptionCache.get(distinctId);
4992
5236
  if (!persisted?.data.subscription) return null;
4993
- const s = persisted.data.subscription;
4994
- const status = s.status;
4995
- return {
4996
- productId: s.productId,
4997
- status,
4998
- expiresAt: s.expiresAt ?? null,
4999
- platform: s.platform,
5000
- autoRenewEnabled: s.willRenew,
5001
- inGracePeriod: status === "in_grace_period"
5002
- };
5237
+ return subscriptionInfoToSub(persisted.data.subscription);
5003
5238
  } catch {
5004
5239
  return null;
5005
5240
  }
@@ -5021,6 +5256,8 @@ function PaywalloProvider({ children, config }) {
5021
5256
  const clearPreloadedWebView = (0, import_react18.useCallback)(() => setPreloadedWebView(null), [setPreloadedWebView]);
5022
5257
  const autoPreloadTriggeredRef = (0, import_react18.useRef)(false);
5023
5258
  const preloadedWebViewRef = (0, import_react18.useRef)(preloadedWebView);
5259
+ const pollTimerRef = (0, import_react18.useRef)(null);
5260
+ const pollCancelledRef = (0, import_react18.useRef)(false);
5024
5261
  (0, import_react18.useEffect)(() => {
5025
5262
  preloadedWebViewRef.current = preloadedWebView;
5026
5263
  }, [preloadedWebView]);
@@ -5029,21 +5266,21 @@ function PaywalloProvider({ children, config }) {
5029
5266
  });
5030
5267
  }, []);
5031
5268
  const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady: baseHandlePreloadedWebViewReady, handlePreloadedClose: baseHandlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, setPreloadedWebView);
5032
- const SCREEN_HEIGHT3 = (0, import_react18.useMemo)(() => import_react_native15.Dimensions.get("window").height, []);
5033
- const slideAnim = (0, import_react18.useRef)(new import_react_native15.Animated.Value(SCREEN_HEIGHT3)).current;
5269
+ const SCREEN_HEIGHT = (0, import_react18.useMemo)(() => import_react_native17.Dimensions.get("window").height, []);
5270
+ const slideAnim = (0, import_react18.useRef)(new import_react_native17.Animated.Value(SCREEN_HEIGHT)).current;
5034
5271
  const handlePreloadedWebViewReady = (0, import_react18.useCallback)(() => {
5035
5272
  if (PaywalloClient.getConfig()?.debug) {
5036
- console.info(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
5273
+ console.log(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
5037
5274
  }
5038
5275
  baseHandlePreloadedWebViewReady();
5039
- }, [config?.debug, preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
5276
+ }, [preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
5040
5277
  const handlePreloadedClose = (0, import_react18.useCallback)(() => {
5041
5278
  const placement = preloadedWebView?.placement;
5042
- import_react_native15.Animated.timing(slideAnim, { toValue: SCREEN_HEIGHT3, duration: 200, easing: import_react_native15.Easing.in(import_react_native15.Easing.cubic), useNativeDriver: true }).start(() => {
5279
+ import_react_native17.Animated.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 200, easing: import_react_native17.Easing.in(import_react_native17.Easing.cubic), useNativeDriver: true }).start(() => {
5043
5280
  baseHandlePreloadedClose();
5044
5281
  if (placement) triggerRepreload(placement);
5045
5282
  });
5046
- }, [baseHandlePreloadedClose, preloadedWebView, triggerRepreload, slideAnim, SCREEN_HEIGHT3]);
5283
+ }, [baseHandlePreloadedClose, preloadedWebView, triggerRepreload, slideAnim, SCREEN_HEIGHT]);
5047
5284
  const { handlePreloadedPurchase: rawPreloadedPurchase, handlePreloadedRestore } = usePurchaseOrchestrator(
5048
5285
  preloadedWebView,
5049
5286
  resolvePreloadedPaywall,
@@ -5061,21 +5298,28 @@ function PaywalloProvider({ children, config }) {
5061
5298
  setIsPreloadPurchasing(false);
5062
5299
  }
5063
5300
  }, [rawPreloadedPurchase]);
5301
+ const configRef = (0, import_react18.useRef)(config);
5064
5302
  (0, import_react18.useEffect)(() => {
5303
+ let mounted = true;
5065
5304
  initLocalization({ detectDevice: true });
5066
- PaywalloClient.init(config).then(async () => {
5305
+ PaywalloClient.init(configRef.current).then(async () => {
5306
+ if (!mounted) return;
5067
5307
  setDistinctId(PaywalloClient.getDistinctId());
5068
5308
  setIsInitialized(true);
5069
5309
  if (!autoPreloadTriggeredRef.current) {
5070
5310
  autoPreloadTriggeredRef.current = true;
5071
5311
  const placement = await PaywalloClient.waitForAutoPreloadedPlacement();
5072
- if (placement) void preloadCampaign(placement).catch(() => {
5312
+ if (mounted && placement) void preloadCampaign(placement).catch(() => {
5073
5313
  });
5074
5314
  }
5075
5315
  }).catch((err) => {
5316
+ if (!mounted) return;
5076
5317
  setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
5077
5318
  });
5078
- }, [config, preloadCampaign]);
5319
+ return () => {
5320
+ mounted = false;
5321
+ };
5322
+ }, [preloadCampaign]);
5079
5323
  const getPaywallConfig = (0, import_react18.useCallback)(async (p) => {
5080
5324
  const api = PaywalloClient.getApiClient();
5081
5325
  return api ? api.getPaywall(p) : null;
@@ -5176,15 +5420,17 @@ function PaywalloProvider({ children, config }) {
5176
5420
  }, [activePaywall]);
5177
5421
  const bridgePaywallPresenter = (0, import_react18.useCallback)((placement) => {
5178
5422
  if (PaywalloClient.getConfig()?.debug) {
5179
- console.info(`[Paywallo PRESENT] called \u2014 preloaded: ${preloadedWebViewRef.current?.loaded === true} placement: ${placement}`);
5423
+ console.log(`[Paywallo PRESENT] called \u2014 preloaded: ${preloadedWebViewRef.current?.loaded === true} placement: ${placement}`);
5180
5424
  }
5181
5425
  if (preloadedWebViewRef.current?.placement === placement) {
5182
5426
  if (preloadedWebViewRef.current.loaded) {
5183
5427
  return presentPreloadedWebView();
5184
5428
  }
5429
+ pollCancelledRef.current = false;
5185
5430
  return new Promise((resolve) => {
5186
5431
  let elapsed = 0;
5187
5432
  const poll = () => {
5433
+ if (pollCancelledRef.current) return;
5188
5434
  if (preloadedWebViewRef.current?.loaded) {
5189
5435
  resolve(presentPreloadedWebView());
5190
5436
  return;
@@ -5194,9 +5440,9 @@ function PaywalloProvider({ children, config }) {
5194
5440
  resolve(presentCampaign(placement));
5195
5441
  return;
5196
5442
  }
5197
- setTimeout(poll, 100);
5443
+ pollTimerRef.current = setTimeout(poll, 100);
5198
5444
  };
5199
- setTimeout(poll, 100);
5445
+ pollTimerRef.current = setTimeout(poll, 100);
5200
5446
  });
5201
5447
  }
5202
5448
  return presentCampaign(placement);
@@ -5214,6 +5460,18 @@ function PaywalloProvider({ children, config }) {
5214
5460
  PaywalloClient.registerActiveChecker(hasActiveSubscription);
5215
5461
  PaywalloClient.registerRestoreHandler(restorePurchases);
5216
5462
  PaywalloClient.registerEmergencyPaywallHandler(onEmergency);
5463
+ return () => {
5464
+ pollCancelledRef.current = true;
5465
+ if (pollTimerRef.current !== null) {
5466
+ clearTimeout(pollTimerRef.current);
5467
+ pollTimerRef.current = null;
5468
+ }
5469
+ PaywalloClient.registerPaywallPresenter(null);
5470
+ PaywalloClient.registerSubscriptionGetter(null);
5471
+ PaywalloClient.registerActiveChecker(null);
5472
+ PaywalloClient.registerRestoreHandler(null);
5473
+ PaywalloClient.registerEmergencyPaywallHandler(null);
5474
+ };
5217
5475
  }, [isInitialized, bridgePaywallPresenter, getSubscription, hasActiveSubscription, restorePurchases]);
5218
5476
  const isReady = isInitialized && !isLoadingProducts;
5219
5477
  const noOp = (0, import_react18.useCallback)(() => {
@@ -5254,10 +5512,10 @@ function PaywalloProvider({ children, config }) {
5254
5512
  ]);
5255
5513
  (0, import_react18.useLayoutEffect)(() => {
5256
5514
  if (showingPreloadedWebView) {
5257
- slideAnim.setValue(SCREEN_HEIGHT3);
5258
- import_react_native15.Animated.timing(slideAnim, { toValue: 0, duration: 200, easing: import_react_native15.Easing.out(import_react_native15.Easing.cubic), useNativeDriver: true }).start();
5515
+ slideAnim.setValue(SCREEN_HEIGHT);
5516
+ import_react_native17.Animated.timing(slideAnim, { toValue: 0, duration: 200, easing: import_react_native17.Easing.out(import_react_native17.Easing.cubic), useNativeDriver: true }).start();
5259
5517
  }
5260
- }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT3]);
5518
+ }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT]);
5261
5519
  const preloadStyle = [
5262
5520
  styles3.preloadOverlay,
5263
5521
  showingPreloadedWebView ? styles3.visible : styles3.hidden,
@@ -5265,7 +5523,7 @@ function PaywalloProvider({ children, config }) {
5265
5523
  ];
5266
5524
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PaywalloContext.Provider, { value: contextValue, children: [
5267
5525
  children,
5268
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PaywallErrorBoundary, { children: !!preloadedWebView?.craftData && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native15.Animated.View, { style: preloadStyle, pointerEvents: showingPreloadedWebView ? "auto" : "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5526
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PaywallErrorBoundary, { children: !!preloadedWebView?.craftData && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native17.Animated.View, { style: preloadStyle, pointerEvents: showingPreloadedWebView ? "auto" : "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5269
5527
  PaywallWebView,
5270
5528
  {
5271
5529
  paywallId: preloadedWebView.paywallId,
@@ -5295,7 +5553,7 @@ function PaywalloProvider({ children, config }) {
5295
5553
  ) })
5296
5554
  ] });
5297
5555
  }
5298
- var styles3 = import_react_native15.StyleSheet.create({
5556
+ var styles3 = import_react_native17.StyleSheet.create({
5299
5557
  preloadOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#ffffff" },
5300
5558
  hidden: { zIndex: -1, opacity: 0, width: 1, height: 1, overflow: "hidden" },
5301
5559
  visible: { zIndex: 9999, opacity: 1 }