@virex-tech/paywallo-sdk 1.4.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -63,6 +63,7 @@ var CLIENT_ERROR_CODES = {
63
63
  MISSING_APP_KEY: "CLIENT_MISSING_APP_KEY",
64
64
  INVALID_EVENT_NAME: "CLIENT_INVALID_EVENT_NAME",
65
65
  PROVIDER_MISSING: "CLIENT_PROVIDER_MISSING",
66
+ INSECURE_REQUEST: "CLIENT_INSECURE_REQUEST",
66
67
  UNKNOWN: "CLIENT_UNKNOWN"
67
68
  };
68
69
 
@@ -129,10 +130,7 @@ function createPurchaseError(code, message) {
129
130
  }
130
131
 
131
132
  // src/domains/iap/NativeStoreKit.ts
132
- import { NativeModules, Platform } from "react-native";
133
-
134
- // src/domains/identity/IdentityManager.ts
135
- import DeviceInfo from "react-native-device-info";
133
+ import { NativeModules as NativeModules3, Platform } from "react-native";
136
134
 
137
135
  // src/domains/identity/IdentityError.ts
138
136
  var IdentityError = class extends PaywalloError {
@@ -152,13 +150,141 @@ var IDENTITY_ERROR_CODES = {
152
150
  // src/domains/identity/IdentityManager.ts
153
151
  init_uuid();
154
152
 
153
+ // src/utils/NativeDeviceInfo.ts
154
+ import { NativeModules } from "react-native";
155
+ var FALLBACK = {
156
+ deviceId: "unknown",
157
+ model: "unknown",
158
+ modelId: "unknown",
159
+ systemVersion: "0.0.0",
160
+ appVersion: "0.0.0",
161
+ buildNumber: "0",
162
+ bundleId: "unknown",
163
+ brand: "unknown",
164
+ systemName: "unknown",
165
+ totalDisk: 0,
166
+ freeDisk: 0,
167
+ totalRam: 0,
168
+ carrier: "unknown",
169
+ installerPackage: null,
170
+ idfv: null
171
+ };
172
+ var _debug = false;
173
+ var warnedOnce = false;
174
+ var cachedDeviceInfo = null;
175
+ function setNativeDeviceInfoDebug(debug) {
176
+ _debug = debug;
177
+ }
178
+ function isDeviceInfoResult(value) {
179
+ if (typeof value !== "object" || value === null) return false;
180
+ const v = value;
181
+ return typeof v["deviceId"] === "string" && typeof v["model"] === "string" && typeof v["systemVersion"] === "string" && typeof v["appVersion"] === "string" && typeof v["bundleId"] === "string";
182
+ }
183
+ function getNativeModule() {
184
+ const mod = NativeModules.PaywalloDevice;
185
+ if (!mod) {
186
+ if (!warnedOnce) {
187
+ warnedOnce = true;
188
+ console.warn("[Paywallo] NativeModules.PaywalloDevice not found \u2014 falling back to defaults");
189
+ }
190
+ return null;
191
+ }
192
+ return mod;
193
+ }
194
+ async function getDeviceInfo() {
195
+ if (cachedDeviceInfo !== null) return cachedDeviceInfo;
196
+ const mod = getNativeModule();
197
+ if (!mod) return FALLBACK;
198
+ const raw = await mod.getDeviceInfo();
199
+ if (!isDeviceInfoResult(raw)) return FALLBACK;
200
+ cachedDeviceInfo = raw;
201
+ return raw;
202
+ }
203
+ async function getInstallReferrer() {
204
+ const mod = getNativeModule();
205
+ if (!mod) return null;
206
+ try {
207
+ return await mod.getInstallReferrer();
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+ function isAvailable() {
213
+ return NativeModules.PaywalloDevice != null;
214
+ }
215
+ function clearCache() {
216
+ cachedDeviceInfo = null;
217
+ }
218
+ var nativeDeviceInfo = {
219
+ getDeviceInfo,
220
+ getInstallReferrer,
221
+ isAvailable,
222
+ clearCache
223
+ };
224
+
225
+ // src/core/storage/NativeStorage.ts
226
+ import { NativeModules as NativeModules2 } from "react-native";
227
+ var NativeModule = NativeModules2.PaywalloStorage;
228
+ var _debug2 = false;
229
+ var _warnedUnavailable = false;
230
+ function setNativeStorageDebug(debug) {
231
+ _debug2 = debug;
232
+ }
233
+ function isAvailable2() {
234
+ return NativeModule != null;
235
+ }
236
+ function warnIfUnavailable() {
237
+ if (NativeModule != null) return true;
238
+ if (!_warnedUnavailable) {
239
+ _warnedUnavailable = true;
240
+ if (_debug2) console.warn("[Paywallo] PaywalloStorage native module not available \u2014 storage will not persist");
241
+ }
242
+ return false;
243
+ }
244
+ function getModule() {
245
+ if (!warnIfUnavailable()) return null;
246
+ return NativeModule;
247
+ }
248
+ async function get(key) {
249
+ return getModule()?.get(key) ?? null;
250
+ }
251
+ async function set(key, value) {
252
+ return getModule()?.set(key, value) ?? false;
253
+ }
254
+ async function remove(key) {
255
+ return getModule()?.remove(key) ?? false;
256
+ }
257
+ async function secureGet(key) {
258
+ return getModule()?.secureGet(key) ?? null;
259
+ }
260
+ async function secureSet(key, value) {
261
+ return getModule()?.secureSet(key, value) ?? false;
262
+ }
263
+ async function secureRemove(key) {
264
+ return getModule()?.secureRemove(key) ?? false;
265
+ }
266
+ async function legacySecureGet(key) {
267
+ const mod = getModule();
268
+ if (!mod?.legacySecureGet) return null;
269
+ return mod.legacySecureGet(key);
270
+ }
271
+ var nativeStorage = {
272
+ isAvailable: isAvailable2,
273
+ get,
274
+ set,
275
+ remove,
276
+ secureGet,
277
+ secureSet,
278
+ secureRemove,
279
+ legacySecureGet
280
+ };
281
+
155
282
  // src/core/storage/SecureStorage.ts
156
- import AsyncStorage from "@react-native-async-storage/async-storage";
157
- import Keychain from "react-native-keychain";
158
- var KEYCHAIN_SERVICE_PREFIX = "com.paywallo.sdk.";
283
+ var KEYCHAIN_KEY_PREFIX = "com.paywallo.sdk.";
284
+ var REGULAR_KEY_PREFIX = "@paywallo:";
159
285
  var SecureStorage = class {
160
286
  // eslint-disable-next-line @typescript-eslint/no-useless-constructor
161
- constructor(_debug = false) {
287
+ constructor(_debug3 = false) {
162
288
  }
163
289
  async get(key) {
164
290
  const keychainValue = await this.getFromKeychain(key);
@@ -166,9 +292,10 @@ var SecureStorage = class {
166
292
  return keychainValue;
167
293
  }
168
294
  try {
169
- const value = await AsyncStorage.getItem(`@paywallo:${key}`);
295
+ const value = await nativeStorage.get(`${REGULAR_KEY_PREFIX}${key}`);
170
296
  if (value) {
171
297
  await this.setToKeychain(key, value);
298
+ nativeStorage.remove(`${REGULAR_KEY_PREFIX}${key}`).catch(() => void 0);
172
299
  }
173
300
  return value;
174
301
  } catch {
@@ -176,20 +303,13 @@ var SecureStorage = class {
176
303
  }
177
304
  }
178
305
  async set(key, value) {
179
- try {
180
- await Promise.all([
181
- AsyncStorage.setItem(`@paywallo:${key}`, value),
182
- this.setToKeychain(key, value)
183
- ]);
184
- return true;
185
- } catch {
186
- return false;
187
- }
306
+ return this.setToKeychain(key, value);
188
307
  }
189
308
  async remove(key) {
190
309
  try {
191
310
  await Promise.all([
192
- AsyncStorage.removeItem(`@paywallo:${key}`),
311
+ // Also remove the legacy plaintext copy if it exists (migration cleanup).
312
+ nativeStorage.remove(`${REGULAR_KEY_PREFIX}${key}`).catch(() => void 0),
193
313
  this.removeFromKeychain(key)
194
314
  ]);
195
315
  return true;
@@ -198,31 +318,25 @@ var SecureStorage = class {
198
318
  }
199
319
  }
200
320
  hasKeychainSupport() {
201
- return true;
321
+ return nativeStorage.isAvailable();
202
322
  }
203
323
  async getFromKeychain(key) {
204
324
  try {
205
- const result = await Keychain.getGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
206
- if (result && result.password) {
207
- return result.password;
208
- }
209
- return null;
325
+ return await nativeStorage.secureGet(`${KEYCHAIN_KEY_PREFIX}${key}`);
210
326
  } catch {
211
327
  return null;
212
328
  }
213
329
  }
214
330
  async setToKeychain(key, value) {
215
331
  try {
216
- await Keychain.setGenericPassword("panel", value, { service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
217
- return true;
332
+ return await nativeStorage.secureSet(`${KEYCHAIN_KEY_PREFIX}${key}`, value);
218
333
  } catch {
219
334
  return false;
220
335
  }
221
336
  }
222
337
  async removeFromKeychain(key) {
223
338
  try {
224
- await Keychain.resetGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
225
- return true;
339
+ return await nativeStorage.secureRemove(`${KEYCHAIN_KEY_PREFIX}${key}`);
226
340
  } catch {
227
341
  return false;
228
342
  }
@@ -230,7 +344,59 @@ var SecureStorage = class {
230
344
  };
231
345
  var secureStorage = new SecureStorage();
232
346
 
347
+ // src/core/storage/StorageMigration.ts
348
+ var MIGRATION_FLAG_KEY = "@paywallo:_migration_v152_done";
349
+ var LEGACY_SECURE_KEYS = [
350
+ "device_id",
351
+ "device_id_fallback",
352
+ "anon_id",
353
+ "user_email",
354
+ "user_properties"
355
+ ];
356
+ var KEYCHAIN_KEY_PREFIX2 = "com.paywallo.sdk.";
357
+ async function runStorageMigrationIfNeeded() {
358
+ if (!nativeStorage.isAvailable()) return;
359
+ const alreadyDone = await nativeStorage.get(MIGRATION_FLAG_KEY);
360
+ if (alreadyDone === "1") return;
361
+ await migrateAsyncStorage();
362
+ await migrateKeychainLegacy();
363
+ await nativeStorage.set(MIGRATION_FLAG_KEY, "1");
364
+ }
365
+ async function migrateAsyncStorage() {
366
+ try {
367
+ const mod = await import("@react-native-async-storage/async-storage");
368
+ const AsyncStorage = mod.default;
369
+ const allKeys = await AsyncStorage.getAllKeys();
370
+ const paywallo = allKeys.filter((k) => k.startsWith("@paywallo:"));
371
+ if (paywallo.length === 0) return;
372
+ const pairs = await AsyncStorage.multiGet(paywallo);
373
+ for (const [key, value] of pairs) {
374
+ if (!value) continue;
375
+ const existing = await nativeStorage.get(key);
376
+ if (!existing) {
377
+ await nativeStorage.set(key, value);
378
+ }
379
+ }
380
+ } catch {
381
+ }
382
+ }
383
+ async function migrateKeychainLegacy() {
384
+ for (const key of LEGACY_SECURE_KEYS) {
385
+ try {
386
+ const legacyValue = await nativeStorage.legacySecureGet(`${KEYCHAIN_KEY_PREFIX2}${key}`);
387
+ if (!legacyValue) continue;
388
+ const newSlotKey = `${KEYCHAIN_KEY_PREFIX2}${key}`;
389
+ const existing = await nativeStorage.secureGet(newSlotKey);
390
+ if (!existing) {
391
+ await nativeStorage.secureSet(newSlotKey, legacyValue);
392
+ }
393
+ } catch {
394
+ }
395
+ }
396
+ }
397
+
233
398
  // src/domains/identity/IdentityManager.ts
399
+ var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
234
400
  var DEVICE_ID_KEY = "device_id";
235
401
  var ANON_ID_KEY = "anon_id";
236
402
  var USER_EMAIL_KEY = "user_email";
@@ -245,18 +411,31 @@ var IdentityManager = class {
245
411
  this.secureStorage = null;
246
412
  this.debug = false;
247
413
  this.initialized = false;
414
+ this.initPromise = null;
248
415
  }
249
416
  async initialize(apiClient, debug = false) {
250
417
  if (this.initialized) {
251
418
  return this.deviceId ?? "";
252
419
  }
420
+ if (this.initPromise) {
421
+ return this.initPromise;
422
+ }
423
+ this.initPromise = this._doInitialize(apiClient, debug).finally(() => {
424
+ this.initPromise = null;
425
+ });
426
+ return this.initPromise;
427
+ }
428
+ async _doInitialize(apiClient, debug) {
253
429
  this.apiClient = apiClient;
254
430
  this.debug = debug;
255
431
  this.secureStorage = new SecureStorage(debug);
432
+ await runStorageMigrationIfNeeded();
256
433
  await this.loadPersistedState();
257
434
  if (!this.deviceId) {
258
435
  try {
259
- this.deviceId = await DeviceInfo.getUniqueId();
436
+ const info = await nativeDeviceInfo.getDeviceInfo();
437
+ const deviceId = info.deviceId;
438
+ this.deviceId = !deviceId || deviceId === "unknown" ? generateUUID() : deviceId;
260
439
  } catch {
261
440
  this.deviceId = generateUUID();
262
441
  }
@@ -287,8 +466,13 @@ var IdentityManager = class {
287
466
  }
288
467
  }
289
468
  if (email) {
290
- this.email = email;
291
- await this.secureStorage.set(USER_EMAIL_KEY, email);
469
+ if (!EMAIL_REGEX.test(email)) {
470
+ if (this.debug) console.warn("[Paywallo] Invalid email format, skipping email field");
471
+ email = void 0;
472
+ } else {
473
+ this.email = email;
474
+ await this.secureStorage.set(USER_EMAIL_KEY, email);
475
+ }
292
476
  }
293
477
  if (properties) {
294
478
  this.properties = { ...this.properties, ...properties };
@@ -392,8 +576,8 @@ var IdentityManager = class {
392
576
  var identityManager = new IdentityManager();
393
577
 
394
578
  // src/domains/iap/NativeStoreKit.ts
395
- var PaywalloStoreKitNative = NativeModules.PaywalloStoreKit ?? null;
396
- function isAvailable() {
579
+ var PaywalloStoreKitNative = NativeModules3.PaywalloStoreKit ?? null;
580
+ function isAvailable3() {
397
581
  return PaywalloStoreKitNative != null;
398
582
  }
399
583
  function mapNativeProduct(native) {
@@ -469,7 +653,7 @@ function mapNativePurchase(native) {
469
653
  };
470
654
  }
471
655
  var nativeStoreKit = {
472
- isAvailable,
656
+ isAvailable: isAvailable3,
473
657
  async getProducts(productIds) {
474
658
  if (!PaywalloStoreKitNative) {
475
659
  return [];
@@ -486,8 +670,8 @@ var nativeStoreKit = {
486
670
  }
487
671
  const distinctId = identityManager.getDistinctId();
488
672
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
489
- if (distinctId && !uuidRegex.test(distinctId) && PaywalloClient.getConfig()?.debug) {
490
- console.warn("[Paywallo] distinctId is not a valid UUID for appAccountToken:", distinctId);
673
+ if (distinctId && !uuidRegex.test(distinctId)) {
674
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] distinctId is not a valid UUID for appAccountToken:", distinctId);
491
675
  }
492
676
  try {
493
677
  const result = await PaywalloStoreKitNative.purchase(productId, distinctId ?? null);
@@ -508,7 +692,7 @@ var nativeStoreKit = {
508
692
  try {
509
693
  await PaywalloStoreKitNative.finishTransaction(transactionId);
510
694
  } catch (err) {
511
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction native error", err);
695
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] finishTransaction native error", err);
512
696
  }
513
697
  },
514
698
  async getActiveTransactions() {
@@ -517,7 +701,7 @@ var nativeStoreKit = {
517
701
  const results = await PaywalloStoreKitNative.getActiveTransactions();
518
702
  return results.map(mapNativePurchase);
519
703
  } catch (err) {
520
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] getActiveTransactions failed", err);
704
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] getActiveTransactions failed", err);
521
705
  return [];
522
706
  }
523
707
  }
@@ -528,6 +712,7 @@ var IAPService = class {
528
712
  constructor(apiClient) {
529
713
  this.productsCache = /* @__PURE__ */ new Map();
530
714
  this.apiClient = null;
715
+ this.purchaseInFlight = null;
531
716
  this.apiClient = apiClient ?? null;
532
717
  }
533
718
  get debug() {
@@ -563,7 +748,7 @@ var IAPService = class {
563
748
  }
564
749
  return products;
565
750
  } catch (err) {
566
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] loadProducts failed", err);
751
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] loadProducts failed", err);
567
752
  return [];
568
753
  }
569
754
  }
@@ -609,7 +794,7 @@ var IAPService = class {
609
794
  try {
610
795
  await nativeStoreKit.finishTransaction(transactionId);
611
796
  } catch (err) {
612
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction failed", err);
797
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] finishTransaction failed", err);
613
798
  }
614
799
  }
615
800
  async purchase(productId, options) {
@@ -619,27 +804,36 @@ var IAPService = class {
619
804
  error: new PurchaseError(PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE, "In-app purchases are not available on this device")
620
805
  };
621
806
  }
807
+ if (this.purchaseInFlight) {
808
+ return {
809
+ success: false,
810
+ error: new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, "A purchase is already in progress")
811
+ };
812
+ }
813
+ this.purchaseInFlight = productId;
622
814
  const debug = PaywalloClient.getConfig()?.debug;
623
- if (debug) console.info(`[Paywallo PURCHASE] starting ${productId}`);
815
+ if (debug) console.log(`[Paywallo PURCHASE] starting ${productId}`);
624
816
  try {
625
817
  const purchase = await nativeStoreKit.purchase(productId);
626
818
  if (!purchase) {
627
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} cancelled`);
819
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} cancelled`);
628
820
  return { success: false };
629
821
  }
630
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} approved`, { transactionId: purchase.transactionId });
822
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} approved`, { transactionId: purchase.transactionId });
631
823
  await this.finishTransaction(purchase.transactionId);
632
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} finished`);
824
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} finished`);
633
825
  this.validateWithServer(purchase, productId, options).then(() => {
634
- if (debug) console.info(`[Paywallo PURCHASE] ${productId} server validated`);
826
+ if (debug) console.log(`[Paywallo PURCHASE] ${productId} server validated`);
635
827
  }).catch((err) => {
636
- if (debug) console.warn("[Paywallo PURCHASE] background validation failed", err);
828
+ if (debug) console.error("[Paywallo PURCHASE] background validation failed", err);
637
829
  });
638
830
  return { success: true, purchase };
639
831
  } catch (error) {
640
- if (debug) console.warn(`[Paywallo PURCHASE] ${productId} failed`, error);
832
+ if (debug) console.error(`[Paywallo PURCHASE] ${productId} failed`, error);
641
833
  const e = error instanceof PurchaseError ? error : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, error instanceof Error ? error.message : String(error));
642
834
  return { success: false, error: e };
835
+ } finally {
836
+ this.purchaseInFlight = null;
643
837
  }
644
838
  }
645
839
  async restore() {
@@ -674,14 +868,14 @@ var IAPService = class {
674
868
  try {
675
869
  await nativeStoreKit.finishTransaction(tx.transactionId);
676
870
  } catch (err) {
677
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction failed", err);
871
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] finishTransaction failed", err);
678
872
  }
679
873
  validated.push(tx);
680
874
  }
681
875
  }
682
876
  return validated;
683
877
  } catch (err) {
684
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] restore failed", err);
878
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] restore failed", err);
685
879
  return [];
686
880
  }
687
881
  }
@@ -712,7 +906,7 @@ function usePaywallTracking() {
712
906
  ...sessionId && { sessionId }
713
907
  });
714
908
  } catch (err) {
715
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackDismissed failed", err);
909
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] trackDismissed failed", err);
716
910
  }
717
911
  }, []);
718
912
  const trackProductSelected = useCallback((opts) => {
@@ -727,7 +921,7 @@ function usePaywallTracking() {
727
921
  ...opts.campaignId && { campaignId: opts.campaignId },
728
922
  ...sessionId && { sessionId }
729
923
  }).catch((err) => {
730
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackProductSelected failed", err);
924
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] trackProductSelected failed", err);
731
925
  });
732
926
  }, []);
733
927
  const trackPurchased = useCallback(async (opts) => {
@@ -744,14 +938,13 @@ function usePaywallTracking() {
744
938
  ...sessionId && { sessionId }
745
939
  });
746
940
  } catch (err) {
747
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackPurchased failed", err);
941
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] trackPurchased failed", err);
748
942
  }
749
943
  }, []);
750
944
  return { trackDismissed, trackProductSelected, trackPurchased };
751
945
  }
752
946
 
753
947
  // src/domains/paywall/usePaywallActions.ts
754
- var SCREEN_HEIGHT = Dimensions.get("window").height;
755
948
  function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId, openAnim) {
756
949
  const [isPurchasing, setIsPurchasing] = useState(false);
757
950
  const [isClosing, setIsClosing] = useState(false);
@@ -762,7 +955,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
762
955
  if (isClosing) return;
763
956
  setIsClosing(true);
764
957
  const animTarget = openAnim ?? slideAnim;
765
- Animated.timing(animTarget, { toValue: SCREEN_HEIGHT, duration: 350, useNativeDriver: true }).start(() => {
958
+ Animated.timing(animTarget, { toValue: Dimensions.get("window").height, duration: 350, useNativeDriver: true }).start(() => {
766
959
  if (paywallConfig) {
767
960
  const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
768
961
  void trackDismissed({ placement, paywallId: paywallConfig.id, durationSeconds, action: "close", variantKey, campaignId });
@@ -851,6 +1044,7 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
851
1044
  trackingDoneRef.current = false;
852
1045
  return;
853
1046
  }
1047
+ let cancelled = false;
854
1048
  const load = async () => {
855
1049
  setError(null);
856
1050
  try {
@@ -871,27 +1065,30 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
871
1065
  if (!fetched) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_FOUND, `Paywall not found for placement: ${placement}`);
872
1066
  config = fetched;
873
1067
  }
1068
+ if (cancelled) return;
874
1069
  setPaywallConfig(config);
875
1070
  const productIds = [];
876
1071
  if (config.primaryProductId) productIds.push(config.primaryProductId);
877
1072
  if (config.secondaryProductId) productIds.push(config.secondaryProductId);
878
1073
  const snapProducts = preloadedProductsRef.current;
879
1074
  if (snapProducts && snapProducts.size > 0) {
880
- setProducts(snapProducts);
1075
+ if (!cancelled) setProducts(snapProducts);
881
1076
  } else if (productIds.length > 0) {
882
1077
  try {
883
1078
  const storeProducts = await getIAPService().loadProducts(productIds);
884
- const map = /* @__PURE__ */ new Map();
885
- for (const p of storeProducts) map.set(p.productId, p);
886
- setProducts(map);
1079
+ if (!cancelled) {
1080
+ const map = /* @__PURE__ */ new Map();
1081
+ for (const p of storeProducts) map.set(p.productId, p);
1082
+ setProducts(map);
1083
+ }
887
1084
  } catch (err) {
888
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] loadProducts in paywall failed", err);
889
- setProducts(/* @__PURE__ */ new Map());
1085
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] loadProducts in paywall failed", err);
1086
+ if (!cancelled) setProducts(/* @__PURE__ */ new Map());
890
1087
  }
891
1088
  } else {
892
- setProducts(/* @__PURE__ */ new Map());
1089
+ if (!cancelled) setProducts(/* @__PURE__ */ new Map());
893
1090
  }
894
- if (!trackingDoneRef.current) {
1091
+ if (!trackingDoneRef.current && !cancelled) {
895
1092
  trackingDoneRef.current = true;
896
1093
  const sessionId = PaywalloClient.getSessionId();
897
1094
  await apiClient.trackEvent("$paywall_viewed", PaywalloClient.getDistinctId(), {
@@ -903,17 +1100,21 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
903
1100
  });
904
1101
  }
905
1102
  } catch (err) {
1103
+ if (cancelled) return;
906
1104
  const e = err instanceof Error ? err : new PaywallError(PAYWALL_ERROR_CODES.LOAD_FAILED, String(err));
907
1105
  setError(e);
908
1106
  }
909
1107
  };
910
1108
  void load();
1109
+ return () => {
1110
+ cancelled = true;
1111
+ };
911
1112
  }, [placement, visible, variantKey, campaignId]);
912
1113
  return { paywallConfig, products, error };
913
1114
  }
914
1115
 
915
1116
  // src/utils/localization.ts
916
- import { NativeModules as NativeModules2, Platform as Platform3 } from "react-native";
1117
+ import { NativeModules as NativeModules4, Platform as Platform3 } from "react-native";
917
1118
  var currentLanguage = "pt-BR";
918
1119
  var defaultLanguage = "pt-BR";
919
1120
  function setCurrentLanguage(language) {
@@ -936,10 +1137,10 @@ function getLocalizedString(text) {
936
1137
  function detectDeviceLanguage() {
937
1138
  try {
938
1139
  if (Platform3.OS === "ios") {
939
- const settings = NativeModules2.SettingsManager?.settings;
1140
+ const settings = NativeModules4.SettingsManager?.settings;
940
1141
  return settings?.AppleLocale ?? settings?.AppleLanguages?.[0] ?? "en-US";
941
1142
  }
942
- return NativeModules2.I18nManager?.localeIdentifier ?? "en-US";
1143
+ return NativeModules4.I18nManager?.localeIdentifier ?? "en-US";
943
1144
  } catch {
944
1145
  return "en-US";
945
1146
  }
@@ -975,9 +1176,8 @@ var SDK_STRINGS = {
975
1176
  };
976
1177
  function getSdkString(key) {
977
1178
  const map = SDK_STRINGS[key];
978
- const lang = currentLanguage;
979
- if (lang in map) return map[lang];
980
- const base = lang.split("-")[0];
1179
+ if (currentLanguage in map) return map[currentLanguage];
1180
+ const base = currentLanguage.split("-")[0];
981
1181
  if (base && base in map) return map[base];
982
1182
  if (defaultLanguage in map) return map[defaultLanguage];
983
1183
  return Object.values(map)[0];
@@ -997,13 +1197,21 @@ import { useCallback as useCallback3, useEffect as useEffect2, useMemo, useRef a
997
1197
  import {
998
1198
  Linking,
999
1199
  NativeEventEmitter,
1000
- NativeModules as NativeModules3,
1200
+ NativeModules as NativeModules5,
1001
1201
  requireNativeComponent,
1002
1202
  StyleSheet,
1003
1203
  View
1004
1204
  } from "react-native";
1005
1205
 
1006
1206
  // src/domains/paywall/parseWebViewMessage.ts
1207
+ var ALLOWED_MESSAGE_TYPES = /* @__PURE__ */ new Set([
1208
+ "purchase",
1209
+ "close",
1210
+ "restore",
1211
+ "select-product",
1212
+ "open-url",
1213
+ "ready"
1214
+ ]);
1007
1215
  function deriveMessageId(message) {
1008
1216
  if (message.id) return message.id;
1009
1217
  const payloadKey = message.payload?.productId ?? message.payload?.url ?? (message.payload?.timestamp != null ? String(message.payload.timestamp) : "");
@@ -1012,10 +1220,20 @@ function deriveMessageId(message) {
1012
1220
  function parseWebViewMessage(raw) {
1013
1221
  try {
1014
1222
  const parsed = JSON.parse(raw);
1015
- if (typeof parsed?.type !== "string") {
1223
+ if (typeof parsed?.type !== "string" || !ALLOWED_MESSAGE_TYPES.has(parsed.type)) {
1016
1224
  return null;
1017
1225
  }
1018
- return parsed;
1226
+ const payload = parsed.payload;
1227
+ const safePayload = payload !== null && typeof payload === "object" ? {
1228
+ productId: typeof payload.productId === "string" ? String(payload.productId) : void 0,
1229
+ url: typeof payload.url === "string" ? String(payload.url) : void 0,
1230
+ timestamp: typeof payload.timestamp === "number" ? Number(payload.timestamp) : void 0
1231
+ } : void 0;
1232
+ return {
1233
+ type: parsed.type,
1234
+ id: typeof parsed.id === "string" ? parsed.id : void 0,
1235
+ payload: safePayload
1236
+ };
1019
1237
  } catch {
1020
1238
  return null;
1021
1239
  }
@@ -1050,7 +1268,7 @@ function getNativeWebView() {
1050
1268
  var webViewEmitter = null;
1051
1269
  function getWebViewEmitter() {
1052
1270
  if (!webViewEmitter) {
1053
- webViewEmitter = new NativeEventEmitter(NativeModules3.PaywalloWebViewModule);
1271
+ webViewEmitter = new NativeEventEmitter(NativeModules5.PaywalloWebViewModule);
1054
1272
  }
1055
1273
  return webViewEmitter;
1056
1274
  }
@@ -1125,12 +1343,24 @@ function PaywallWebView({
1125
1343
  setScriptToInject(script);
1126
1344
  }, [craftData, products, primaryProductId, secondaryProductId]);
1127
1345
  const processedIdsRef = useRef3(/* @__PURE__ */ new Set());
1346
+ const dedupeTimersRef = useRef3(/* @__PURE__ */ new Map());
1347
+ useEffect2(() => {
1348
+ return () => {
1349
+ for (const timerId of dedupeTimersRef.current.values()) clearTimeout(timerId);
1350
+ dedupeTimersRef.current.clear();
1351
+ processedIdsRef.current.clear();
1352
+ };
1353
+ }, []);
1128
1354
  const handleParsedMessage = useCallback3(
1129
1355
  (message) => {
1130
1356
  const msgId = deriveMessageId(message);
1131
1357
  if (processedIdsRef.current.has(msgId)) return;
1132
1358
  processedIdsRef.current.add(msgId);
1133
- setTimeout(() => processedIdsRef.current.delete(msgId), 5e3);
1359
+ const timerId = setTimeout(() => {
1360
+ processedIdsRef.current.delete(msgId);
1361
+ dedupeTimersRef.current.delete(msgId);
1362
+ }, 5e3);
1363
+ dedupeTimersRef.current.set(msgId, timerId);
1134
1364
  switch (message.type) {
1135
1365
  case "ready":
1136
1366
  if (!paywallDataSent.current) {
@@ -1151,7 +1381,7 @@ function PaywallWebView({
1151
1381
  case "open-url":
1152
1382
  if (message.payload?.url) {
1153
1383
  const url = message.payload.url;
1154
- if (/^https?:\/\//i.test(url) || /^mailto:/i.test(url)) {
1384
+ if (/^https?:\/\//i.test(url) || /^mailto:/i.test(url) || /^itms-apps:/i.test(url) || /^itms-services:/i.test(url) || /^market:/i.test(url)) {
1155
1385
  void Linking.openURL(url);
1156
1386
  }
1157
1387
  }
@@ -1249,7 +1479,6 @@ var styles = StyleSheet.create({
1249
1479
 
1250
1480
  // src/domains/paywall/PaywallModal.tsx
1251
1481
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1252
- var SCREEN_HEIGHT2 = Dimensions2.get("window").height;
1253
1482
  function ErrorFallback({ description, onRetry, onClose }) {
1254
1483
  const overrides = PaywalloClient.getConfig()?.errorStrings;
1255
1484
  const title = overrides?.title ?? getSdkString("paywallErrorTitle");
@@ -1279,7 +1508,8 @@ function PaywallModal({
1279
1508
  variantKey,
1280
1509
  campaignId
1281
1510
  );
1282
- const openAnim = useRef4(new Animated2.Value(SCREEN_HEIGHT2)).current;
1511
+ const screenHeight = Dimensions2.get("window").height;
1512
+ const openAnim = useRef4(new Animated2.Value(screenHeight)).current;
1283
1513
  const [modalVisible, setModalVisible] = useState4(false);
1284
1514
  const handleResult = useCallback4((result) => {
1285
1515
  setModalVisible(false);
@@ -1308,7 +1538,7 @@ function PaywallModal({
1308
1538
  useEffect3(() => {
1309
1539
  if (visible) {
1310
1540
  setModalVisible(true);
1311
- openAnim.setValue(SCREEN_HEIGHT2);
1541
+ openAnim.setValue(Dimensions2.get("window").height);
1312
1542
  Animated2.timing(openAnim, {
1313
1543
  toValue: 0,
1314
1544
  duration: 550,
@@ -1458,6 +1688,7 @@ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
1458
1688
  resolverRef.current({ presented: false, purchased: false, cancelled: true, restored: false });
1459
1689
  resolverRef.current = null;
1460
1690
  }
1691
+ setShowingPreloadedWebView(false);
1461
1692
  };
1462
1693
  }, []);
1463
1694
  const presentPreloadedWebView = useCallback5(() => {
@@ -1502,230 +1733,10 @@ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
1502
1733
  return { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef };
1503
1734
  }
1504
1735
 
1505
- // src/domains/affiliate/AffiliateError.ts
1506
- var AffiliateError = class extends PaywalloError {
1507
- constructor(code, message) {
1508
- super("affiliate", code, message);
1509
- this.name = "AffiliateError";
1510
- }
1511
- };
1512
- var AFFILIATE_ERROR_CODES = {
1513
- NOT_INITIALIZED: "AFFILIATE_NOT_INITIALIZED",
1514
- COUPON_REGISTER_FAILED: "AFFILIATE_COUPON_REGISTER_FAILED",
1515
- COUPON_APPLY_FAILED: "AFFILIATE_COUPON_APPLY_FAILED",
1516
- BALANCE_FETCH_FAILED: "AFFILIATE_BALANCE_FETCH_FAILED",
1517
- WITHDRAWAL_FAILED: "AFFILIATE_WITHDRAWAL_FAILED",
1518
- NETWORK_ERROR: "AFFILIATE_NETWORK_ERROR"
1519
- };
1520
-
1521
- // src/domains/affiliate/affiliateHttp.ts
1522
- function buildQueryString(params) {
1523
- if (!params) return "";
1524
- const entries = Object.entries(params).filter(([, v]) => v !== void 0 && v !== null);
1525
- if (entries.length === 0) return "";
1526
- return `?${entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&")}`;
1527
- }
1528
- async function affiliateGet(apiClient, path, params) {
1529
- const response = await apiClient.get(`${path}${buildQueryString(params)}`);
1530
- return response.data;
1531
- }
1532
- async function affiliatePost(apiClient, path, body) {
1533
- const response = await apiClient.post(path, body);
1534
- return response.data;
1535
- }
1536
-
1537
- // src/domains/affiliate/affiliateDateMappers.ts
1538
- function mapRawCoupon(raw) {
1539
- return { code: raw.code, status: raw.status, createdAt: new Date(raw.createdAt) };
1540
- }
1541
- function mapRawCommission(raw) {
1542
- return { ...raw, createdAt: new Date(raw.createdAt) };
1543
- }
1544
- function mapRawWithdrawal(raw) {
1545
- return { ...raw, createdAt: new Date(raw.createdAt) };
1546
- }
1547
-
1548
- // src/domains/affiliate/AffiliateManager.ts
1549
- var AffiliateManager = class {
1550
- constructor() {
1551
- this.apiClient = null;
1552
- this.debug = false;
1553
- }
1554
- initialize(apiClient, debug = false) {
1555
- this.apiClient = apiClient;
1556
- this.debug = debug;
1557
- this.log("AffiliateManager initialized");
1558
- }
1559
- async registerCoupon(code) {
1560
- this.ensureInitialized();
1561
- const distinctId = identityManager.getDistinctId();
1562
- try {
1563
- const response = await this.post("/api/v1/affiliate/register-coupon", {
1564
- code,
1565
- distinctId
1566
- });
1567
- if (response.success && response.coupon) {
1568
- this.log("Coupon registered:", code);
1569
- return { success: true, coupon: mapRawCoupon(response.coupon) };
1570
- }
1571
- return {
1572
- success: false,
1573
- error: response.error
1574
- };
1575
- } catch (error) {
1576
- this.log("Failed to register coupon:", error);
1577
- return {
1578
- success: false,
1579
- error: error instanceof Error ? error.message : "Unknown error"
1580
- };
1581
- }
1582
- }
1583
- async applyCoupon(code) {
1584
- this.ensureInitialized();
1585
- const distinctId = identityManager.getDistinctId();
1586
- try {
1587
- const response = await this.post("/api/v1/affiliate/apply-coupon", {
1588
- code,
1589
- distinctId
1590
- });
1591
- if (response.success) {
1592
- this.log("Coupon applied:", code);
1593
- }
1594
- return response;
1595
- } catch (error) {
1596
- this.log("Failed to apply coupon:", error);
1597
- return {
1598
- success: false,
1599
- error: error instanceof Error ? error.message : "Unknown error"
1600
- };
1601
- }
1602
- }
1603
- async getMyCoupon() {
1604
- this.ensureInitialized();
1605
- const distinctId = identityManager.getDistinctId();
1606
- try {
1607
- const response = await this.get("/api/v1/affiliate/my-coupon", { distinctId });
1608
- if (response.coupon) {
1609
- return { coupon: mapRawCoupon(response.coupon) };
1610
- }
1611
- return { coupon: null };
1612
- } catch (error) {
1613
- this.log("Failed to get my coupon:", error);
1614
- return { coupon: null };
1615
- }
1616
- }
1617
- async getAppliedCoupon() {
1618
- this.ensureInitialized();
1619
- const distinctId = identityManager.getDistinctId();
1620
- try {
1621
- const response = await this.get("/api/v1/affiliate/applied-coupon", { distinctId });
1622
- return response;
1623
- } catch (error) {
1624
- this.log("Failed to get applied coupon:", error);
1625
- return { coupon: null };
1626
- }
1627
- }
1628
- async getBalance() {
1629
- this.ensureInitialized();
1630
- const distinctId = identityManager.getDistinctId();
1631
- try {
1632
- const response = await this.get("/api/v1/affiliate/balance", {
1633
- distinctId
1634
- });
1635
- return response;
1636
- } catch (error) {
1637
- this.log("Failed to get balance:", error);
1638
- return {
1639
- total: 0,
1640
- pending: 0,
1641
- available: 0,
1642
- withdrawn: 0
1643
- };
1644
- }
1645
- }
1646
- async getCommissions() {
1647
- this.ensureInitialized();
1648
- const distinctId = identityManager.getDistinctId();
1649
- try {
1650
- const response = await this.get("/api/v1/affiliate/commissions", { distinctId });
1651
- return response.commissions.map(mapRawCommission);
1652
- } catch (error) {
1653
- this.log("Failed to get commissions:", error);
1654
- return [];
1655
- }
1656
- }
1657
- async requestWithdrawal(amount) {
1658
- this.ensureInitialized();
1659
- const distinctId = identityManager.getDistinctId();
1660
- try {
1661
- const response = await this.post("/api/v1/affiliate/withdraw", {
1662
- amount,
1663
- distinctId
1664
- });
1665
- if (response.success && response.withdrawal) {
1666
- this.log("Withdrawal requested:", amount);
1667
- return { success: true, withdrawal: mapRawWithdrawal(response.withdrawal) };
1668
- }
1669
- return {
1670
- success: false,
1671
- error: response.error
1672
- };
1673
- } catch (error) {
1674
- this.log("Failed to request withdrawal:", error);
1675
- return {
1676
- success: false,
1677
- error: error instanceof Error ? error.message : "Unknown error"
1678
- };
1679
- }
1680
- }
1681
- async getWithdrawals() {
1682
- this.ensureInitialized();
1683
- const distinctId = identityManager.getDistinctId();
1684
- try {
1685
- const response = await this.get("/api/v1/affiliate/withdrawals", { distinctId });
1686
- return response.withdrawals.map(mapRawWithdrawal);
1687
- } catch (error) {
1688
- this.log("Failed to get withdrawals:", error);
1689
- return [];
1690
- }
1691
- }
1692
- async get(path, params) {
1693
- this.ensureInitialized();
1694
- return affiliateGet(this.apiClient, path, params);
1695
- }
1696
- async post(path, body) {
1697
- this.ensureInitialized();
1698
- return affiliatePost(this.apiClient, path, body);
1699
- }
1700
- ensureInitialized() {
1701
- if (!this.apiClient) {
1702
- throw new AffiliateError(
1703
- AFFILIATE_ERROR_CODES.NOT_INITIALIZED,
1704
- "AffiliateManager not initialized. Call initialize() first"
1705
- );
1706
- }
1707
- }
1708
- log(...args) {
1709
- if (this.debug) console.log("[Paywallo:Affiliate]", ...args);
1710
- }
1711
- };
1712
- var affiliateManager = new AffiliateManager();
1713
-
1714
- // src/domains/affiliate/PaywalloAffiliate.ts
1715
- var PaywalloAffiliate = {
1716
- registerCoupon: (code) => affiliateManager.registerCoupon(code),
1717
- applyCoupon: (code) => affiliateManager.applyCoupon(code),
1718
- getMyCoupon: () => affiliateManager.getMyCoupon(),
1719
- getAppliedCoupon: () => affiliateManager.getAppliedCoupon(),
1720
- getBalance: () => affiliateManager.getBalance(),
1721
- getCommissions: () => affiliateManager.getCommissions(),
1722
- requestWithdrawal: (amount) => affiliateManager.requestWithdrawal(amount),
1723
- getWithdrawals: () => affiliateManager.getWithdrawals()
1724
- };
1725
-
1726
1736
  // src/domains/subscription/SubscriptionCache.ts
1727
- var LEGACY_CACHE_KEY = "subscription_cache";
1737
+ var LEGACY_CACHE_KEY = "@panel:subscription_cache";
1728
1738
  var CACHE_KEY_PREFIX = "subscription_cache:";
1739
+ var CACHE_INDEX_KEY = "subscription_cache:__index__";
1729
1740
  var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
1730
1741
  function keyFor(distinctId) {
1731
1742
  return `${CACHE_KEY_PREFIX}${distinctId}`;
@@ -1805,9 +1816,30 @@ var SubscriptionCache = class {
1805
1816
  this.memoryCache.set(distinctId, cached);
1806
1817
  try {
1807
1818
  await this.storage.set(keyFor(distinctId), JSON.stringify(cached));
1819
+ await this.addToIndex(distinctId);
1820
+ } catch {
1821
+ }
1822
+ }
1823
+ async addToIndex(distinctId) {
1824
+ try {
1825
+ const raw = await this.storage.get(CACHE_INDEX_KEY);
1826
+ const ids = raw ? JSON.parse(raw) : [];
1827
+ if (!ids.includes(distinctId)) {
1828
+ ids.push(distinctId);
1829
+ await this.storage.set(CACHE_INDEX_KEY, JSON.stringify(ids));
1830
+ }
1808
1831
  } catch {
1809
1832
  }
1810
1833
  }
1834
+ async readIndex() {
1835
+ try {
1836
+ const raw = await this.storage.get(CACHE_INDEX_KEY);
1837
+ if (!raw) return [];
1838
+ return JSON.parse(raw);
1839
+ } catch {
1840
+ return [];
1841
+ }
1842
+ }
1811
1843
  async invalidate(distinctId) {
1812
1844
  if (!distinctId) return;
1813
1845
  this.memoryCache.delete(distinctId);
@@ -1817,11 +1849,15 @@ var SubscriptionCache = class {
1817
1849
  }
1818
1850
  }
1819
1851
  async invalidateAll() {
1852
+ const memKeys = Array.from(this.memoryCache.keys());
1853
+ const indexKeys = await this.readIndex();
1854
+ const allKeys = Array.from(/* @__PURE__ */ new Set([...memKeys, ...indexKeys]));
1820
1855
  this.memoryCache.clear();
1821
- try {
1822
- await this.storage.remove(LEGACY_CACHE_KEY);
1823
- } catch {
1824
- }
1856
+ await Promise.allSettled([
1857
+ this.storage.remove(LEGACY_CACHE_KEY),
1858
+ this.storage.remove(CACHE_INDEX_KEY),
1859
+ ...allKeys.map((k) => this.storage.remove(keyFor(k)))
1860
+ ]);
1825
1861
  }
1826
1862
  isExpired(cached) {
1827
1863
  return Date.now() - cached.cachedAt > this.ttl;
@@ -1832,9 +1868,6 @@ var SubscriptionCache = class {
1832
1868
  };
1833
1869
  var subscriptionCache = new SubscriptionCache();
1834
1870
 
1835
- // src/domains/subscription/SubscriptionManager.ts
1836
- import { Platform as Platform4 } from "react-native";
1837
-
1838
1871
  // src/domains/session/SessionError.ts
1839
1872
  var SessionError = class extends PaywalloError {
1840
1873
  constructor(code, message) {
@@ -1922,11 +1955,10 @@ var SubscriptionManagerClass = class {
1922
1955
  if (!this.config) {
1923
1956
  return this.getEmptyStatus();
1924
1957
  }
1925
- const url = new URL(`${this.config.serverUrl}/subscriptions/status`);
1958
+ const url = new URL(`${this.config.serverUrl}/purchases/status/${encodeURIComponent(this.config.appKey)}`);
1926
1959
  if (this.userId) {
1927
- url.searchParams.set("userId", this.userId);
1960
+ url.searchParams.set("distinctId", this.userId);
1928
1961
  }
1929
- url.searchParams.set("platform", Platform4.OS);
1930
1962
  const response = await fetch(url.toString(), {
1931
1963
  method: "GET",
1932
1964
  headers: {
@@ -2029,7 +2061,7 @@ async function validatePurchase(client, appKey, platform, receipt, productId, tr
2029
2061
  false
2030
2062
  );
2031
2063
  if (!response.ok) {
2032
- throw new Error(`Server validation failed: HTTP ${response.status}`);
2064
+ throw new PurchaseError(PURCHASE_ERROR_CODES.VALIDATION_FAILED, `Server validation failed: HTTP ${response.status}`);
2033
2065
  }
2034
2066
  return response.data;
2035
2067
  }
@@ -2038,7 +2070,7 @@ async function getSubscriptionStatus(client, baseUrl, appKey, distinctId) {
2038
2070
  url.searchParams.set("distinctId", distinctId);
2039
2071
  const response = await client.get(url.toString());
2040
2072
  if (!response.ok) {
2041
- throw new Error(`Subscription status check failed: HTTP ${response.status}`);
2073
+ throw new PurchaseError(PURCHASE_ERROR_CODES.NETWORK_ERROR, `Subscription status check failed: HTTP ${response.status}`);
2042
2074
  }
2043
2075
  return response.data;
2044
2076
  }
@@ -2080,7 +2112,7 @@ var HttpClient = class {
2080
2112
  constructor(baseUrl, config) {
2081
2113
  this.config = {
2082
2114
  baseUrl,
2083
- timeout: config?.timeout ?? DEFAULT_HTTP_CONFIG.timeout,
2115
+ timeout: config?.timeout ?? DEFAULT_HTTP_CONFIG.timeout ?? 1e4,
2084
2116
  retry: { ...DEFAULT_RETRY_CONFIG, ...config?.retry },
2085
2117
  debug: config?.debug ?? false
2086
2118
  };
@@ -2110,7 +2142,7 @@ var HttpClient = class {
2110
2142
  if (this.shouldRetry(response.status, retryConfig)) {
2111
2143
  if (state.attempt < retryConfig.maxRetries) {
2112
2144
  state.attempt++;
2113
- const delay = this.calculateDelay(state.attempt, retryConfig);
2145
+ const delay = this.calculateDelayForResponse(state.attempt, response.headers, retryConfig);
2114
2146
  await this.sleep(delay);
2115
2147
  continue;
2116
2148
  }
@@ -2164,7 +2196,7 @@ var HttpClient = class {
2164
2196
  };
2165
2197
  } catch (error) {
2166
2198
  this.log("Request failed", {
2167
- url,
2199
+ url: this.redactUrl(url),
2168
2200
  error: error instanceof Error ? error.message : String(error)
2169
2201
  });
2170
2202
  throw error;
@@ -2177,6 +2209,14 @@ var HttpClient = class {
2177
2209
  }
2178
2210
  buildUrl(path) {
2179
2211
  if (path.startsWith("http://") || path.startsWith("https://")) {
2212
+ if (path.startsWith("http://")) {
2213
+ const isLocalDev = /^http:\/\/(localhost|127\.0\.0\.1|192\.168\.\d{1,3}\.\d{1,3})/.test(path);
2214
+ if (!isLocalDev) {
2215
+ const sanitizedPath = path.split("?")[0];
2216
+ this.log("Blocked http:// request \u2014 only HTTPS is allowed in production:", sanitizedPath);
2217
+ throw new ClientError(CLIENT_ERROR_CODES.INSECURE_REQUEST, `Insecure HTTP request blocked: ${sanitizedPath}`);
2218
+ }
2219
+ }
2180
2220
  return path;
2181
2221
  }
2182
2222
  return `${this.config.baseUrl}${path}`;
@@ -2216,9 +2256,28 @@ var HttpClient = class {
2216
2256
  const jitter = Math.random() * 0.3 * exponentialDelay;
2217
2257
  return Math.min(exponentialDelay + jitter, config.maxDelay);
2218
2258
  }
2259
+ calculateDelayForResponse(attempt, headers, config) {
2260
+ const retryAfter = headers["retry-after"];
2261
+ if (retryAfter) {
2262
+ const seconds = Number(retryAfter);
2263
+ if (!Number.isNaN(seconds) && seconds > 0) {
2264
+ return Math.min(seconds * 1e3, config.maxDelay);
2265
+ }
2266
+ const date = new Date(retryAfter);
2267
+ if (!Number.isNaN(date.getTime())) {
2268
+ const ms = date.getTime() - Date.now();
2269
+ if (ms > 0) return Math.min(ms, config.maxDelay);
2270
+ }
2271
+ }
2272
+ return this.calculateDelay(attempt, config);
2273
+ }
2219
2274
  sleep(ms) {
2220
2275
  return new Promise((resolve) => setTimeout(resolve, ms));
2221
2276
  }
2277
+ /** Strips path segments that look like app keys (alphanumeric, 20–64 chars) from URLs before logging. */
2278
+ redactUrl(url) {
2279
+ return url.replace(/\/([a-zA-Z0-9_-]{20,64})(\?|$|\/)/g, "/[REDACTED]$2");
2280
+ }
2222
2281
  log(...args) {
2223
2282
  if (this.config.debug) console.log("[Paywallo:Http]", ...args);
2224
2283
  }
@@ -2270,7 +2329,17 @@ var NetworkMonitorClass = class {
2270
2329
  setupAppStateListener() {
2271
2330
  this.appStateSubscription = AppState.addEventListener("change", (state) => {
2272
2331
  if (state === "active") {
2332
+ if (!this.checkIntervalId) {
2333
+ this.checkIntervalId = setInterval(() => {
2334
+ void this.checkConnectivity();
2335
+ }, this.config.checkInterval);
2336
+ }
2273
2337
  void this.checkConnectivity();
2338
+ } else if (state === "background" || state === "inactive") {
2339
+ if (this.checkIntervalId) {
2340
+ clearInterval(this.checkIntervalId);
2341
+ this.checkIntervalId = null;
2342
+ }
2274
2343
  }
2275
2344
  });
2276
2345
  }
@@ -2352,7 +2421,6 @@ var NetworkMonitorClass = class {
2352
2421
  var networkMonitor = new NetworkMonitorClass();
2353
2422
 
2354
2423
  // src/core/queue/OfflineQueue.ts
2355
- import AsyncStorage2 from "@react-native-async-storage/async-storage";
2356
2424
  init_uuid();
2357
2425
 
2358
2426
  // src/core/queue/deduplication.ts
@@ -2414,7 +2482,7 @@ var OfflineQueueClass = class {
2414
2482
  }
2415
2483
  async loadFromStorage() {
2416
2484
  try {
2417
- const stored = await AsyncStorage2.getItem(this.config.storageKey);
2485
+ const stored = await nativeStorage.get(this.config.storageKey);
2418
2486
  if (stored) {
2419
2487
  this.queue = JSON.parse(stored);
2420
2488
  }
@@ -2424,7 +2492,7 @@ var OfflineQueueClass = class {
2424
2492
  }
2425
2493
  async saveToStorage() {
2426
2494
  try {
2427
- await AsyncStorage2.setItem(this.config.storageKey, JSON.stringify(this.queue));
2495
+ await nativeStorage.set(this.config.storageKey, JSON.stringify(this.queue));
2428
2496
  return true;
2429
2497
  } catch (error) {
2430
2498
  this.log("Failed to save queue to storage", error);
@@ -2439,7 +2507,7 @@ var OfflineQueueClass = class {
2439
2507
  cleanupExpired() {
2440
2508
  const now = Date.now();
2441
2509
  const before = this.queue.length;
2442
- this.queue = this.queue.filter((item) => Date.now() - item.timestamp < this.config.maxAge);
2510
+ this.queue = this.queue.filter((item) => now - item.timestamp < this.config.maxAge);
2443
2511
  if (before !== this.queue.length) {
2444
2512
  void this.saveToStorage();
2445
2513
  }
@@ -2462,10 +2530,16 @@ var OfflineQueueClass = class {
2462
2530
  return this.queue[existingIndex];
2463
2531
  }
2464
2532
  if (this.queue.length >= this.config.maxItems) {
2465
- const removed = this.queue.shift();
2466
- if (removed) {
2467
- this.emit({ type: "item:evicted", item: removed, queueSize: this.queue.length });
2533
+ let evictIndex = 0;
2534
+ for (let i = 1; i < this.queue.length; i++) {
2535
+ const candidate = this.queue[i];
2536
+ const current = this.queue[evictIndex];
2537
+ if (candidate.attempts > current.attempts || candidate.attempts === current.attempts && candidate.timestamp < current.timestamp) {
2538
+ evictIndex = i;
2539
+ }
2468
2540
  }
2541
+ const [removed] = this.queue.splice(evictIndex, 1);
2542
+ this.emit({ type: "item:evicted", item: removed, queueSize: this.queue.length });
2469
2543
  }
2470
2544
  this.queue.push(item);
2471
2545
  await this.saveToStorage();
@@ -2664,9 +2738,78 @@ var QueueProcessorClass = class {
2664
2738
  this.log("Offline, skipping queue processing");
2665
2739
  return { processed: 0, failed: 0 };
2666
2740
  }
2667
- return offlineQueue.processQueue(async (item) => {
2741
+ const allItems = offlineQueue.getQueue();
2742
+ const eventItems = allItems.filter(
2743
+ (i) => i.url.includes("/api/v1/events") && !i.url.includes("/batch")
2744
+ );
2745
+ const eventItemIds = new Set(eventItems.map((i) => i.id));
2746
+ const { processed: batchProcessed, failed: batchFailed, retryIds } = await this.processEventBatches(eventItems);
2747
+ const batchResult = { processed: batchProcessed, failed: batchFailed };
2748
+ const individualResult = await offlineQueue.processQueue(async (item) => {
2749
+ if (eventItemIds.has(item.id)) {
2750
+ if (retryIds.has(item.id)) {
2751
+ return { success: false, permanent: false };
2752
+ }
2753
+ return { success: true, permanent: false };
2754
+ }
2668
2755
  return this.processItem(item);
2669
2756
  });
2757
+ this.log("Queue processing complete", {
2758
+ batchProcessed: batchResult.processed,
2759
+ batchFailed: batchResult.failed,
2760
+ individualProcessed: individualResult.processed,
2761
+ individualFailed: individualResult.failed
2762
+ });
2763
+ return {
2764
+ processed: batchResult.processed + individualResult.processed,
2765
+ failed: batchResult.failed + individualResult.failed
2766
+ };
2767
+ }
2768
+ async processEventBatches(eventItems) {
2769
+ if (eventItems.length === 0 || !this.httpClient) {
2770
+ return { processed: 0, failed: 0, retryIds: /* @__PURE__ */ new Set() };
2771
+ }
2772
+ const BATCH_SIZE = 50;
2773
+ let processed = 0;
2774
+ let failed = 0;
2775
+ const retryIds = /* @__PURE__ */ new Set();
2776
+ for (let i = 0; i < eventItems.length; i += BATCH_SIZE) {
2777
+ const batch = eventItems.slice(i, i + BATCH_SIZE);
2778
+ const bodies = batch.map((item) => item.body);
2779
+ const headers = batch[0].headers;
2780
+ this.log("Sending event batch", { size: batch.length, batchIndex: Math.floor(i / BATCH_SIZE) });
2781
+ const response = await this.httpClient.request("/api/v1/events/batch", {
2782
+ method: "POST",
2783
+ body: { events: bodies },
2784
+ headers,
2785
+ skipRetry: false
2786
+ });
2787
+ if (response.ok) {
2788
+ for (const item of batch) {
2789
+ await offlineQueue.removeItem(item.id);
2790
+ processed++;
2791
+ }
2792
+ this.log("Event batch processed successfully", { size: batch.length });
2793
+ } else if (response.status >= 400 && response.status < 500) {
2794
+ for (const item of batch) {
2795
+ await offlineQueue.removeItem(item.id);
2796
+ failed++;
2797
+ }
2798
+ this.log("Event batch failed permanently (4xx) - removing from queue", {
2799
+ status: response.status,
2800
+ size: batch.length
2801
+ });
2802
+ } else {
2803
+ for (const item of batch) {
2804
+ retryIds.add(item.id);
2805
+ }
2806
+ this.log("Event batch failed with server error - will retry", {
2807
+ status: response.status,
2808
+ size: batch.length
2809
+ });
2810
+ }
2811
+ }
2812
+ return { processed, failed, retryIds };
2670
2813
  }
2671
2814
  async processItem(item) {
2672
2815
  if (!this.httpClient) {
@@ -2676,7 +2819,6 @@ var QueueProcessorClass = class {
2676
2819
  id: item.id,
2677
2820
  url: item.url,
2678
2821
  method: item.method,
2679
- headers: item.headers,
2680
2822
  attempts: item.attempts
2681
2823
  });
2682
2824
  const response = await this.httpClient.request(item.url, {
@@ -2694,7 +2836,7 @@ var QueueProcessorClass = class {
2694
2836
  id: item.id,
2695
2837
  url: item.url,
2696
2838
  status: response.status,
2697
- appKeyInHeaders: item.headers?.["X-App-Key"]
2839
+ hasAppKey: item.headers?.["X-App-Key"] != null
2698
2840
  });
2699
2841
  return { success: false, permanent: true, status: response.status };
2700
2842
  }
@@ -2816,9 +2958,94 @@ var ApiCache = class {
2816
2958
  }
2817
2959
  };
2818
2960
 
2961
+ // src/core/EventBatcher.ts
2962
+ import { Platform as Platform4 } from "react-native";
2963
+ var BATCH_MAX_SIZE = 10;
2964
+ var BATCH_FLUSH_MS = 5e3;
2965
+ var EventBatcher = class {
2966
+ constructor(post, debug = false) {
2967
+ this.batch = [];
2968
+ this.timer = null;
2969
+ this.post = post;
2970
+ this.debug = debug;
2971
+ }
2972
+ async track(eventName, distinctId, environment, properties, timestamp) {
2973
+ const EVENT_NAME_REGEX = /^(\$[a-zA-Z0-9_]+|[a-z][a-z0-9_]*)$/;
2974
+ if (!EVENT_NAME_REGEX.test(eventName)) {
2975
+ if (this.debug) console.warn(`[Paywallo] Invalid event name "${eventName}". Must be snake_case or start with $`);
2976
+ }
2977
+ const event = {
2978
+ eventName,
2979
+ distinctId,
2980
+ properties: { ...properties, platform: Platform4.OS },
2981
+ timestamp: timestamp ?? Date.now(),
2982
+ environment: environment.toLowerCase()
2983
+ };
2984
+ this.batch.push(event);
2985
+ if (this.batch.length >= BATCH_MAX_SIZE) {
2986
+ if (this.timer !== null) {
2987
+ clearTimeout(this.timer);
2988
+ this.timer = null;
2989
+ }
2990
+ await this.flush();
2991
+ return;
2992
+ }
2993
+ if (this.timer !== null) clearTimeout(this.timer);
2994
+ this.timer = setTimeout(() => {
2995
+ this.timer = null;
2996
+ void this.flush();
2997
+ }, BATCH_FLUSH_MS);
2998
+ }
2999
+ async flush() {
3000
+ if (this.batch.length === 0) return;
3001
+ const batch = this.batch.splice(0, this.batch.length);
3002
+ if (this.timer !== null) {
3003
+ clearTimeout(this.timer);
3004
+ this.timer = null;
3005
+ }
3006
+ try {
3007
+ await this.post("/api/v1/events/batch", { events: batch }, `event_batch:${batch.length}`);
3008
+ } catch {
3009
+ for (const event of batch) {
3010
+ await this.post("/api/v1/events", event, `event:${event.eventName}`);
3011
+ }
3012
+ }
3013
+ }
3014
+ dispose() {
3015
+ if (this.timer !== null) {
3016
+ clearTimeout(this.timer);
3017
+ this.timer = null;
3018
+ }
3019
+ this.batch = [];
3020
+ }
3021
+ };
3022
+
3023
+ // src/core/FlagStorage.ts
3024
+ var FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
3025
+ async function readFlagFromStorage(flagKey, distinctId) {
3026
+ try {
3027
+ const raw = await secureStorage.get(`flag:${flagKey}:${distinctId}`);
3028
+ if (!raw) return null;
3029
+ const parsed = JSON.parse(raw);
3030
+ if (typeof parsed !== "object" || parsed === null || !("variant" in parsed) || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") return null;
3031
+ const { variant, payload, cachedAt } = parsed;
3032
+ if (Date.now() - cachedAt > FLAG_MAX_AGE_MS) return null;
3033
+ return { variant, payload };
3034
+ } catch {
3035
+ return null;
3036
+ }
3037
+ }
3038
+ async function writeFlagToStorage(flagKey, distinctId, flag) {
3039
+ try {
3040
+ const value = JSON.stringify({ variant: flag.variant, payload: flag.payload, cachedAt: Date.now() });
3041
+ await secureStorage.set(`flag:${flagKey}:${distinctId}`, value);
3042
+ } catch {
3043
+ }
3044
+ }
3045
+
2819
3046
  // src/core/ApiClient.ts
2820
- var SDK_VERSION = "1.4.1";
2821
- var _ApiClient = class _ApiClient {
3047
+ var SDK_VERSION = "1.5.2";
3048
+ var ApiClient = class {
2822
3049
  constructor(appKey, apiUrl, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
2823
3050
  this.cache = new ApiCache();
2824
3051
  this.appKey = appKey;
@@ -2828,6 +3055,7 @@ var _ApiClient = class _ApiClient {
2828
3055
  this.environment = environment;
2829
3056
  this.offlineQueueEnabled = offlineQueueEnabled;
2830
3057
  this.httpClient = new HttpClient(this.baseUrl, { timeout, debug });
3058
+ this.batcher = new EventBatcher((url, payload, label) => this.postWithQueue(url, payload, label), debug);
2831
3059
  }
2832
3060
  getHttpClient() {
2833
3061
  return this.httpClient;
@@ -2838,6 +3066,9 @@ var _ApiClient = class _ApiClient {
2838
3066
  getWebUrl() {
2839
3067
  return this.webUrl;
2840
3068
  }
3069
+ getAppKey() {
3070
+ return this.appKey;
3071
+ }
2841
3072
  getEnvironment() {
2842
3073
  return this.environment;
2843
3074
  }
@@ -2876,27 +3107,30 @@ var _ApiClient = class _ApiClient {
2876
3107
  platform: Platform5.OS
2877
3108
  }, true);
2878
3109
  } catch (err) {
2879
- if (this.debug) console.warn("[Paywallo:ApiClient] reportError failed", err);
3110
+ if (this.debug) console.log("[Paywallo:ApiClient] reportError failed", err);
2880
3111
  }
2881
3112
  }
2882
3113
  async trackEvent(eventName, distinctId, properties, timestamp) {
2883
- await this.postWithQueue("/api/v1/events", {
2884
- eventName,
2885
- distinctId,
2886
- properties: { ...properties, platform: Platform5.OS },
2887
- timestamp: timestamp ?? Date.now(),
2888
- environment: this.environment.toLowerCase()
2889
- }, `event:${eventName}`);
3114
+ await this.batcher.track(eventName, distinctId, this.environment, properties, timestamp);
3115
+ }
3116
+ async flushEventBatch() {
3117
+ await this.batcher.flush();
3118
+ }
3119
+ dispose() {
3120
+ this.batcher.dispose();
2890
3121
  }
2891
3122
  async identify(distinctId, properties, email, deviceId) {
2892
3123
  await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: Platform5.OS, ...deviceId && { deviceId } }, "identify");
2893
3124
  }
2894
- async startSession(distinctId, sessionId, platform) {
3125
+ async startSession(distinctId, sessionId, platform, appVersion, deviceModel, osVersion) {
2895
3126
  await this.postWithQueue("/api/v1/sessions/start", {
2896
3127
  distinctId,
2897
3128
  sessionId,
2898
3129
  platform: platform ?? Platform5.OS,
2899
- environment: this.environment.toLowerCase()
3130
+ environment: this.environment.toLowerCase(),
3131
+ appVersion: appVersion ?? "unknown",
3132
+ deviceModel: deviceModel ?? "unknown",
3133
+ osVersion: osVersion ?? String(Platform5.Version)
2900
3134
  }, `session.start:${sessionId}`);
2901
3135
  return { success: true };
2902
3136
  }
@@ -2908,7 +3142,7 @@ var _ApiClient = class _ApiClient {
2908
3142
  const key = `${flagKey}:${distinctId}`;
2909
3143
  const hit = this.cache.getVariant(key);
2910
3144
  if (hit !== void 0) return hit;
2911
- const persisted = await this.readFlagFromStorage(flagKey, distinctId);
3145
+ const persisted = await readFlagFromStorage(flagKey, distinctId);
2912
3146
  if (persisted !== null) {
2913
3147
  this.cache.setVariant(key, persisted);
2914
3148
  return persisted;
@@ -2916,25 +3150,20 @@ var _ApiClient = class _ApiClient {
2916
3150
  try {
2917
3151
  const res = await this.get(`/api/v1/flags/${flagKey}?distinctId=${encodeURIComponent(distinctId)}`);
2918
3152
  if (res.status === 404) {
2919
- this.log("Flag not found (404):", flagKey);
2920
3153
  const v = { variant: null };
2921
3154
  this.cache.setVariant(key, v, this.cache.nullTTL);
2922
3155
  return v;
2923
3156
  }
2924
3157
  if (!res.ok) {
2925
- this.log("Non-OK response for variant:", flagKey, res.status);
2926
3158
  const stale = this.cache.getStaleVariant(key);
2927
- if (stale !== void 0) return stale;
2928
- return { variant: null };
3159
+ return stale !== void 0 ? stale : { variant: null };
2929
3160
  }
2930
3161
  this.cache.setVariant(key, res.data);
2931
- await this.writeFlagToStorage(flagKey, distinctId, res.data);
3162
+ await writeFlagToStorage(flagKey, distinctId, res.data);
2932
3163
  return res.data;
2933
- } catch (error) {
2934
- this.log("Failed to get variant:", flagKey, error);
3164
+ } catch {
2935
3165
  const stale = this.cache.getStaleVariant(key);
2936
- if (stale !== void 0) return stale;
2937
- return { variant: null };
3166
+ return stale !== void 0 ? stale : { variant: null };
2938
3167
  }
2939
3168
  }
2940
3169
  async getPaywall(placement) {
@@ -2943,29 +3172,22 @@ var _ApiClient = class _ApiClient {
2943
3172
  try {
2944
3173
  const res = await this.get(`/api/v1/paywalls/${placement}`);
2945
3174
  if (res.status === 404) {
2946
- this.log("Paywall not found (404):", placement);
2947
3175
  this.cache.setPaywall(placement, null, this.cache.nullTTL);
2948
3176
  return null;
2949
3177
  }
2950
3178
  if (!res.ok) {
2951
- this.log("Non-OK response for paywall:", placement, res.status);
2952
3179
  const stale = this.cache.getStalePaywall(placement);
2953
- if (stale !== void 0) return stale;
2954
- return null;
3180
+ return stale !== void 0 ? stale : null;
2955
3181
  }
2956
3182
  this.cache.setPaywall(placement, res.data);
2957
3183
  return res.data;
2958
- } catch (error) {
2959
- this.log("Failed to get paywall:", placement, error);
3184
+ } catch {
2960
3185
  const stale = this.cache.getStalePaywall(placement);
2961
- if (stale !== void 0) return stale;
2962
- return null;
3186
+ return stale !== void 0 ? stale : null;
2963
3187
  }
2964
3188
  }
2965
3189
  async getEmergencyPaywall() {
2966
- const result = await getEmergencyPaywall(this);
2967
- this.log("Got emergency paywall:", result.enabled);
2968
- return result;
3190
+ return getEmergencyPaywall(this);
2969
3191
  }
2970
3192
  async getCampaign(placement, distinctId, context) {
2971
3193
  const key = `${placement}:${distinctId}`;
@@ -2974,18 +3196,14 @@ var _ApiClient = class _ApiClient {
2974
3196
  try {
2975
3197
  const result = await getCampaignData(this, this.baseUrl, placement, distinctId, context);
2976
3198
  if (!result) {
2977
- this.log("No campaign found for placement:", placement);
2978
3199
  this.cache.setCampaign(key, null, this.cache.nullTTL);
2979
3200
  return null;
2980
3201
  }
2981
- this.log("Got campaign:", placement, result.variantKey);
2982
3202
  this.cache.setCampaign(key, result);
2983
3203
  return result;
2984
- } catch (error) {
2985
- this.log("Failed to get campaign:", placement, error);
3204
+ } catch {
2986
3205
  const stale = this.cache.getStaleCampaign(key);
2987
- if (stale !== void 0) return stale;
2988
- return null;
3206
+ return stale !== void 0 ? stale : null;
2989
3207
  }
2990
3208
  }
2991
3209
  async getPrimaryCampaign(distinctId) {
@@ -2995,63 +3213,34 @@ var _ApiClient = class _ApiClient {
2995
3213
  try {
2996
3214
  const res = await this.get(`/campaigns/public/primary?distinctId=${encodeURIComponent(distinctId)}`);
2997
3215
  if (res.status === 404) {
2998
- this.log("Primary campaign not found (404)");
2999
3216
  this.cache.setCampaign(key, null, this.cache.nullTTL);
3000
3217
  return null;
3001
3218
  }
3002
3219
  if (!res.ok) {
3003
- this.log("Non-OK response for primary campaign:", res.status);
3004
3220
  const stale = this.cache.getStaleCampaign(key);
3005
- if (stale !== void 0) return stale;
3006
- return null;
3221
+ return stale !== void 0 ? stale : null;
3007
3222
  }
3008
- this.log("Got primary campaign:", res.data?.campaignId);
3009
3223
  this.cache.setCampaign(key, res.data);
3010
3224
  return res.data;
3011
- } catch (error) {
3012
- this.log("Failed to get primary campaign:", error);
3225
+ } catch {
3013
3226
  const stale = this.cache.getStaleCampaign(key);
3014
- if (stale !== void 0) return stale;
3015
- return null;
3227
+ return stale !== void 0 ? stale : null;
3016
3228
  }
3017
3229
  }
3018
3230
  async validatePurchase(platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
3019
- const result = await validatePurchase(
3020
- this,
3021
- this.appKey,
3022
- platform,
3023
- receipt,
3024
- productId,
3025
- transactionId,
3026
- priceLocal,
3027
- currency,
3028
- country,
3029
- distinctId,
3030
- paywallPlacement,
3031
- variantKey
3032
- );
3033
- this.log("Purchase validated:", result.success);
3034
- return result;
3231
+ return validatePurchase(this, this.appKey, platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey);
3035
3232
  }
3036
3233
  async getSubscriptionStatus(distinctId) {
3037
- const result = await getSubscriptionStatus(this, this.baseUrl, this.appKey, distinctId);
3038
- this.log("Subscription status:", result.hasActiveSubscription);
3039
- return result;
3234
+ return getSubscriptionStatus(this, this.baseUrl, this.appKey, distinctId);
3040
3235
  }
3041
3236
  async evaluateFlags(keys, distinctId) {
3042
3237
  const query = keys.map(encodeURIComponent).join(",");
3043
- const path = `/api/v1/flags/evaluate?keys=${query}`;
3044
3238
  try {
3045
- const res = await this.httpClient.get(path, {
3239
+ const res = await this.httpClient.get(`/api/v1/flags/evaluate?keys=${query}`, {
3046
3240
  headers: { "X-App-Key": this.appKey, "x-distinct-id": distinctId }
3047
3241
  });
3048
- if (!res.ok) {
3049
- this.log("evaluateFlags failed:", res.status, keys);
3050
- return {};
3051
- }
3052
- const raw = res.data;
3053
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
3054
- const record = raw;
3242
+ if (!res.ok || typeof res.data !== "object" || res.data === null || Array.isArray(res.data)) return {};
3243
+ const record = res.data;
3055
3244
  const result = {};
3056
3245
  for (const key of Object.keys(record)) {
3057
3246
  const value = record[key];
@@ -3059,11 +3248,10 @@ var _ApiClient = class _ApiClient {
3059
3248
  result[key] = variant;
3060
3249
  const flagVariant = { variant };
3061
3250
  this.cache.setVariant(`${key}:${distinctId}`, flagVariant);
3062
- await this.writeFlagToStorage(key, distinctId, flagVariant);
3251
+ await writeFlagToStorage(key, distinctId, flagVariant);
3063
3252
  }
3064
3253
  return result;
3065
- } catch (error) {
3066
- this.log("Failed to evaluateFlags:", error);
3254
+ } catch {
3067
3255
  return {};
3068
3256
  }
3069
3257
  }
@@ -3071,15 +3259,9 @@ var _ApiClient = class _ApiClient {
3071
3259
  try {
3072
3260
  const url = buildConditionalFlagUrl(this.baseUrl, flagKey, context);
3073
3261
  const response = await this.get(url);
3074
- if (response.status === 404) return { value: false, flagKey };
3075
- if (!response.ok) {
3076
- this.log("Non-OK response for conditional flag:", flagKey, response.status);
3077
- return { value: false, flagKey };
3078
- }
3079
- this.log("Got conditional flag:", flagKey, response.data.value);
3262
+ if (response.status === 404 || !response.ok) return { value: false, flagKey };
3080
3263
  return { value: response.data.value, flagKey };
3081
3264
  } catch {
3082
- this.log("Failed to get conditional flag:", flagKey);
3083
3265
  return { value: false, flagKey };
3084
3266
  }
3085
3267
  }
@@ -3087,34 +3269,13 @@ var _ApiClient = class _ApiClient {
3087
3269
  const key = `${flagKey}:${distinctId}`;
3088
3270
  const hit = this.cache.getVariant(key);
3089
3271
  if (hit !== void 0) return hit;
3090
- const persisted = await this.readFlagFromStorage(flagKey, distinctId);
3272
+ const persisted = await readFlagFromStorage(flagKey, distinctId);
3091
3273
  if (persisted !== null) {
3092
3274
  this.cache.setVariant(key, persisted);
3093
3275
  return persisted;
3094
3276
  }
3095
3277
  return null;
3096
3278
  }
3097
- async readFlagFromStorage(flagKey, distinctId) {
3098
- try {
3099
- const raw = await secureStorage.get(`flag:${flagKey}:${distinctId}`);
3100
- if (!raw) return null;
3101
- const parsed = JSON.parse(raw);
3102
- if (typeof parsed !== "object" || parsed === null || !("variant" in parsed) || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") return null;
3103
- const { variant, payload, cachedAt } = parsed;
3104
- if (Date.now() - cachedAt > _ApiClient.FLAG_MAX_AGE_MS) return null;
3105
- return { variant, payload };
3106
- } catch {
3107
- return null;
3108
- }
3109
- }
3110
- async writeFlagToStorage(flagKey, distinctId, flag) {
3111
- try {
3112
- const value = JSON.stringify({ variant: flag.variant, payload: flag.payload, cachedAt: Date.now() });
3113
- await secureStorage.set(`flag:${flagKey}:${distinctId}`, value);
3114
- } catch (err) {
3115
- this.log("writeFlagToStorage failed", err);
3116
- }
3117
- }
3118
3279
  async postWithQueue(url, payload, label) {
3119
3280
  if (this.offlineQueueEnabled && !networkMonitor.isOnline()) {
3120
3281
  await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
@@ -3127,7 +3288,6 @@ var _ApiClient = class _ApiClient {
3127
3288
  this.log("Request failed:", label, error);
3128
3289
  if (this.offlineQueueEnabled) {
3129
3290
  await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
3130
- this.log("Queued after failure:", label);
3131
3291
  return;
3132
3292
  }
3133
3293
  throw error;
@@ -3137,8 +3297,6 @@ var _ApiClient = class _ApiClient {
3137
3297
  if (this.debug) console.log("[Paywallo:ApiClient]", ...args);
3138
3298
  }
3139
3299
  };
3140
- _ApiClient.FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
3141
- var ApiClient = _ApiClient;
3142
3300
 
3143
3301
  // src/domains/campaign/CampaignError.ts
3144
3302
  var CampaignError = class extends PaywalloError {
@@ -3163,6 +3321,7 @@ var CampaignGateService = class {
3163
3321
  constructor() {
3164
3322
  this.preloadedCampaigns = /* @__PURE__ */ new Map();
3165
3323
  this.pendingPreloads = /* @__PURE__ */ new Set();
3324
+ this.activePreloadPromises = /* @__PURE__ */ new Map();
3166
3325
  }
3167
3326
  async getCampaign(apiClient, placement, context) {
3168
3327
  const distinctId = identityManager.getDistinctId();
@@ -3202,12 +3361,23 @@ var CampaignGateService = class {
3202
3361
  }
3203
3362
  async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
3204
3363
  const isActive = await hasActive();
3205
- if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo] requireSubscription \u2014 hasActive: ${isActive}`);
3364
+ if (PaywalloClient.getConfig()?.debug) console.log(`[Paywallo] requireSubscription \u2014 hasActive: ${isActive}`);
3206
3365
  if (isActive) return true;
3207
- const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context);
3366
+ const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, { ...context, forceShow: true });
3208
3367
  return r.purchased || r.restored;
3209
3368
  }
3210
3369
  async preloadCampaign(apiClient, placement, context) {
3370
+ const existing = this.activePreloadPromises.get(placement);
3371
+ if (existing) return existing;
3372
+ const promise = this._doPreload(apiClient, placement, context);
3373
+ this.activePreloadPromises.set(placement, promise);
3374
+ try {
3375
+ return await promise;
3376
+ } finally {
3377
+ this.activePreloadPromises.delete(placement);
3378
+ }
3379
+ }
3380
+ async _doPreload(apiClient, placement, context) {
3211
3381
  this.pendingPreloads.add(placement);
3212
3382
  try {
3213
3383
  const campaign = await this.getCampaign(apiClient, placement, context);
@@ -3216,7 +3386,7 @@ var CampaignGateService = class {
3216
3386
  }
3217
3387
  this.preloadedCampaigns.set(placement, { campaign, preloadedAt: Date.now() });
3218
3388
  if (PaywalloClient.getConfig()?.debug) {
3219
- console.info(`[Paywallo PRELOAD] data cached \u2014 placement: ${placement}`);
3389
+ console.log(`[Paywallo PRELOAD] data cached \u2014 placement: ${placement}`);
3220
3390
  }
3221
3391
  return { success: true };
3222
3392
  } catch (error) {
@@ -3273,9 +3443,7 @@ var AdvertisingIdManager = class {
3273
3443
  return this.cached;
3274
3444
  }
3275
3445
  if (Platform6.OS === "android") {
3276
- const androidStatus = await tracking.getTrackingPermissionsAsync();
3277
- const optedIn = androidStatus.granted;
3278
- const rawGaid = optedIn ? tracking.getAdvertisingId() : null;
3446
+ const rawGaid = tracking.getAdvertisingId();
3279
3447
  const gaid = this.sanitizeId(rawGaid);
3280
3448
  this.cached = { idfv: null, idfa: null, gaid, attStatus: "unavailable" };
3281
3449
  return this.cached;
@@ -3289,9 +3457,8 @@ var AdvertisingIdManager = class {
3289
3457
  }
3290
3458
  async collectIdfv() {
3291
3459
  try {
3292
- const app = await import("expo-application");
3293
- const idfv = await app.getIosIdForVendorAsync();
3294
- return this.sanitizeId(idfv);
3460
+ const info = await nativeDeviceInfo.getDeviceInfo();
3461
+ return this.sanitizeId(info.idfv);
3295
3462
  } catch {
3296
3463
  return null;
3297
3464
  }
@@ -3304,13 +3471,11 @@ var AdvertisingIdManager = class {
3304
3471
 
3305
3472
  // src/domains/identity/InstallTracker.ts
3306
3473
  import { Dimensions as Dimensions3, Platform as Platform8 } from "react-native";
3307
- import AsyncStorage3 from "@react-native-async-storage/async-storage";
3308
- import RNDeviceInfo from "react-native-device-info";
3309
3474
 
3310
3475
  // src/domains/identity/MetaBridge.ts
3311
- import { NativeModules as NativeModules4 } from "react-native";
3476
+ import { NativeModules as NativeModules6 } from "react-native";
3312
3477
  var TIMEOUT_MS = 2e3;
3313
- var NativeBridge = NativeModules4.PaywalloFBBridge ?? null;
3478
+ var NativeBridge = NativeModules6.PaywalloFBBridge ?? null;
3314
3479
  var MetaBridge = class {
3315
3480
  async getAnonymousID() {
3316
3481
  if (!NativeBridge) return null;
@@ -3376,8 +3541,7 @@ var InstallReferrerManager = class {
3376
3541
  }
3377
3542
  async fetchReferrer() {
3378
3543
  try {
3379
- const Application = await import("expo-application");
3380
- const referrer = await Application.getInstallReferrerAsync();
3544
+ const referrer = await nativeDeviceInfo.getInstallReferrer();
3381
3545
  if (!referrer) return this.emptyResult();
3382
3546
  const capped = referrer.slice(0, 2048);
3383
3547
  const params = this.parseReferrer(capped);
@@ -3425,14 +3589,42 @@ var installReferrerManager = new InstallReferrerManager();
3425
3589
  import { AppState as AppState2 } from "react-native";
3426
3590
  init_uuid();
3427
3591
 
3592
+ // src/utils/deviceInfo.ts
3593
+ async function collectDeviceInfo() {
3594
+ const result = await nativeDeviceInfo.getDeviceInfo();
3595
+ return {
3596
+ appVersion: result.appVersion,
3597
+ deviceModel: result.model,
3598
+ osVersion: result.systemVersion
3599
+ };
3600
+ }
3601
+
3428
3602
  // src/domains/session/sessionTracking.ts
3429
3603
  async function trackSessionStart(apiClient, sessionId, sessionStartedAt) {
3430
3604
  const distinctId = identityManager.getDistinctId();
3431
- await apiClient.trackEvent("$session_start", distinctId, {
3432
- sessionId,
3433
- timestamp: sessionStartedAt?.toISOString() ?? null
3434
- }).catch(() => {
3435
- });
3605
+ const deviceInfo = await collectDeviceInfo().catch(() => ({
3606
+ appVersion: "unknown",
3607
+ deviceModel: "unknown",
3608
+ osVersion: "unknown"
3609
+ }));
3610
+ await Promise.all([
3611
+ apiClient.startSession(
3612
+ distinctId,
3613
+ sessionId,
3614
+ void 0,
3615
+ deviceInfo.appVersion,
3616
+ deviceInfo.deviceModel,
3617
+ deviceInfo.osVersion
3618
+ ),
3619
+ apiClient.trackEvent("$session_start", distinctId, {
3620
+ sessionId,
3621
+ timestamp: sessionStartedAt?.toISOString() ?? null,
3622
+ appVersion: deviceInfo.appVersion,
3623
+ deviceModel: deviceInfo.deviceModel,
3624
+ osVersion: deviceInfo.osVersion
3625
+ }).catch(() => {
3626
+ })
3627
+ ]);
3436
3628
  }
3437
3629
  async function trackSessionEnd(apiClient, sessionId, durationSeconds, sessionStartedAt) {
3438
3630
  const distinctId = identityManager.getDistinctId();
@@ -3529,6 +3721,9 @@ var SessionManager = class {
3529
3721
  if (!this.sessionId || !this.sessionStartedAt) {
3530
3722
  return;
3531
3723
  }
3724
+ if (this.apiClient) {
3725
+ await this.apiClient.flushEventBatch();
3726
+ }
3532
3727
  await this.trackSessionEnd();
3533
3728
  this.sessionId = null;
3534
3729
  this.sessionStartedAt = null;
@@ -3613,6 +3808,9 @@ var SessionManager = class {
3613
3808
  async handleBackground() {
3614
3809
  this.backgroundTimestamp = Date.now();
3615
3810
  await this.trackAppBackground();
3811
+ if (this.apiClient) {
3812
+ await this.apiClient.flushEventBatch();
3813
+ }
3616
3814
  }
3617
3815
  async checkEmergencyPaywall() {
3618
3816
  if (!this.apiClient || !this.emergencyPaywallHandler) {
@@ -3661,16 +3859,26 @@ var sessionManager = new SessionManager();
3661
3859
  // src/domains/identity/InstallTracker.ts
3662
3860
  var InstallTracker = class {
3663
3861
  constructor(debug = false, advertisingIdManager) {
3862
+ this._trackingInProgress = false;
3664
3863
  this.debug = debug;
3665
3864
  this.advertisingIdManager = advertisingIdManager ?? new AdvertisingIdManager();
3666
3865
  }
3667
3866
  async trackIfNeeded(apiClient, requestATT = false) {
3867
+ if (this._trackingInProgress) return;
3868
+ this._trackingInProgress = true;
3869
+ try {
3870
+ await this._doTrack(apiClient, requestATT);
3871
+ } finally {
3872
+ this._trackingInProgress = false;
3873
+ }
3874
+ }
3875
+ async _doTrack(apiClient, requestATT) {
3668
3876
  const storage = new SecureStorage(this.debug);
3669
3877
  const alreadyTracked = await storage.get("@panel:install_tracked");
3670
3878
  if (alreadyTracked) return;
3671
3879
  let fallbackTracked = null;
3672
3880
  try {
3673
- fallbackTracked = await AsyncStorage3.getItem("@paywallo:install_tracked");
3881
+ fallbackTracked = await nativeStorage.get("@paywallo:install_tracked");
3674
3882
  } catch {
3675
3883
  }
3676
3884
  if (fallbackTracked) return;
@@ -3678,7 +3886,6 @@ var InstallTracker = class {
3678
3886
  const sessionId = sessionManager.getSessionId();
3679
3887
  const installedAt = Date.now();
3680
3888
  if (!distinctId) return;
3681
- const deviceInfo = RNDeviceInfo;
3682
3889
  let deviceData = {};
3683
3890
  try {
3684
3891
  const screen = Dimensions3.get("screen");
@@ -3686,39 +3893,40 @@ var InstallTracker = class {
3686
3893
  const screenHeight = Math.round(screen.height ?? 0);
3687
3894
  const locale = Intl.DateTimeFormat().resolvedOptions().locale ?? "unknown";
3688
3895
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "unknown";
3689
- const deviceModel = deviceInfo?.getModel() || "unknown";
3690
- const osVersion = deviceInfo?.getSystemVersion() || "unknown";
3691
- const appVersion = deviceInfo?.getVersion() || "unknown";
3692
- const buildNumber = deviceInfo?.getBuildNumber() || "0";
3896
+ const info = await nativeDeviceInfo.getDeviceInfo();
3897
+ const deviceModel = info.modelId || info.model || "unknown";
3898
+ const osVersion = info.systemVersion || "unknown";
3899
+ const appVersion = info.appVersion || "unknown";
3900
+ const buildNumber = info.buildNumber || "0";
3693
3901
  let carrier = "unknown";
3694
3902
  try {
3695
- carrier = await deviceInfo?.getCarrier?.() ?? "unknown";
3903
+ carrier = info.carrier ?? "unknown";
3696
3904
  } catch {
3697
3905
  }
3698
3906
  let totalDisk = 0;
3699
3907
  try {
3700
- totalDisk = Number(await deviceInfo?.getTotalDiskCapacity?.()) || 0;
3908
+ totalDisk = Number(info.totalDisk) || 0;
3701
3909
  } catch {
3702
3910
  }
3703
3911
  let freeDisk = 0;
3704
3912
  try {
3705
- freeDisk = Number(await deviceInfo?.getFreeDiskStorage?.()) || 0;
3913
+ freeDisk = Number(info.freeDisk) || 0;
3706
3914
  } catch {
3707
3915
  }
3708
3916
  let totalRam = 0;
3709
3917
  try {
3710
- totalRam = Number(await deviceInfo?.getTotalMemory?.()) || 0;
3918
+ totalRam = Number(info.totalRam) || 0;
3711
3919
  } catch {
3712
3920
  }
3713
3921
  let brand = "unknown";
3714
3922
  try {
3715
- brand = deviceInfo?.getBrand?.() ?? "unknown";
3923
+ brand = info.brand ?? "unknown";
3716
3924
  } catch {
3717
3925
  }
3718
3926
  let installerPackage = null;
3719
3927
  try {
3720
3928
  if (Platform8.OS === "android") {
3721
- installerPackage = await deviceInfo?.getInstallerPackageName?.() ?? null;
3929
+ installerPackage = info.installerPackage ?? null;
3722
3930
  }
3723
3931
  } catch {
3724
3932
  }
@@ -3757,37 +3965,34 @@ var InstallTracker = class {
3757
3965
  });
3758
3966
  }
3759
3967
  }
3760
- try {
3761
- await apiClient.trackEvent("$app_installed", distinctId, {
3762
- installedAt,
3763
- platform: Platform8.OS,
3764
- ...sessionId && { sessionId },
3765
- ...deviceData,
3766
- ...effectiveAnonId && { fbAnonId: effectiveAnonId },
3767
- ...referrer.rawReferrer && { installReferrer: referrer.rawReferrer },
3768
- ...referrer.trackingId && { referrerTrackingId: referrer.trackingId },
3769
- ...referrer.fbclid && { referrerFbclid: referrer.fbclid },
3770
- ...referrer.utmSource && { referrerUtmSource: referrer.utmSource },
3771
- ...referrer.utmMedium && { referrerUtmMedium: referrer.utmMedium },
3772
- ...referrer.utmCampaign && { referrerUtmCampaign: referrer.utmCampaign },
3773
- ...adIds.idfa && { idfa: adIds.idfa },
3774
- ...adIds.idfv && { idfv: adIds.idfv },
3775
- ...adIds.gaid && { gaid: adIds.gaid },
3776
- attStatus: adIds.attStatus
3968
+ await apiClient.trackEvent("$app_installed", distinctId, {
3969
+ installedAt,
3970
+ platform: Platform8.OS,
3971
+ ...sessionId && { sessionId },
3972
+ ...deviceData,
3973
+ ...effectiveAnonId && { fbAnonId: effectiveAnonId },
3974
+ ...referrer.rawReferrer && { installReferrer: referrer.rawReferrer },
3975
+ ...referrer.trackingId && { referrerTrackingId: referrer.trackingId },
3976
+ ...referrer.fbclid && { referrerFbclid: referrer.fbclid },
3977
+ ...referrer.utmSource && { referrerUtmSource: referrer.utmSource },
3978
+ ...referrer.utmMedium && { referrerUtmMedium: referrer.utmMedium },
3979
+ ...referrer.utmCampaign && { referrerUtmCampaign: referrer.utmCampaign },
3980
+ ...adIds.idfa && { idfa: adIds.idfa },
3981
+ ...adIds.idfv && { idfv: adIds.idfv },
3982
+ ...adIds.gaid && { gaid: adIds.gaid },
3983
+ attStatus: adIds.attStatus
3984
+ });
3985
+ if (Platform8.OS === "ios") {
3986
+ void this.callDeferredMatch(apiClient, deviceData, adIds, effectiveAnonId, installedAt).catch(() => {
3777
3987
  });
3778
- if (Platform8.OS === "ios") {
3779
- void this.callDeferredMatch(apiClient, deviceData, adIds, effectiveAnonId).catch(() => {
3780
- });
3781
- }
3782
- await storage.set("@panel:install_tracked", String(installedAt));
3783
- try {
3784
- await AsyncStorage3.setItem("@paywallo:install_tracked", String(installedAt));
3785
- } catch {
3786
- }
3988
+ }
3989
+ await storage.set("@panel:install_tracked", String(installedAt));
3990
+ try {
3991
+ await nativeStorage.set("@paywallo:install_tracked", String(installedAt));
3787
3992
  } catch {
3788
3993
  }
3789
3994
  }
3790
- async callDeferredMatch(apiClient, deviceData, adIds, anonId) {
3995
+ async callDeferredMatch(apiClient, deviceData, adIds, anonId, installedAt) {
3791
3996
  const storage = new SecureStorage(this.debug);
3792
3997
  const alreadyMatched = await storage.get("@panel:deferred_match_done");
3793
3998
  if (alreadyMatched) return;
@@ -3795,7 +4000,7 @@ var InstallTracker = class {
3795
4000
  const timer = setTimeout(() => controller.abort(), 5e3);
3796
4001
  try {
3797
4002
  const baseUrl = apiClient.getBaseUrl();
3798
- const appKey = apiClient.appKey;
4003
+ const appKey = apiClient.getAppKey();
3799
4004
  await fetch(`${baseUrl}/api/v1/attribution/deferred-match`, {
3800
4005
  method: "POST",
3801
4006
  headers: { "Content-Type": "application/json", "X-App-Key": appKey },
@@ -3807,7 +4012,8 @@ var InstallTracker = class {
3807
4012
  screenHeight: deviceData.screenHeight,
3808
4013
  timezone: deviceData.timezone,
3809
4014
  locale: deviceData.locale,
3810
- fbAnonId: anonId
4015
+ fbAnonId: anonId,
4016
+ installTimestamp: new Date(installedAt).toISOString()
3811
4017
  }),
3812
4018
  signal: controller.signal
3813
4019
  });
@@ -3932,7 +4138,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3932
4138
  error: error instanceof Error ? error.message : String(error)
3933
4139
  });
3934
4140
  } catch (err) {
3935
- if (config.debug) console.warn("[Paywallo] reportInitFailure error", err);
4141
+ if (config.debug) console.error("[Paywallo] reportInitFailure error", err);
3936
4142
  }
3937
4143
  }
3938
4144
  reportInitSuccess(config, attempts) {
@@ -3941,7 +4147,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3941
4147
  void this.apiClient.reportError("INIT_RECOVERED", `Succeeded after ${attempts} retries`, { attempts });
3942
4148
  }
3943
4149
  } catch (err) {
3944
- if (config.debug) console.warn("[Paywallo] reportInitSuccess error", err);
4150
+ if (config.debug) console.error("[Paywallo] reportInitSuccess error", err);
3945
4151
  }
3946
4152
  }
3947
4153
  async resolveSessionFlags(keys, apiClient, distinctId) {
@@ -3968,7 +4174,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3968
4174
  ]);
3969
4175
  this.apiClient = new ApiClient(config.appKey, config.apiUrl ?? "https://paywallo.com.br", config.debug, environment, offlineQueueEnabled, config.timeout);
3970
4176
  queueProcessor.initialize(this.apiClient.getHttpClient(), { debug: config.debug });
3971
- affiliateManager.initialize(this.apiClient, config.debug);
3972
4177
  subscriptionManager.init({
3973
4178
  serverUrl: this.apiClient.getBaseUrl(),
3974
4179
  appKey: config.appKey,
@@ -3989,18 +4194,26 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3989
4194
  if (config.sessionFlags && config.sessionFlags.length > 0) {
3990
4195
  await this.resolveSessionFlags(config.sessionFlags, this.apiClient, distinctId);
3991
4196
  }
4197
+ setNativeStorageDebug(config.debug ?? false);
4198
+ setNativeDeviceInfoDebug(config.debug ?? false);
3992
4199
  this.config = config;
3993
4200
  if (this.resolveReady) {
3994
4201
  this.resolveReady();
3995
4202
  this.resolveReady = null;
3996
4203
  }
3997
- if (config.debug) console.info("[Paywallo INIT] complete");
3998
- if (this.apiClient) {
4204
+ if (config.debug) console.log("[Paywallo INIT] complete");
4205
+ if (distinctId) {
4206
+ this.apiClient.getSubscriptionStatus(distinctId).then(() => {
4207
+ if (config.debug) console.log("[Paywallo SUB] preloaded on boot");
4208
+ }).catch(() => {
4209
+ });
4210
+ }
4211
+ {
3999
4212
  const apiClientRef = this.apiClient;
4000
4213
  if (config.autoPreloadCampaign) {
4001
4214
  this.autoPreloadedPlacement = config.autoPreloadCampaign;
4002
4215
  this.autoPreloadPlacementPromise = Promise.resolve(config.autoPreloadCampaign);
4003
- if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${config.autoPreloadCampaign}`);
4216
+ if (config.debug) console.log(`[Paywallo PRELOAD] started \u2014 placement: ${config.autoPreloadCampaign}`);
4004
4217
  void campaignGateService.preloadCampaign(apiClientRef, config.autoPreloadCampaign).catch(() => {
4005
4218
  });
4006
4219
  } else {
@@ -4008,7 +4221,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4008
4221
  const placement = primary?.placement ?? primary?.paywall?.placement;
4009
4222
  if (placement) {
4010
4223
  this.autoPreloadedPlacement = placement;
4011
- if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${placement}`);
4224
+ if (config.debug) console.log(`[Paywallo PRELOAD] started \u2014 placement: ${placement}`);
4012
4225
  void campaignGateService.preloadCampaign(apiClientRef, placement).catch(() => {
4013
4226
  });
4014
4227
  return placement;
@@ -4020,21 +4233,21 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4020
4233
  }
4021
4234
  async identify(options) {
4022
4235
  this.ensureInitialized();
4023
- if (this.config?.debug) console.info("[Paywallo IDENTIFY]", options);
4236
+ if (this.config?.debug) console.log("[Paywallo IDENTIFY]", options ? { hasEmail: "email" in options && !!options.email, hasProperties: "properties" in options } : void 0);
4024
4237
  await identityManager.identify(options);
4025
4238
  }
4026
4239
  async track(eventName, options) {
4027
4240
  const apiClient = this.getApiClientOrThrow();
4028
4241
  const distinctId = identityManager.getDistinctId();
4029
4242
  if (!distinctId) {
4030
- if (this.config?.debug) console.info(`[Paywallo TRACK] ${eventName} skipped (no distinctId)`);
4243
+ if (this.config?.debug) console.log(`[Paywallo TRACK] ${eventName} skipped (no distinctId)`);
4031
4244
  return;
4032
4245
  }
4033
4246
  if (!isValidEventName(eventName)) {
4034
4247
  const msg = `Invalid event name: "${eventName}". Use snake_case or $reserved_name.`;
4035
4248
  throw new ClientError(CLIENT_ERROR_CODES.INVALID_EVENT_NAME, msg);
4036
4249
  }
4037
- if (this.config?.debug && eventName.startsWith("$purchase")) console.info(`[Paywallo TRACK] ${eventName}`, options?.properties ?? {});
4250
+ if (this.config?.debug && eventName.startsWith("$purchase")) console.log(`[Paywallo TRACK] ${eventName}`, options?.properties ?? {});
4038
4251
  const sessionId = sessionManager.getSessionId();
4039
4252
  await apiClient.trackEvent(eventName, distinctId, {
4040
4253
  ...identityManager.getProperties(),
@@ -4093,14 +4306,14 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4093
4306
  try {
4094
4307
  const distinctId = identityManager.getDistinctId();
4095
4308
  if (!distinctId) {
4096
- if (this.config?.debug) console.info(`[Paywallo FLAG] ${flagKey} \u2192 null (no distinctId)`);
4309
+ if (this.config?.debug) console.log(`[Paywallo FLAG] ${flagKey} \u2192 null (no distinctId)`);
4097
4310
  return { variant: null };
4098
4311
  }
4099
4312
  const result = await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
4100
- if (this.config?.debug) console.info(`[Paywallo FLAG] ${flagKey} \u2192`, result.variant);
4313
+ if (this.config?.debug) console.log(`[Paywallo FLAG] ${flagKey} \u2192`, result.variant);
4101
4314
  return result;
4102
4315
  } catch (error) {
4103
- if (this.config?.debug) console.warn(`[Paywallo FLAG] ${flagKey} \u2192 error`, error);
4316
+ if (this.config?.debug) console.log(`[Paywallo FLAG] ${flagKey} \u2192 error`, error);
4104
4317
  if (this.apiClient) void this.apiClient.reportError("GET_VARIANT_FAILED", error instanceof Error ? error.message : String(error));
4105
4318
  return { variant: null };
4106
4319
  }
@@ -4133,11 +4346,11 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4133
4346
  }
4134
4347
  async presentCampaign(placement, context) {
4135
4348
  this.ensureInitialized();
4136
- if (this.config?.debug) console.info(`[Paywallo CAMPAIGN] presenting ${placement}`);
4349
+ if (this.config?.debug) console.log(`[Paywallo CAMPAIGN] presenting ${placement}`);
4137
4350
  const result = await campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, () => this.hasActiveSubscription(), this.paywallPresenter, this.campaignPresenter, context);
4138
4351
  if (this.config?.debug) {
4139
- if (result.error) console.warn(`[Paywallo CAMPAIGN] ${placement} error:`, result.error.message);
4140
- else console.info(`[Paywallo CAMPAIGN] ${placement} result:`, { purchased: result.purchased, cancelled: result.cancelled, restored: result.restored, skippedReason: result.skippedReason });
4352
+ if (result.error) console.log(`[Paywallo CAMPAIGN] ${placement} error:`, result.error.message);
4353
+ else console.log(`[Paywallo CAMPAIGN] ${placement} result:`, { purchased: result.purchased, cancelled: result.cancelled, restored: result.restored, skippedReason: result.skippedReason });
4141
4354
  }
4142
4355
  return result;
4143
4356
  }
@@ -4147,20 +4360,20 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4147
4360
  if (this.activeChecker) {
4148
4361
  const result = await this.activeChecker();
4149
4362
  const isActive2 = result === true;
4150
- if (this.config?.debug) console.info("[Paywallo SUB] hasActiveSubscription (checker):", isActive2);
4363
+ if (this.config?.debug) console.log("[Paywallo SUB] hasActiveSubscription (checker):", isActive2);
4151
4364
  return isActive2;
4152
4365
  }
4153
4366
  const distinctId = identityManager.getDistinctId();
4154
4367
  if (!distinctId || !this.apiClient) {
4155
- if (this.config?.debug) console.info("[Paywallo SUB] hasActiveSubscription: false (no distinctId/apiClient)");
4368
+ if (this.config?.debug) console.log("[Paywallo SUB] hasActiveSubscription: false (no distinctId/apiClient)");
4156
4369
  return false;
4157
4370
  }
4158
4371
  const status = await this.apiClient.getSubscriptionStatus(distinctId);
4159
4372
  const isActive = status.hasActiveSubscription === true;
4160
- if (this.config?.debug) console.info("[Paywallo SUB] hasActiveSubscription (server):", isActive);
4373
+ if (this.config?.debug) console.log("[Paywallo SUB] hasActiveSubscription (server):", isActive);
4161
4374
  return isActive;
4162
4375
  } catch (error) {
4163
- if (this.config?.debug) console.warn("[Paywallo SUB] hasActiveSubscription error", error);
4376
+ if (this.config?.debug) console.error("[Paywallo SUB] hasActiveSubscription error", error);
4164
4377
  if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", error instanceof Error ? error.message : "hasActiveSubscription failed");
4165
4378
  return false;
4166
4379
  }
@@ -4181,9 +4394,9 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4181
4394
  async restorePurchases() {
4182
4395
  this.ensureInitialized();
4183
4396
  if (!this.restoreHandler) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
4184
- if (this.config?.debug) console.info("[Paywallo RESTORE] starting");
4397
+ if (this.config?.debug) console.log("[Paywallo RESTORE] starting");
4185
4398
  const result = await this.restoreHandler();
4186
- if (this.config?.debug) console.info("[Paywallo RESTORE] result:", { success: result.success, restoredProducts: result.restoredProducts?.length ?? 0 });
4399
+ if (this.config?.debug) console.log("[Paywallo RESTORE] result:", { success: result.success, restoredProducts: result.restoredProducts?.length ?? 0 });
4187
4400
  return result;
4188
4401
  }
4189
4402
  async reset() {
@@ -4193,6 +4406,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4193
4406
  async fullReset() {
4194
4407
  const wasDebug = this.config?.debug ?? false;
4195
4408
  await sessionManager.endSession();
4409
+ sessionManager.destroy();
4196
4410
  await identityManager.reset();
4197
4411
  await subscriptionCache.invalidateAll();
4198
4412
  queueProcessor.dispose();
@@ -4201,6 +4415,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4201
4415
  campaignGateService.clearPreloaded();
4202
4416
  this.cleanupNetworkRecovery();
4203
4417
  this.sessionFlagsMap.clear();
4418
+ this.apiClient?.dispose();
4204
4419
  this.apiClient = null;
4205
4420
  this.config = null;
4206
4421
  this.initPromise = null;
@@ -4279,6 +4494,10 @@ var _PaywalloClientClass = class _PaywalloClientClass {
4279
4494
  registerEmergencyPaywallHandler(h) {
4280
4495
  sessionManager.registerEmergencyPaywallHandler(h);
4281
4496
  }
4497
+ async getEmergencyPaywall() {
4498
+ if (!this.apiClient) throw new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, "SDK not initialized");
4499
+ return this.apiClient.getEmergencyPaywall();
4500
+ }
4282
4501
  async requireSubscription(paywallPlacement) {
4283
4502
  this.ensureInitialized();
4284
4503
  if (await this.hasActiveSubscription()) return true;
@@ -4533,6 +4752,7 @@ function usePurchase() {
4533
4752
 
4534
4753
  // src/hooks/useSubscription.ts
4535
4754
  import { useCallback as useCallback8, useEffect as useEffect6, useState as useState8 } from "react";
4755
+ var EMPTY_ENTITLEMENTS = [];
4536
4756
  function useSubscription() {
4537
4757
  const [status, setStatus] = useState8(null);
4538
4758
  const [isLoading, setIsLoading] = useState8(true);
@@ -4568,7 +4788,8 @@ function useSubscription() {
4568
4788
  setStatus(s);
4569
4789
  setIsLoading(false);
4570
4790
  }).catch((err) => {
4571
- setError(err instanceof Error ? err : new Error(String(err)));
4791
+ const e = err instanceof PurchaseError ? err : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, String(err));
4792
+ setError(e);
4572
4793
  setIsLoading(false);
4573
4794
  });
4574
4795
  const removeListener = subscriptionManager.addListener((newStatus) => {
@@ -4583,7 +4804,7 @@ function useSubscription() {
4583
4804
  isActive: status?.hasActiveSubscription ?? false,
4584
4805
  isLoading,
4585
4806
  error,
4586
- entitlements: status?.entitlements ?? [],
4807
+ entitlements: status?.entitlements ?? EMPTY_ENTITLEMENTS,
4587
4808
  refresh,
4588
4809
  restore
4589
4810
  };
@@ -4715,7 +4936,7 @@ function useCampaignPreload() {
4715
4936
  campaignId: campaign.campaignId,
4716
4937
  variantKey: campaign.variantKey
4717
4938
  });
4718
- if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo WEBVIEW] mounting hidden \u2014 placement: ${placement}`);
4939
+ if (PaywalloClient.getConfig()?.debug) console.log(`[Paywallo WEBVIEW] mounting hidden \u2014 placement: ${placement}`);
4719
4940
  return { success: true };
4720
4941
  } catch (error) {
4721
4942
  return {
@@ -4750,7 +4971,7 @@ function useProductLoader() {
4750
4971
  for (const p of loaded) map.set(p.productId, p);
4751
4972
  setProducts(map);
4752
4973
  } catch (err) {
4753
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] refreshProducts failed", err);
4974
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] refreshProducts failed", err);
4754
4975
  } finally {
4755
4976
  setIsLoadingProducts(false);
4756
4977
  }
@@ -4790,7 +5011,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4790
5011
  clearPreloadedWebView();
4791
5012
  resolvePreloadedPaywall({ presented: true, purchased: true, cancelled: false, restored: false });
4792
5013
  } catch (error) {
4793
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] preloaded purchase failed", error);
5014
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] preloaded purchase failed", error);
4794
5015
  setShowingPreloadedWebView(false);
4795
5016
  clearPreloadedWebView();
4796
5017
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
@@ -4811,7 +5032,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4811
5032
  clearPreloadedWebView();
4812
5033
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: true });
4813
5034
  } catch (error) {
4814
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] preloaded restore failed", error);
5035
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] preloaded restore failed", error);
4815
5036
  setShowingPreloadedWebView(false);
4816
5037
  clearPreloadedWebView();
4817
5038
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
@@ -4823,12 +5044,29 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4823
5044
  // src/hooks/useSubscriptionSync.ts
4824
5045
  import { useCallback as useCallback12, useRef as useRef7, useState as useState11 } from "react";
4825
5046
  var CACHE_TTL_MS2 = 5 * 60 * 1e3;
4826
- var ACTIVE_STATUSES = ["active", "grace_period"];
5047
+ var ACTIVE_STATUSES = ["active", "in_grace_period"];
5048
+ var PUBLIC_TO_DOMAIN_STATUS = {
5049
+ active: "active",
5050
+ expired: "expired",
5051
+ in_billing_retry: "billing_retry",
5052
+ in_grace_period: "grace_period",
5053
+ revoked: "unknown",
5054
+ cancelled: "canceled"
5055
+ };
5056
+ var DOMAIN_TO_PUBLIC_STATUS = {
5057
+ active: "active",
5058
+ expired: "expired",
5059
+ billing_retry: "in_billing_retry",
5060
+ grace_period: "in_grace_period",
5061
+ canceled: "cancelled",
5062
+ paused: "expired",
5063
+ unknown: "expired"
5064
+ };
4827
5065
  function toDomainSubscriptionInfo(sub) {
4828
5066
  return {
4829
5067
  id: "",
4830
5068
  productId: sub.productId,
4831
- status: sub.status,
5069
+ status: PUBLIC_TO_DOMAIN_STATUS[sub.status] ?? "unknown",
4832
5070
  expiresAt: sub.expiresAt ?? /* @__PURE__ */ new Date(0),
4833
5071
  originalPurchaseDate: /* @__PURE__ */ new Date(0),
4834
5072
  latestPurchaseDate: /* @__PURE__ */ new Date(0),
@@ -4842,7 +5080,7 @@ function toDomainResponse(status) {
4842
5080
  return {
4843
5081
  hasActiveSubscription: status.hasActiveSubscription,
4844
5082
  subscription: status.subscription ? toDomainSubscriptionInfo(status.subscription) : null,
4845
- entitlements: []
5083
+ entitlements: status.entitlements ?? []
4846
5084
  };
4847
5085
  }
4848
5086
  function isSubscriptionStillActive(sub) {
@@ -4851,6 +5089,10 @@ function isSubscriptionStillActive(sub) {
4851
5089
  if (sub.expiresAt && sub.expiresAt.getTime() !== 0 && sub.expiresAt < /* @__PURE__ */ new Date()) return false;
4852
5090
  return true;
4853
5091
  }
5092
+ function subscriptionInfoToSub(s) {
5093
+ const status = DOMAIN_TO_PUBLIC_STATUS[s.status] ?? "expired";
5094
+ return { productId: s.productId, status, expiresAt: s.expiresAt ?? null, platform: s.platform, autoRenewEnabled: s.willRenew, inGracePeriod: status === "in_grace_period" };
5095
+ }
4854
5096
  async function resolveOfflineFromCache(distinctId) {
4855
5097
  try {
4856
5098
  const cached = await subscriptionCache.get(distinctId);
@@ -4891,23 +5133,12 @@ function useSubscriptionSync() {
4891
5133
  const distinctId = PaywalloClient.getDistinctId();
4892
5134
  if (!apiClient || !distinctId) return false;
4893
5135
  const cached = cacheRef.current;
4894
- if (cached && cached.expiresAt > Date.now()) {
4895
- return isActiveCacheEntry(cached);
4896
- }
5136
+ if (cached && cached.expiresAt > Date.now()) return isActiveCacheEntry(cached);
4897
5137
  try {
4898
5138
  const persisted = await subscriptionCache.get(distinctId);
4899
5139
  if (persisted && !persisted.isStale) {
4900
- const persistedSub = persisted.data.subscription;
4901
- const sub = persistedSub ? {
4902
- productId: persistedSub.productId,
4903
- status: persistedSub.status,
4904
- expiresAt: persistedSub.expiresAt ?? null,
4905
- platform: persistedSub.platform,
4906
- autoRenewEnabled: persistedSub.willRenew,
4907
- inGracePeriod: persistedSub.status === "grace_period"
4908
- } : null;
4909
- const stillActive = isSubscriptionStillActive(sub);
4910
- if (stillActive) {
5140
+ const sub = persisted.data.subscription ? subscriptionInfoToSub(persisted.data.subscription) : null;
5141
+ if (isSubscriptionStillActive(sub)) {
4911
5142
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4912
5143
  setSubscription(sub);
4913
5144
  return true;
@@ -4916,17 +5147,14 @@ function useSubscriptionSync() {
4916
5147
  } catch {
4917
5148
  }
4918
5149
  try {
4919
- const status = await apiClient.getSubscriptionStatus(distinctId);
4920
- const sub = status.subscription ? {
4921
- ...status.subscription,
4922
- expiresAt: status.subscription.expiresAt ? new Date(status.subscription.expiresAt) : null
4923
- } : null;
5150
+ const apiStatus = await apiClient.getSubscriptionStatus(distinctId);
5151
+ const sub = apiStatus.subscription ? { ...apiStatus.subscription, expiresAt: apiStatus.subscription.expiresAt ? new Date(apiStatus.subscription.expiresAt) : null } : null;
4924
5152
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4925
5153
  setSubscription(sub);
4926
- void subscriptionCache.set(distinctId, toDomainResponse(status));
4927
- return status.hasActiveSubscription === true;
5154
+ void subscriptionCache.set(distinctId, toDomainResponse(apiStatus));
5155
+ return apiStatus.hasActiveSubscription === true;
4928
5156
  } catch {
4929
- return await resolveOfflineFromCache(distinctId);
5157
+ return resolveOfflineFromCache(distinctId);
4930
5158
  }
4931
5159
  }, []);
4932
5160
  const getSubscription = useCallback12(async () => {
@@ -4941,16 +5169,7 @@ function useSubscriptionSync() {
4941
5169
  try {
4942
5170
  const persisted = await subscriptionCache.get(distinctId);
4943
5171
  if (!persisted?.data.subscription) return null;
4944
- const s = persisted.data.subscription;
4945
- const normalisedStatus = s.status === "grace_period" ? "in_grace_period" : s.status;
4946
- return {
4947
- productId: s.productId,
4948
- status: normalisedStatus,
4949
- expiresAt: s.expiresAt ?? null,
4950
- platform: s.platform,
4951
- autoRenewEnabled: s.willRenew,
4952
- inGracePeriod: normalisedStatus === "in_grace_period"
4953
- };
5172
+ return subscriptionInfoToSub(persisted.data.subscription);
4954
5173
  } catch {
4955
5174
  return null;
4956
5175
  }
@@ -4972,6 +5191,8 @@ function PaywalloProvider({ children, config }) {
4972
5191
  const clearPreloadedWebView = useCallback13(() => setPreloadedWebView(null), [setPreloadedWebView]);
4973
5192
  const autoPreloadTriggeredRef = useRef8(false);
4974
5193
  const preloadedWebViewRef = useRef8(preloadedWebView);
5194
+ const pollTimerRef = useRef8(null);
5195
+ const pollCancelledRef = useRef8(false);
4975
5196
  useEffect7(() => {
4976
5197
  preloadedWebViewRef.current = preloadedWebView;
4977
5198
  }, [preloadedWebView]);
@@ -4980,21 +5201,21 @@ function PaywalloProvider({ children, config }) {
4980
5201
  });
4981
5202
  }, []);
4982
5203
  const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady: baseHandlePreloadedWebViewReady, handlePreloadedClose: baseHandlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, setPreloadedWebView);
4983
- const SCREEN_HEIGHT3 = useMemo2(() => Dimensions4.get("window").height, []);
4984
- const slideAnim = useRef8(new Animated3.Value(SCREEN_HEIGHT3)).current;
5204
+ const SCREEN_HEIGHT = useMemo2(() => Dimensions4.get("window").height, []);
5205
+ const slideAnim = useRef8(new Animated3.Value(SCREEN_HEIGHT)).current;
4985
5206
  const handlePreloadedWebViewReady = useCallback13(() => {
4986
5207
  if (PaywalloClient.getConfig()?.debug) {
4987
- console.info(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
5208
+ console.log(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
4988
5209
  }
4989
5210
  baseHandlePreloadedWebViewReady();
4990
- }, [config?.debug, preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
5211
+ }, [preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
4991
5212
  const handlePreloadedClose = useCallback13(() => {
4992
5213
  const placement = preloadedWebView?.placement;
4993
- Animated3.timing(slideAnim, { toValue: SCREEN_HEIGHT3, duration: 200, easing: Easing2.in(Easing2.cubic), useNativeDriver: true }).start(() => {
5214
+ Animated3.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 200, easing: Easing2.in(Easing2.cubic), useNativeDriver: true }).start(() => {
4994
5215
  baseHandlePreloadedClose();
4995
5216
  if (placement) triggerRepreload(placement);
4996
5217
  });
4997
- }, [baseHandlePreloadedClose, preloadedWebView, triggerRepreload, slideAnim, SCREEN_HEIGHT3]);
5218
+ }, [baseHandlePreloadedClose, preloadedWebView, triggerRepreload, slideAnim, SCREEN_HEIGHT]);
4998
5219
  const { handlePreloadedPurchase: rawPreloadedPurchase, handlePreloadedRestore } = usePurchaseOrchestrator(
4999
5220
  preloadedWebView,
5000
5221
  resolvePreloadedPaywall,
@@ -5012,21 +5233,28 @@ function PaywalloProvider({ children, config }) {
5012
5233
  setIsPreloadPurchasing(false);
5013
5234
  }
5014
5235
  }, [rawPreloadedPurchase]);
5236
+ const configRef = useRef8(config);
5015
5237
  useEffect7(() => {
5238
+ let mounted = true;
5016
5239
  initLocalization({ detectDevice: true });
5017
- PaywalloClient.init(config).then(async () => {
5240
+ PaywalloClient.init(configRef.current).then(async () => {
5241
+ if (!mounted) return;
5018
5242
  setDistinctId(PaywalloClient.getDistinctId());
5019
5243
  setIsInitialized(true);
5020
5244
  if (!autoPreloadTriggeredRef.current) {
5021
5245
  autoPreloadTriggeredRef.current = true;
5022
5246
  const placement = await PaywalloClient.waitForAutoPreloadedPlacement();
5023
- if (placement) void preloadCampaign(placement).catch(() => {
5247
+ if (mounted && placement) void preloadCampaign(placement).catch(() => {
5024
5248
  });
5025
5249
  }
5026
5250
  }).catch((err) => {
5251
+ if (!mounted) return;
5027
5252
  setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
5028
5253
  });
5029
- }, [config, preloadCampaign]);
5254
+ return () => {
5255
+ mounted = false;
5256
+ };
5257
+ }, [preloadCampaign]);
5030
5258
  const getPaywallConfig = useCallback13(async (p) => {
5031
5259
  const api = PaywalloClient.getApiClient();
5032
5260
  return api ? api.getPaywall(p) : null;
@@ -5127,15 +5355,17 @@ function PaywalloProvider({ children, config }) {
5127
5355
  }, [activePaywall]);
5128
5356
  const bridgePaywallPresenter = useCallback13((placement) => {
5129
5357
  if (PaywalloClient.getConfig()?.debug) {
5130
- console.info(`[Paywallo PRESENT] called \u2014 preloaded: ${preloadedWebViewRef.current?.loaded === true} placement: ${placement}`);
5358
+ console.log(`[Paywallo PRESENT] called \u2014 preloaded: ${preloadedWebViewRef.current?.loaded === true} placement: ${placement}`);
5131
5359
  }
5132
5360
  if (preloadedWebViewRef.current?.placement === placement) {
5133
5361
  if (preloadedWebViewRef.current.loaded) {
5134
5362
  return presentPreloadedWebView();
5135
5363
  }
5364
+ pollCancelledRef.current = false;
5136
5365
  return new Promise((resolve) => {
5137
5366
  let elapsed = 0;
5138
5367
  const poll = () => {
5368
+ if (pollCancelledRef.current) return;
5139
5369
  if (preloadedWebViewRef.current?.loaded) {
5140
5370
  resolve(presentPreloadedWebView());
5141
5371
  return;
@@ -5145,9 +5375,9 @@ function PaywalloProvider({ children, config }) {
5145
5375
  resolve(presentCampaign(placement));
5146
5376
  return;
5147
5377
  }
5148
- setTimeout(poll, 100);
5378
+ pollTimerRef.current = setTimeout(poll, 100);
5149
5379
  };
5150
- setTimeout(poll, 100);
5380
+ pollTimerRef.current = setTimeout(poll, 100);
5151
5381
  });
5152
5382
  }
5153
5383
  return presentCampaign(placement);
@@ -5165,6 +5395,18 @@ function PaywalloProvider({ children, config }) {
5165
5395
  PaywalloClient.registerActiveChecker(hasActiveSubscription);
5166
5396
  PaywalloClient.registerRestoreHandler(restorePurchases);
5167
5397
  PaywalloClient.registerEmergencyPaywallHandler(onEmergency);
5398
+ return () => {
5399
+ pollCancelledRef.current = true;
5400
+ if (pollTimerRef.current !== null) {
5401
+ clearTimeout(pollTimerRef.current);
5402
+ pollTimerRef.current = null;
5403
+ }
5404
+ PaywalloClient.registerPaywallPresenter(null);
5405
+ PaywalloClient.registerSubscriptionGetter(null);
5406
+ PaywalloClient.registerActiveChecker(null);
5407
+ PaywalloClient.registerRestoreHandler(null);
5408
+ PaywalloClient.registerEmergencyPaywallHandler(null);
5409
+ };
5168
5410
  }, [isInitialized, bridgePaywallPresenter, getSubscription, hasActiveSubscription, restorePurchases]);
5169
5411
  const isReady = isInitialized && !isLoadingProducts;
5170
5412
  const noOp = useCallback13(() => {
@@ -5205,10 +5447,10 @@ function PaywalloProvider({ children, config }) {
5205
5447
  ]);
5206
5448
  useLayoutEffect(() => {
5207
5449
  if (showingPreloadedWebView) {
5208
- slideAnim.setValue(SCREEN_HEIGHT3);
5450
+ slideAnim.setValue(SCREEN_HEIGHT);
5209
5451
  Animated3.timing(slideAnim, { toValue: 0, duration: 200, easing: Easing2.out(Easing2.cubic), useNativeDriver: true }).start();
5210
5452
  }
5211
- }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT3]);
5453
+ }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT]);
5212
5454
  const preloadStyle = [
5213
5455
  styles3.preloadOverlay,
5214
5456
  showingPreloadedWebView ? styles3.visible : styles3.hidden,
@@ -5374,10 +5616,7 @@ function hasVariables(text) {
5374
5616
  var Paywallo = PaywalloClient;
5375
5617
  var index_default = Paywallo;
5376
5618
  export {
5377
- AFFILIATE_ERROR_CODES,
5378
5619
  ANALYTICS_ERROR_CODES,
5379
- AffiliateError,
5380
- AffiliateManager,
5381
5620
  AnalyticsError,
5382
5621
  ApiClient,
5383
5622
  CAMPAIGN_ERROR_CODES,
@@ -5395,7 +5634,6 @@ export {
5395
5634
  PaywallError,
5396
5635
  PaywallModal,
5397
5636
  Paywallo,
5398
- PaywalloAffiliate,
5399
5637
  PaywalloClient,
5400
5638
  PaywalloContext,
5401
5639
  PaywalloError,
@@ -5405,7 +5643,6 @@ export {
5405
5643
  SessionError,
5406
5644
  SessionManager,
5407
5645
  SubscriptionCache,
5408
- affiliateManager,
5409
5646
  createPurchaseError,
5410
5647
  index_default as default,
5411
5648
  detectDeviceLanguage,