@virex-tech/paywallo-sdk 1.1.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,3 +1,32 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/utils/uuid.ts
12
+ var uuid_exports = {};
13
+ __export(uuid_exports, {
14
+ generateUUID: () => generateUUID
15
+ });
16
+ function generateUUID() {
17
+ const template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
18
+ return template.replace(/[xy]/g, (c) => {
19
+ const r = Math.random() * 16 | 0;
20
+ const v = c === "x" ? r : r & 3 | 8;
21
+ return v.toString(16);
22
+ });
23
+ }
24
+ var init_uuid = __esm({
25
+ "src/utils/uuid.ts"() {
26
+ "use strict";
27
+ }
28
+ });
29
+
1
30
  // src/errors/PaywalloError.ts
2
31
  var PaywalloError = class extends Error {
3
32
  constructor(domain, code, message) {
@@ -102,6 +131,9 @@ function createPurchaseError(code, message) {
102
131
  // src/domains/iap/NativeStoreKit.ts
103
132
  import { NativeModules, Platform } from "react-native";
104
133
 
134
+ // src/domains/identity/IdentityManager.ts
135
+ import DeviceInfo from "react-native-device-info";
136
+
105
137
  // src/domains/identity/IdentityError.ts
106
138
  var IdentityError = class extends PaywalloError {
107
139
  constructor(code, message) {
@@ -117,37 +149,17 @@ var IDENTITY_ERROR_CODES = {
117
149
  IDENTIFY_FAILED: "IDENTITY_IDENTIFY_FAILED"
118
150
  };
119
151
 
120
- // src/utils/uuid.ts
121
- function generateUUID() {
122
- const template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
123
- return template.replace(/[xy]/g, (c) => {
124
- const r = Math.random() * 16 | 0;
125
- const v = c === "x" ? r : r & 3 | 8;
126
- return v.toString(16);
127
- });
128
- }
152
+ // src/domains/identity/IdentityManager.ts
153
+ init_uuid();
129
154
 
130
155
  // src/core/storage/SecureStorage.ts
131
156
  import AsyncStorage from "@react-native-async-storage/async-storage";
132
- var _keychain = void 0;
133
- async function loadKeychain() {
134
- if (_keychain !== void 0) return _keychain;
135
- try {
136
- const mod = await import("react-native-keychain");
137
- _keychain = mod.default;
138
- } catch {
139
- _keychain = null;
140
- }
141
- return _keychain;
142
- }
157
+ import Keychain from "react-native-keychain";
143
158
  var KEYCHAIN_SERVICE_PREFIX = "com.paywallo.sdk.";
144
159
  var SecureStorage = class {
145
160
  // eslint-disable-next-line @typescript-eslint/no-useless-constructor
146
161
  constructor(_debug = false) {
147
162
  }
148
- /**
149
- * Get a value, trying Keychain first (survives reinstalls on iOS), then AsyncStorage.
150
- */
151
163
  async get(key) {
152
164
  const keychainValue = await this.getFromKeychain(key);
153
165
  if (keychainValue) {
@@ -163,9 +175,6 @@ var SecureStorage = class {
163
175
  return null;
164
176
  }
165
177
  }
166
- /**
167
- * Set a value in both Keychain (if available) and AsyncStorage.
168
- */
169
178
  async set(key, value) {
170
179
  try {
171
180
  await Promise.all([
@@ -189,13 +198,11 @@ var SecureStorage = class {
189
198
  }
190
199
  }
191
200
  hasKeychainSupport() {
192
- return _keychain !== null && _keychain !== void 0;
201
+ return true;
193
202
  }
194
203
  async getFromKeychain(key) {
195
- const keychain = await loadKeychain();
196
- if (!keychain) return null;
197
204
  try {
198
- const result = await keychain.getGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
205
+ const result = await Keychain.getGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
199
206
  if (result && result.password) {
200
207
  return result.password;
201
208
  }
@@ -205,20 +212,16 @@ var SecureStorage = class {
205
212
  }
206
213
  }
207
214
  async setToKeychain(key, value) {
208
- const keychain = await loadKeychain();
209
- if (!keychain) return false;
210
215
  try {
211
- await keychain.setGenericPassword("panel", value, { service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
216
+ await Keychain.setGenericPassword("panel", value, { service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
212
217
  return true;
213
218
  } catch {
214
219
  return false;
215
220
  }
216
221
  }
217
222
  async removeFromKeychain(key) {
218
- const keychain = await loadKeychain();
219
- if (!keychain) return false;
220
223
  try {
221
- await keychain.resetGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
224
+ await Keychain.resetGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
222
225
  return true;
223
226
  } catch {
224
227
  return false;
@@ -229,11 +232,13 @@ var secureStorage = new SecureStorage();
229
232
 
230
233
  // src/domains/identity/IdentityManager.ts
231
234
  var DEVICE_ID_KEY = "device_id";
235
+ var ANON_ID_KEY = "anon_id";
232
236
  var USER_EMAIL_KEY = "user_email";
233
237
  var USER_PROPERTIES_KEY = "user_properties";
234
238
  var IdentityManager = class {
235
239
  constructor() {
236
240
  this.deviceId = null;
241
+ this.anonId = null;
237
242
  this.email = null;
238
243
  this.properties = {};
239
244
  this.apiClient = null;
@@ -250,33 +255,22 @@ var IdentityManager = class {
250
255
  this.secureStorage = new SecureStorage(debug);
251
256
  await this.loadPersistedState();
252
257
  if (!this.deviceId) {
253
- let DeviceInfo = null;
254
- try {
255
- const mod = await import("react-native-device-info");
256
- DeviceInfo = mod.default;
257
- } catch {
258
- DeviceInfo = null;
259
- }
260
258
  try {
261
- if (DeviceInfo) {
262
- this.deviceId = await DeviceInfo.getUniqueId();
263
- this.log("Got device ID from DeviceInfo:", this.deviceId);
264
- } else {
265
- this.deviceId = generateUUID();
266
- this.log("DeviceInfo not installed, using generated UUID:", this.deviceId);
267
- }
259
+ this.deviceId = await DeviceInfo.getUniqueId();
268
260
  } catch {
269
261
  this.deviceId = generateUUID();
270
- this.log("DeviceInfo unavailable, using generated UUID:", this.deviceId);
271
262
  }
272
263
  const stored = await this.secureStorage.set(DEVICE_ID_KEY, this.deviceId);
273
264
  if (!stored) {
274
- this.log("Failed to persist device ID to secure storage, using fallback");
275
265
  await this.secureStorage.set("device_id_fallback", this.deviceId).catch(() => {
276
- this.log("Fallback also failed \u2014 device ID only in memory");
277
266
  });
278
267
  }
279
268
  }
269
+ if (!this.anonId) {
270
+ this.anonId = "$paywallo_anon:" + generateUUID();
271
+ await this.secureStorage.set(ANON_ID_KEY, this.anonId).catch(() => {
272
+ });
273
+ }
280
274
  this.initialized = true;
281
275
  return this.deviceId ?? "";
282
276
  }
@@ -300,21 +294,18 @@ var IdentityManager = class {
300
294
  this.properties = { ...this.properties, ...properties };
301
295
  await this.secureStorage.set(USER_PROPERTIES_KEY, JSON.stringify(this.properties));
302
296
  }
303
- if (this.apiClient && this.deviceId) {
297
+ if (this.apiClient && this.anonId) {
304
298
  try {
305
- await this.apiClient.identify(this.deviceId, properties, email);
306
- this.log("Identity updated on server");
307
- } catch (error) {
308
- this.log("Failed to update identity on server:", error);
299
+ await this.apiClient.identify(this.anonId, properties, email, this.deviceId ?? void 0);
300
+ } catch {
309
301
  }
310
302
  }
311
303
  }
312
304
  getDistinctId() {
313
- if (!this.initialized || !this.deviceId) {
314
- if (this.debug) console.warn("[Paywallo Identity] getDistinctId called before initialization");
305
+ if (!this.initialized || !this.anonId) {
315
306
  return "";
316
307
  }
317
- return this.deviceId;
308
+ return this.anonId;
318
309
  }
319
310
  getDeviceId() {
320
311
  return this.deviceId;
@@ -329,24 +320,22 @@ var IdentityManager = class {
329
320
  this.ensureInitialized();
330
321
  this.properties = { ...this.properties, ...properties };
331
322
  await this.secureStorage.set(USER_PROPERTIES_KEY, JSON.stringify(this.properties));
332
- if (this.apiClient && this.deviceId) {
323
+ if (this.apiClient && this.anonId) {
333
324
  try {
334
- await this.apiClient.identify(this.deviceId, this.properties, this.email ?? void 0);
335
- } catch (error) {
336
- this.log("Failed to update properties on server:", error);
325
+ await this.apiClient.identify(this.anonId, this.properties, this.email ?? void 0, this.deviceId ?? void 0);
326
+ } catch {
337
327
  }
338
328
  }
339
329
  }
340
330
  async reset() {
331
+ const newAnonId = "$paywallo_anon:" + generateUUID();
332
+ this.anonId = newAnonId;
341
333
  this.email = null;
342
334
  this.properties = {};
343
- this.initialized = false;
344
- this.apiClient = null;
345
- this.deviceId = null;
346
335
  await Promise.all([
336
+ this.secureStorage?.set(ANON_ID_KEY, newAnonId) ?? Promise.resolve(false),
347
337
  this.secureStorage?.remove(USER_EMAIL_KEY) ?? Promise.resolve(false),
348
- this.secureStorage?.remove(USER_PROPERTIES_KEY) ?? Promise.resolve(false),
349
- this.secureStorage?.remove("device_id_fallback") ?? Promise.resolve(false)
338
+ this.secureStorage?.remove(USER_PROPERTIES_KEY) ?? Promise.resolve(false)
350
339
  ]);
351
340
  }
352
341
  getState() {
@@ -363,21 +352,23 @@ var IdentityManager = class {
363
352
  if (!this.secureStorage) {
364
353
  return;
365
354
  }
366
- const [storedDeviceId, storedEmail, storedPropertiesJson] = await Promise.all([
355
+ const [storedDeviceId, storedAnonId, storedEmail, storedPropertiesJson] = await Promise.all([
367
356
  this.secureStorage.get(DEVICE_ID_KEY),
357
+ this.secureStorage.get(ANON_ID_KEY),
368
358
  this.secureStorage.get(USER_EMAIL_KEY),
369
359
  this.secureStorage.get(USER_PROPERTIES_KEY)
370
360
  ]);
371
361
  if (storedDeviceId) {
372
362
  this.deviceId = storedDeviceId;
373
- this.log("Loaded device ID from storage:", this.deviceId);
374
363
  } else {
375
364
  const fallbackDeviceId = await this.secureStorage.get("device_id_fallback");
376
365
  if (fallbackDeviceId) {
377
366
  this.deviceId = fallbackDeviceId;
378
- this.log("Loaded device ID from fallback storage:", this.deviceId);
379
367
  }
380
368
  }
369
+ if (storedAnonId) {
370
+ this.anonId = storedAnonId;
371
+ }
381
372
  if (storedEmail) {
382
373
  this.email = storedEmail;
383
374
  }
@@ -397,11 +388,6 @@ var IdentityManager = class {
397
388
  );
398
389
  }
399
390
  }
400
- log(...args) {
401
- if (this.debug) {
402
- console.warn("[Paywallo Identity]", ...args);
403
- }
404
- }
405
391
  };
406
392
  var identityManager = new IdentityManager();
407
393
 
@@ -486,7 +472,6 @@ var nativeStoreKit = {
486
472
  isAvailable,
487
473
  async getProducts(productIds) {
488
474
  if (!PaywalloStoreKitNative) {
489
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] StoreKit native module not available");
490
475
  return [];
491
476
  }
492
477
  const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
@@ -500,20 +485,41 @@ var nativeStoreKit = {
500
485
  );
501
486
  }
502
487
  const distinctId = identityManager.getDistinctId();
503
- const result = await PaywalloStoreKitNative.purchase(productId, distinctId ?? null);
504
- if (!result || result.pending) {
505
- return null;
488
+ 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);
491
+ }
492
+ try {
493
+ const result = await PaywalloStoreKitNative.purchase(productId, distinctId ?? null);
494
+ if (!result || result.pending) {
495
+ return null;
496
+ }
497
+ return mapNativePurchase(result);
498
+ } catch (err) {
499
+ const message = err instanceof Error ? err.message : String(err);
500
+ if (message.toLowerCase().includes("cancel")) {
501
+ throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, message, true);
502
+ }
503
+ throw new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, message);
506
504
  }
507
- return mapNativePurchase(result);
508
505
  },
509
506
  async finishTransaction(transactionId) {
510
507
  if (!PaywalloStoreKitNative) return;
511
- await PaywalloStoreKitNative.finishTransaction(transactionId);
508
+ try {
509
+ await PaywalloStoreKitNative.finishTransaction(transactionId);
510
+ } catch (err) {
511
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction native error", err);
512
+ }
512
513
  },
513
514
  async getActiveTransactions() {
514
515
  if (!PaywalloStoreKitNative) return [];
515
- const results = await PaywalloStoreKitNative.getActiveTransactions();
516
- return results.map(mapNativePurchase);
516
+ try {
517
+ const results = await PaywalloStoreKitNative.getActiveTransactions();
518
+ return results.map(mapNativePurchase);
519
+ } catch (err) {
520
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] getActiveTransactions failed", err);
521
+ return [];
522
+ }
517
523
  }
518
524
  };
519
525
 
@@ -556,8 +562,8 @@ var IAPService = class {
556
562
  this.productsCache.set(product.productId, product);
557
563
  }
558
564
  return products;
559
- } catch (error) {
560
- if (this.debug) console.warn("[Paywallo][Products] StoreKit error:", error);
565
+ } catch (err) {
566
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] loadProducts failed", err);
561
567
  return [];
562
568
  }
563
569
  }
@@ -567,35 +573,47 @@ var IAPService = class {
567
573
  getCachedProducts() {
568
574
  return new Map(this.productsCache);
569
575
  }
576
+ async validateWithRetry(attempt, maxRetries = 2, delayMs = 3e3) {
577
+ let lastError;
578
+ for (let i = 0; i <= maxRetries; i++) {
579
+ try {
580
+ return await attempt();
581
+ } catch (error) {
582
+ lastError = error;
583
+ if (i < maxRetries) await new Promise((resolve) => setTimeout(resolve, delayMs));
584
+ }
585
+ }
586
+ throw lastError;
587
+ }
570
588
  async validateWithServer(purchase, productId, options) {
571
589
  const apiClient = this.getApiClient();
572
590
  const product = this.productsCache.get(productId);
573
591
  const platform = Platform2.OS;
574
592
  const distinctId = PaywalloClient.getDistinctId();
575
- const validation = await apiClient.validatePurchase(
576
- platform,
577
- purchase.receipt,
578
- purchase.productId,
579
- purchase.transactionId,
580
- product?.priceValue ?? 0,
581
- product?.currency ?? "USD",
582
- this.getDeviceCountry(),
583
- distinctId,
584
- options?.paywallPlacement,
585
- options?.variantKey
593
+ return this.validateWithRetry(
594
+ () => apiClient.validatePurchase(
595
+ platform,
596
+ purchase.receipt,
597
+ purchase.productId,
598
+ purchase.transactionId,
599
+ product?.priceValue ?? 0,
600
+ product?.currency ?? "USD",
601
+ this.getDeviceCountry(),
602
+ distinctId,
603
+ options?.paywallPlacement,
604
+ options?.variantKey
605
+ )
586
606
  );
587
- return validation;
588
607
  }
589
608
  async finishTransaction(transactionId) {
590
609
  try {
591
610
  await nativeStoreKit.finishTransaction(transactionId);
592
- } catch (finishError) {
593
- if (this.debug) console.warn("[Paywallo][Purchase] Failed to finish transaction:", finishError instanceof Error ? finishError.message : finishError);
611
+ } catch (err) {
612
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction failed", err);
594
613
  }
595
614
  }
596
615
  async purchase(productId, options) {
597
616
  if (!nativeStoreKit.isAvailable()) {
598
- if (this.debug) console.warn("[Paywallo][Purchase] StoreKit not available");
599
617
  return {
600
618
  success: false,
601
619
  error: new PurchaseError(PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE, "In-app purchases are not available on this device")
@@ -606,27 +624,13 @@ var IAPService = class {
606
624
  if (!purchase) {
607
625
  return { success: false };
608
626
  }
609
- let validation = null;
610
- try {
611
- validation = await this.validateWithServer(purchase, productId, options);
612
- } catch (validationError) {
613
- if (this.debug) console.warn("[Paywallo][Purchase] Server validation FAILED:", validationError instanceof Error ? validationError.message : validationError);
614
- return {
615
- success: false,
616
- error: new PurchaseError(PURCHASE_ERROR_CODES.VALIDATION_FAILED, validationError instanceof Error ? validationError.message : "Server validation failed")
617
- };
618
- }
619
- if (!validation?.success) {
620
- return {
621
- success: false,
622
- error: new PurchaseError(PURCHASE_ERROR_CODES.VALIDATION_FAILED, validation?.error ? `Server validation: ${validation.error}` : "Server validation failed")
623
- };
624
- }
625
627
  await this.finishTransaction(purchase.transactionId);
628
+ this.validateWithServer(purchase, productId, options).catch((err) => {
629
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] background validation failed", err);
630
+ });
626
631
  return { success: true, purchase };
627
632
  } catch (error) {
628
633
  const e = error instanceof PurchaseError ? error : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, error instanceof Error ? error.message : String(error));
629
- if (this.debug) console.warn("[Paywallo][Purchase] FAILED:", e.message);
630
634
  return { success: false, error: e };
631
635
  }
632
636
  }
@@ -661,15 +665,15 @@ var IAPService = class {
661
665
  if (r.status === "fulfilled" && r.value.success) {
662
666
  try {
663
667
  await nativeStoreKit.finishTransaction(tx.transactionId);
664
- } catch (finishError) {
665
- if (this.debug) console.warn("[Paywallo][Restore] Failed to finish tx:", tx.transactionId, finishError instanceof Error ? finishError.message : finishError);
668
+ } catch (err) {
669
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] finishTransaction failed", err);
666
670
  }
667
671
  validated.push(tx);
668
672
  }
669
673
  }
670
674
  return validated;
671
- } catch (error) {
672
- if (this.debug) console.warn("[Paywallo][Restore] \u274C FAILED:", error instanceof Error ? error.message : error);
675
+ } catch (err) {
676
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] restore failed", err);
673
677
  return [];
674
678
  }
675
679
  }
@@ -699,7 +703,8 @@ function usePaywallTracking() {
699
703
  ...opts.campaignId && { campaignId: opts.campaignId },
700
704
  ...sessionId && { sessionId }
701
705
  });
702
- } catch {
706
+ } catch (err) {
707
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackDismissed failed", err);
703
708
  }
704
709
  }, []);
705
710
  const trackProductSelected = useCallback((opts) => {
@@ -713,7 +718,8 @@ function usePaywallTracking() {
713
718
  ...opts.variantKey && { variantKey: opts.variantKey },
714
719
  ...opts.campaignId && { campaignId: opts.campaignId },
715
720
  ...sessionId && { sessionId }
716
- }).catch(() => {
721
+ }).catch((err) => {
722
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackProductSelected failed", err);
717
723
  });
718
724
  }, []);
719
725
  const trackPurchased = useCallback(async (opts) => {
@@ -729,7 +735,8 @@ function usePaywallTracking() {
729
735
  ...opts.campaignId && { campaignId: opts.campaignId },
730
736
  ...sessionId && { sessionId }
731
737
  });
732
- } catch {
738
+ } catch (err) {
739
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] trackPurchased failed", err);
733
740
  }
734
741
  }, []);
735
742
  return { trackDismissed, trackProductSelected, trackPurchased };
@@ -737,7 +744,7 @@ function usePaywallTracking() {
737
744
 
738
745
  // src/domains/paywall/usePaywallActions.ts
739
746
  var SCREEN_HEIGHT = Dimensions.get("window").height;
740
- function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId) {
747
+ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId, openAnim) {
741
748
  const [isPurchasing, setIsPurchasing] = useState(false);
742
749
  const [isClosing, setIsClosing] = useState(false);
743
750
  const slideAnim = useRef(new Animated.Value(0)).current;
@@ -746,7 +753,8 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
746
753
  const handleClose = useCallback2(() => {
747
754
  if (isClosing) return;
748
755
  setIsClosing(true);
749
- Animated.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 300, useNativeDriver: true }).start(() => {
756
+ const animTarget = openAnim ?? slideAnim;
757
+ Animated.timing(animTarget, { toValue: SCREEN_HEIGHT, duration: 350, useNativeDriver: true }).start(() => {
750
758
  if (paywallConfig) {
751
759
  const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
752
760
  void trackDismissed({ placement, paywallId: paywallConfig.id, durationSeconds, action: "close", variantKey, campaignId });
@@ -755,7 +763,7 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
755
763
  setIsClosing(false);
756
764
  onResult({ presented: true, purchased: false, cancelled: true, restored: false });
757
765
  });
758
- }, [isClosing, onResult, paywallConfig, placement, slideAnim, trackDismissed, variantKey, campaignId]);
766
+ }, [isClosing, openAnim, onResult, paywallConfig, placement, slideAnim, trackDismissed, variantKey, campaignId]);
759
767
  const handlePurchase = useCallback2(async (productId) => {
760
768
  if (isPurchasing) return;
761
769
  setIsPurchasing(true);
@@ -773,7 +781,6 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
773
781
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
774
782
  }
775
783
  } catch (error) {
776
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Purchase error:", error);
777
784
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
778
785
  } finally {
779
786
  setIsPurchasing(false);
@@ -794,7 +801,6 @@ function usePaywallActions(placement, paywallConfig, onResult, variantKey, campa
794
801
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
795
802
  }
796
803
  } catch (error) {
797
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Restore error:", error);
798
804
  onResult({ presented: true, purchased: false, cancelled: false, restored: false });
799
805
  } finally {
800
806
  setIsPurchasing(false);
@@ -870,7 +876,8 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
870
876
  const map = /* @__PURE__ */ new Map();
871
877
  for (const p of storeProducts) map.set(p.productId, p);
872
878
  setProducts(map);
873
- } catch {
879
+ } catch (err) {
880
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] loadProducts in paywall failed", err);
874
881
  setProducts(/* @__PURE__ */ new Map());
875
882
  }
876
883
  } else {
@@ -889,7 +896,6 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
889
896
  }
890
897
  } catch (err) {
891
898
  const e = err instanceof Error ? err : new PaywallError(PAYWALL_ERROR_CODES.LOAD_FAILED, String(err));
892
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Error loading paywall:", e.message);
893
899
  setError(e);
894
900
  }
895
901
  };
@@ -898,18 +904,103 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
898
904
  return { paywallConfig, products, error };
899
905
  }
900
906
 
907
+ // src/utils/localization.ts
908
+ import { NativeModules as NativeModules2, Platform as Platform3 } from "react-native";
909
+ var currentLanguage = "pt-BR";
910
+ var defaultLanguage = "pt-BR";
911
+ function setCurrentLanguage(language) {
912
+ currentLanguage = language;
913
+ }
914
+ function setDefaultLanguage(language) {
915
+ defaultLanguage = language;
916
+ }
917
+ function getCurrentLanguage() {
918
+ return currentLanguage;
919
+ }
920
+ function getDefaultLanguage() {
921
+ return defaultLanguage;
922
+ }
923
+ function getLocalizedString(text) {
924
+ if (!text) return "";
925
+ if (typeof text === "string") return text;
926
+ return text[currentLanguage] ?? text[defaultLanguage] ?? Object.values(text)[0] ?? "";
927
+ }
928
+ function detectDeviceLanguage() {
929
+ try {
930
+ if (Platform3.OS === "ios") {
931
+ const settings = NativeModules2.SettingsManager?.settings;
932
+ return settings?.AppleLocale ?? settings?.AppleLanguages?.[0] ?? "en-US";
933
+ }
934
+ return NativeModules2.I18nManager?.localeIdentifier ?? "en-US";
935
+ } catch {
936
+ return "en-US";
937
+ }
938
+ }
939
+ var SDK_STRINGS = {
940
+ paywallErrorTitle: {
941
+ "pt-BR": "Algo deu errado",
942
+ "pt": "Algo deu errado",
943
+ "en": "Something went wrong",
944
+ "en-US": "Something went wrong",
945
+ "en-GB": "Something went wrong",
946
+ "es": "Algo sali\xF3 mal",
947
+ "es-419": "Algo sali\xF3 mal"
948
+ },
949
+ paywallRetryButton: {
950
+ "pt-BR": "Tentar novamente",
951
+ "pt": "Tentar novamente",
952
+ "en": "Try again",
953
+ "en-US": "Try again",
954
+ "en-GB": "Try again",
955
+ "es": "Reintentar",
956
+ "es-419": "Reintentar"
957
+ },
958
+ paywallCloseButton: {
959
+ "pt-BR": "Fechar",
960
+ "pt": "Fechar",
961
+ "en": "Close",
962
+ "en-US": "Close",
963
+ "en-GB": "Close",
964
+ "es": "Cerrar",
965
+ "es-419": "Cerrar"
966
+ }
967
+ };
968
+ function getSdkString(key) {
969
+ const map = SDK_STRINGS[key];
970
+ const lang = currentLanguage;
971
+ if (lang in map) return map[lang];
972
+ const base = lang.split("-")[0];
973
+ if (base && base in map) return map[base];
974
+ if (defaultLanguage in map) return map[defaultLanguage];
975
+ return Object.values(map)[0];
976
+ }
977
+ function initLocalization(options) {
978
+ if (options?.defaultLanguage) {
979
+ defaultLanguage = options.defaultLanguage;
980
+ }
981
+ if (options?.detectDevice !== false) {
982
+ const deviceLanguage = detectDeviceLanguage();
983
+ currentLanguage = deviceLanguage;
984
+ }
985
+ }
986
+
901
987
  // src/domains/paywall/PaywallWebView.tsx
902
988
  import { useCallback as useCallback3, useEffect as useEffect2, useMemo, useRef as useRef3, useState as useState3 } from "react";
903
989
  import {
904
990
  Linking,
905
991
  NativeEventEmitter,
906
- NativeModules as NativeModules2,
992
+ NativeModules as NativeModules3,
907
993
  requireNativeComponent,
908
994
  StyleSheet,
909
995
  View
910
996
  } from "react-native";
911
997
 
912
998
  // src/domains/paywall/parseWebViewMessage.ts
999
+ function deriveMessageId(message) {
1000
+ if (message.id) return message.id;
1001
+ const payloadKey = message.payload?.productId ?? message.payload?.url ?? (message.payload?.timestamp != null ? String(message.payload.timestamp) : "");
1002
+ return `${message.type}:${payloadKey}`;
1003
+ }
913
1004
  function parseWebViewMessage(raw) {
914
1005
  try {
915
1006
  const parsed = JSON.parse(raw);
@@ -933,7 +1024,7 @@ function buildPaywallInjectionScript(data) {
933
1024
  secondaryProductId: data.secondaryProductId
934
1025
  }
935
1026
  };
936
- return `(function(){var d=${JSON.stringify(paywallData)},a=0,m=50;function t(){a++;if(window.receiveNativeMessage){window.receiveNativeMessage(d);return;}if(a<m){setTimeout(t,100);}else{console.warn('[Paywallo WV] FAILED to inject after '+m+' attempts');}}t();})();true;`;
1027
+ return `(function(){var d=${JSON.stringify(paywallData)},a=0,m=50;function t(){a++;if(window.receiveNativeMessage){window.receiveNativeMessage(d);return;}if(a<m){setTimeout(t,100);}}t();})();true;`;
937
1028
  }
938
1029
  function buildPurchaseStateScript(isPurchasing) {
939
1030
  return `if(window.receiveNativeMessage){window.receiveNativeMessage({type:"purchaseState",data:{isPurchasing:${isPurchasing}}});}true;`;
@@ -951,7 +1042,7 @@ function getNativeWebView() {
951
1042
  var webViewEmitter = null;
952
1043
  function getWebViewEmitter() {
953
1044
  if (!webViewEmitter) {
954
- webViewEmitter = new NativeEventEmitter(NativeModules2.PaywalloWebViewModule);
1045
+ webViewEmitter = new NativeEventEmitter(NativeModules3.PaywalloWebViewModule);
955
1046
  }
956
1047
  return webViewEmitter;
957
1048
  }
@@ -969,6 +1060,7 @@ function PaywallWebView({
969
1060
  primaryProductId,
970
1061
  secondaryProductId,
971
1062
  isPurchasing,
1063
+ webUrl: webUrlProp,
972
1064
  onPurchase,
973
1065
  onClose,
974
1066
  onRestore,
@@ -980,8 +1072,39 @@ function PaywallWebView({
980
1072
  const [scriptToInject, setScriptToInject] = useState3(void 0);
981
1073
  const onErrorRef = useRef3(onError);
982
1074
  onErrorRef.current = onError;
1075
+ const onReadyRef = useRef3(onReady);
1076
+ onReadyRef.current = onReady;
983
1077
  const NativeWebView = useMemo(() => getNativeWebView(), []);
984
- const webUrl = useMemo(() => PaywalloClient.getWebUrl() ?? "http://localhost:3000", []);
1078
+ const [resolvedWebUrl, setResolvedWebUrl] = useState3(
1079
+ () => webUrlProp ?? PaywalloClient.getWebUrl()
1080
+ );
1081
+ useEffect2(() => {
1082
+ if (webUrlProp !== void 0) {
1083
+ setResolvedWebUrl(webUrlProp ?? null);
1084
+ return;
1085
+ }
1086
+ const url = PaywalloClient.getWebUrl();
1087
+ if (url) {
1088
+ setResolvedWebUrl(url);
1089
+ return;
1090
+ }
1091
+ const interval = setInterval(() => {
1092
+ const u = PaywalloClient.getWebUrl();
1093
+ if (u) {
1094
+ setResolvedWebUrl(u);
1095
+ clearInterval(interval);
1096
+ }
1097
+ }, 200);
1098
+ return () => clearInterval(interval);
1099
+ }, [webUrlProp]);
1100
+ useEffect2(() => {
1101
+ if (!resolvedWebUrl) {
1102
+ onErrorRef.current?.({
1103
+ code: -1,
1104
+ description: "Paywall URL not configured. SDK may not be initialized."
1105
+ });
1106
+ }
1107
+ }, []);
985
1108
  const buildAndSetInjectionScript = useCallback3(() => {
986
1109
  if (!craftData || paywallDataSent.current) return;
987
1110
  paywallDataSent.current = true;
@@ -992,19 +1115,21 @@ function PaywallWebView({
992
1115
  secondaryProductId
993
1116
  });
994
1117
  setScriptToInject(script);
995
- onReady?.();
996
- }, [craftData, products, primaryProductId, secondaryProductId, onReady]);
997
- const lastProcessedRef = useRef3({ type: "", time: 0 });
1118
+ }, [craftData, products, primaryProductId, secondaryProductId]);
1119
+ const processedIdsRef = useRef3(/* @__PURE__ */ new Set());
998
1120
  const handleParsedMessage = useCallback3(
999
1121
  (message) => {
1000
- const now = Date.now();
1001
- if (message.type === lastProcessedRef.current.type && now - lastProcessedRef.current.time < 500) {
1002
- return;
1003
- }
1004
- lastProcessedRef.current = { type: message.type, time: now };
1122
+ const msgId = deriveMessageId(message);
1123
+ if (processedIdsRef.current.has(msgId)) return;
1124
+ processedIdsRef.current.add(msgId);
1125
+ setTimeout(() => processedIdsRef.current.delete(msgId), 5e3);
1005
1126
  switch (message.type) {
1006
1127
  case "ready":
1007
- if (!paywallDataSent.current) buildAndSetInjectionScript();
1128
+ if (!paywallDataSent.current) {
1129
+ buildAndSetInjectionScript();
1130
+ } else {
1131
+ onReadyRef.current?.();
1132
+ }
1008
1133
  break;
1009
1134
  case "purchase":
1010
1135
  if (message.payload?.productId) onPurchase(message.payload.productId);
@@ -1095,7 +1220,8 @@ function PaywallWebView({
1095
1220
  if (!paywallDataSent.current) return;
1096
1221
  setScriptToInject(buildPurchaseStateScript(isPurchasing));
1097
1222
  }, [isPurchasing]);
1098
- const paywallUrl = `${webUrl}/paywall/${paywallId}`;
1223
+ if (!resolvedWebUrl) return null;
1224
+ const paywallUrl = `${resolvedWebUrl}/paywall/${paywallId}`;
1099
1225
  return /* @__PURE__ */ jsx(View, { style: styles.container, children: /* @__PURE__ */ jsx(
1100
1226
  NativeWebView,
1101
1227
  {
@@ -1116,6 +1242,18 @@ var styles = StyleSheet.create({
1116
1242
  // src/domains/paywall/PaywallModal.tsx
1117
1243
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1118
1244
  var SCREEN_HEIGHT2 = Dimensions2.get("window").height;
1245
+ function ErrorFallback({ description, onRetry, onClose }) {
1246
+ const overrides = PaywalloClient.getConfig()?.errorStrings;
1247
+ const title = overrides?.title ?? getSdkString("paywallErrorTitle");
1248
+ const retryLabel = overrides?.retry ?? getSdkString("paywallRetryButton");
1249
+ const closeLabel = overrides?.close ?? getSdkString("paywallCloseButton");
1250
+ return /* @__PURE__ */ jsx2(View2, { style: styles2.fallbackContainer, children: /* @__PURE__ */ jsxs(View2, { style: styles2.fallbackCard, children: [
1251
+ /* @__PURE__ */ jsx2(Text, { style: styles2.fallbackTitle, children: title }),
1252
+ /* @__PURE__ */ jsx2(Text, { style: styles2.fallbackDescription, children: description }),
1253
+ /* @__PURE__ */ jsx2(Pressable, { style: styles2.retryButton, onPress: onRetry, children: /* @__PURE__ */ jsx2(Text, { style: styles2.retryButtonText, children: retryLabel }) }),
1254
+ /* @__PURE__ */ jsx2(Pressable, { style: styles2.closeButton, onPress: onClose, children: /* @__PURE__ */ jsx2(Text, { style: styles2.closeButtonText, children: closeLabel }) })
1255
+ ] }) });
1256
+ }
1119
1257
  function PaywallModal({
1120
1258
  placement,
1121
1259
  visible,
@@ -1133,7 +1271,13 @@ function PaywallModal({
1133
1271
  variantKey,
1134
1272
  campaignId
1135
1273
  );
1136
- const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId);
1274
+ const openAnim = useRef4(new Animated2.Value(SCREEN_HEIGHT2)).current;
1275
+ const [modalVisible, setModalVisible] = useState4(false);
1276
+ const handleResult = useCallback4((result) => {
1277
+ setModalVisible(false);
1278
+ onResult(result);
1279
+ }, [onResult]);
1280
+ const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, handleResult, variantKey, campaignId, openAnim);
1137
1281
  const [webViewError, setWebViewError] = useState4(null);
1138
1282
  const [retryCount, setRetryCount] = useState4(0);
1139
1283
  const retryCountRef = useRef4(0);
@@ -1153,9 +1297,9 @@ function PaywallModal({
1153
1297
  setWebViewError(null);
1154
1298
  setRetryCount(0);
1155
1299
  }, []);
1156
- const openAnim = useRef4(new Animated2.Value(SCREEN_HEIGHT2)).current;
1157
1300
  useEffect3(() => {
1158
1301
  if (visible) {
1302
+ setModalVisible(true);
1159
1303
  openAnim.setValue(SCREEN_HEIGHT2);
1160
1304
  Animated2.timing(openAnim, {
1161
1305
  toValue: 0,
@@ -1165,11 +1309,11 @@ function PaywallModal({
1165
1309
  }).start();
1166
1310
  }
1167
1311
  }, [visible, openAnim]);
1168
- if (!visible) return null;
1312
+ if (!modalVisible && !visible) return null;
1169
1313
  return /* @__PURE__ */ jsx2(
1170
1314
  Modal,
1171
1315
  {
1172
- visible,
1316
+ visible: modalVisible,
1173
1317
  animationType: "none",
1174
1318
  presentationStyle: "overFullScreen",
1175
1319
  onRequestClose: handleClose,
@@ -1177,12 +1321,14 @@ function PaywallModal({
1177
1321
  statusBarTranslucent: true,
1178
1322
  children: /* @__PURE__ */ jsx2(View2, { style: styles2.overlay, children: /* @__PURE__ */ jsx2(Animated2.View, { style: [styles2.animatedContainer, { transform: [{ translateY: openAnim }] }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.container, children: [
1179
1323
  error && /* @__PURE__ */ jsx2(View2, { style: styles2.errorContainer, children: /* @__PURE__ */ jsx2(Text, { style: styles2.errorText, children: error.message }) }),
1180
- !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ jsx2(Fragment, { children: webViewError ? /* @__PURE__ */ jsx2(View2, { style: styles2.fallbackContainer, children: /* @__PURE__ */ jsxs(View2, { style: styles2.fallbackCard, children: [
1181
- /* @__PURE__ */ jsx2(Text, { style: styles2.fallbackTitle, children: "Algo deu errado" }),
1182
- /* @__PURE__ */ jsx2(Text, { style: styles2.fallbackDescription, children: webViewError.description }),
1183
- /* @__PURE__ */ jsx2(Pressable, { style: styles2.retryButton, onPress: handleRetry, children: /* @__PURE__ */ jsx2(Text, { style: styles2.retryButtonText, children: "Tentar novamente" }) }),
1184
- /* @__PURE__ */ jsx2(Pressable, { style: styles2.closeButton, onPress: handleClose, children: /* @__PURE__ */ jsx2(Text, { style: styles2.closeButtonText, children: "Fechar" }) })
1185
- ] }) }) : /* @__PURE__ */ jsx2(
1324
+ !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ jsx2(Fragment, { children: webViewError ? /* @__PURE__ */ jsx2(
1325
+ ErrorFallback,
1326
+ {
1327
+ description: webViewError.description,
1328
+ onRetry: handleRetry,
1329
+ onClose: handleClose
1330
+ }
1331
+ ) : /* @__PURE__ */ jsx2(
1186
1332
  PaywallWebView,
1187
1333
  {
1188
1334
  paywallId: paywallConfig.id,
@@ -1267,7 +1413,11 @@ var PaywallErrorBoundary = class extends Component {
1267
1413
  return { hasError: true };
1268
1414
  }
1269
1415
  componentDidCatch(error) {
1270
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Paywall render error:", error.message);
1416
+ if (PaywalloClient.getConfig()?.debug) console.error("[Paywallo] Paywall render error:", error);
1417
+ try {
1418
+ PaywalloClient.getApiClient()?.reportError("PAYWALL_RENDER_ERROR", error.message);
1419
+ } catch {
1420
+ }
1271
1421
  }
1272
1422
  render() {
1273
1423
  return this.state.hasError ? null : this.props.children;
@@ -1290,7 +1440,7 @@ function usePaywallContext() {
1290
1440
 
1291
1441
  // src/domains/paywall/usePaywallPresenter.ts
1292
1442
  import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef5, useState as useState5 } from "react";
1293
- function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView) {
1443
+ function usePaywallPresenter(preloadedWebView, setPreloadedWebView) {
1294
1444
  const [showingPreloadedWebView, setShowingPreloadedWebView] = useState5(false);
1295
1445
  const resolverRef = useRef5(null);
1296
1446
  const presentedAtRef = useRef5(Date.now());
@@ -1339,9 +1489,8 @@ function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreload
1339
1489
  };
1340
1490
  void track();
1341
1491
  setShowingPreloadedWebView(false);
1342
- clearPreloadedWebView();
1343
1492
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: true, restored: false });
1344
- }, [preloadedWebView, clearPreloadedWebView, resolvePreloadedPaywall]);
1493
+ }, [preloadedWebView, resolvePreloadedPaywall]);
1345
1494
  return { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef };
1346
1495
  }
1347
1496
 
@@ -1549,9 +1698,7 @@ var AffiliateManager = class {
1549
1698
  }
1550
1699
  }
1551
1700
  log(...args) {
1552
- if (this.debug) {
1553
- console.warn("[AffiliateManager]", ...args);
1554
- }
1701
+ if (this.debug) console.log("[Paywallo:Affiliate]", ...args);
1555
1702
  }
1556
1703
  };
1557
1704
  var affiliateManager = new AffiliateManager();
@@ -1569,26 +1716,42 @@ var PaywalloAffiliate = {
1569
1716
  };
1570
1717
 
1571
1718
  // src/domains/subscription/SubscriptionCache.ts
1572
- var CACHE_KEY = "subscription_cache";
1719
+ var LEGACY_CACHE_KEY = "subscription_cache";
1720
+ var CACHE_KEY_PREFIX = "subscription_cache:";
1573
1721
  var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
1722
+ function keyFor(distinctId) {
1723
+ return `${CACHE_KEY_PREFIX}${distinctId}`;
1724
+ }
1574
1725
  var SubscriptionCache = class {
1575
1726
  constructor(ttl = DEFAULT_TTL) {
1576
- this.memoryCache = null;
1727
+ this.memoryCache = /* @__PURE__ */ new Map();
1728
+ this.legacyCleanupDone = false;
1577
1729
  this.ttl = ttl;
1578
1730
  this.storage = new SecureStorage();
1579
1731
  }
1580
- async get() {
1581
- if (this.memoryCache && !this.isExpired(this.memoryCache)) {
1582
- return this.memoryCache;
1732
+ async cleanupLegacyCache() {
1733
+ if (this.legacyCleanupDone) return;
1734
+ this.legacyCleanupDone = true;
1735
+ try {
1736
+ await this.storage.remove(LEGACY_CACHE_KEY);
1737
+ } catch {
1738
+ }
1739
+ }
1740
+ async get(distinctId) {
1741
+ await this.cleanupLegacyCache();
1742
+ if (!distinctId) return null;
1743
+ const memEntry = this.memoryCache.get(distinctId);
1744
+ if (memEntry && !this.isExpired(memEntry)) {
1745
+ return memEntry;
1583
1746
  }
1584
1747
  try {
1585
- const stored = await this.storage.get(CACHE_KEY);
1748
+ const stored = await this.storage.get(keyFor(distinctId));
1586
1749
  if (!stored) {
1587
1750
  return null;
1588
1751
  }
1589
1752
  const parsed = JSON.parse(stored);
1590
1753
  if (typeof parsed !== "object" || parsed === null || !("data" in parsed) || typeof parsed.data !== "object" || parsed.data === null || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") {
1591
- await this.storage.remove(CACHE_KEY);
1754
+ await this.storage.remove(keyFor(distinctId));
1592
1755
  return null;
1593
1756
  }
1594
1757
  const cached = parsed;
@@ -1617,32 +1780,39 @@ var SubscriptionCache = class {
1617
1780
  }
1618
1781
  cached.isStale = this.isExpired(cached);
1619
1782
  if (!cached.isStale) {
1620
- this.memoryCache = cached;
1783
+ this.memoryCache.set(distinctId, cached);
1621
1784
  }
1622
1785
  return cached;
1623
1786
  } catch {
1624
1787
  return null;
1625
1788
  }
1626
1789
  }
1627
- async set(data) {
1790
+ async set(distinctId, data) {
1791
+ if (!distinctId) return;
1628
1792
  const cached = {
1629
1793
  data,
1630
1794
  cachedAt: Date.now(),
1631
1795
  isStale: false
1632
1796
  };
1633
- this.memoryCache = cached;
1797
+ this.memoryCache.set(distinctId, cached);
1634
1798
  try {
1635
- await this.storage.set(CACHE_KEY, JSON.stringify(cached));
1636
- } catch (error) {
1637
- if (__DEV__) console.warn("[SubscriptionCache] set() failed:", error instanceof Error ? error.message : String(error));
1799
+ await this.storage.set(keyFor(distinctId), JSON.stringify(cached));
1800
+ } catch {
1638
1801
  }
1639
1802
  }
1640
- async invalidate() {
1641
- this.memoryCache = null;
1803
+ async invalidate(distinctId) {
1804
+ if (!distinctId) return;
1805
+ this.memoryCache.delete(distinctId);
1642
1806
  try {
1643
- await this.storage.remove(CACHE_KEY);
1644
- } catch (error) {
1645
- if (__DEV__) console.warn("[SubscriptionCache] invalidate() failed:", error instanceof Error ? error.message : String(error));
1807
+ await this.storage.remove(keyFor(distinctId));
1808
+ } catch {
1809
+ }
1810
+ }
1811
+ async invalidateAll() {
1812
+ this.memoryCache.clear();
1813
+ try {
1814
+ await this.storage.remove(LEGACY_CACHE_KEY);
1815
+ } catch {
1646
1816
  }
1647
1817
  }
1648
1818
  isExpired(cached) {
@@ -1654,60 +1824,214 @@ var SubscriptionCache = class {
1654
1824
  };
1655
1825
  var subscriptionCache = new SubscriptionCache();
1656
1826
 
1657
- // src/core/ApiClient.ts
1658
- import { Platform as Platform3 } from "react-native";
1827
+ // src/domains/subscription/SubscriptionManager.ts
1828
+ import { Platform as Platform4 } from "react-native";
1659
1829
 
1660
- // src/core/apiUrlUtils.ts
1661
- var DEFAULT_WEB_URL = "https://paywallo.com.br";
1662
- function deriveWebUrl(baseUrl) {
1663
- try {
1664
- const url = new URL(baseUrl);
1665
- if (url.hostname.startsWith("192.168") || url.hostname === "localhost") {
1666
- url.port = "3000";
1667
- return url.toString().replace(/\/$/, "");
1830
+ // src/domains/session/SessionError.ts
1831
+ var SessionError = class extends PaywalloError {
1832
+ constructor(code, message) {
1833
+ super("session", code, message);
1834
+ this.name = "SessionError";
1835
+ }
1836
+ };
1837
+ var SESSION_ERROR_CODES = {
1838
+ NOT_INITIALIZED: "SESSION_NOT_INITIALIZED",
1839
+ START_FAILED: "SESSION_START_FAILED",
1840
+ END_FAILED: "SESSION_END_FAILED",
1841
+ RESTORE_FAILED: "SESSION_RESTORE_FAILED"
1842
+ };
1843
+
1844
+ // src/domains/subscription/SubscriptionManager.ts
1845
+ var SubscriptionManagerClass = class {
1846
+ constructor() {
1847
+ this.config = null;
1848
+ this.listeners = /* @__PURE__ */ new Set();
1849
+ this.userId = null;
1850
+ this.cache = new SubscriptionCache();
1851
+ }
1852
+ cacheKey() {
1853
+ return this.userId ?? "__anonymous__";
1854
+ }
1855
+ init(config) {
1856
+ this.config = config;
1857
+ if (config.cacheTTL) {
1858
+ this.cache.setTTL(config.cacheTTL);
1668
1859
  }
1669
- if (url.hostname.startsWith("api.")) {
1670
- url.hostname = url.hostname.replace("api.", "app.");
1671
- return url.toString().replace(/\/$/, "");
1860
+ }
1861
+ setUserId(userId) {
1862
+ if (this.userId !== userId) {
1863
+ this.userId = userId;
1864
+ void this.cache.invalidateAll();
1672
1865
  }
1673
- return DEFAULT_WEB_URL;
1674
- } catch {
1675
- return DEFAULT_WEB_URL;
1676
1866
  }
1677
- }
1678
- function buildConditionalFlagUrl(baseUrl, flagKey, context) {
1679
- const url = new URL(`${baseUrl}/api/v1/conditional-flags/${flagKey}`);
1680
- if (context.platform) url.searchParams.set("platform", context.platform);
1681
- if (context.appVersion) url.searchParams.set("appVersion", context.appVersion);
1682
- if (context.country) url.searchParams.set("country", context.country);
1683
- if (context.distinctId) url.searchParams.set("distinctId", context.distinctId);
1684
- return url.toString();
1685
- }
1686
-
1687
- // src/domains/iap/apiPurchaseMethods.ts
1688
- async function validatePurchase(client, appKey, platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
1689
- const response = await client.post(
1690
- `/purchases/validate/${appKey}`,
1691
- {
1692
- platform,
1693
- receipt,
1694
- productId,
1695
- transactionId,
1696
- priceLocal,
1697
- currency,
1698
- country,
1699
- distinctId,
1700
- ...paywallPlacement !== void 0 && { paywallPlacement },
1701
- ...variantKey !== void 0 && { variantKey }
1702
- },
1703
- false
1704
- );
1705
- return response.data;
1706
- }
1707
- async function getSubscriptionStatus(client, baseUrl, appKey, distinctId) {
1867
+ async hasActiveSubscription(forceRefresh = false) {
1868
+ const status = await this.getSubscriptionStatus(forceRefresh);
1869
+ return status.hasActiveSubscription;
1870
+ }
1871
+ async getSubscription(forceRefresh = false) {
1872
+ const status = await this.getSubscriptionStatus(forceRefresh);
1873
+ return status.subscription;
1874
+ }
1875
+ async getEntitlements(forceRefresh = false) {
1876
+ const status = await this.getSubscriptionStatus(forceRefresh);
1877
+ return status.entitlements;
1878
+ }
1879
+ async getSubscriptionStatus(forceRefresh = false) {
1880
+ if (!this.config) {
1881
+ return this.getEmptyStatus();
1882
+ }
1883
+ if (!forceRefresh) {
1884
+ const cached = await this.cache.get(this.cacheKey());
1885
+ if (cached && !cached.isStale) {
1886
+ return cached.data;
1887
+ }
1888
+ }
1889
+ try {
1890
+ const status = await this.fetchSubscriptionStatus();
1891
+ await this.cache.set(this.cacheKey(), status);
1892
+ this.notifyListeners(status);
1893
+ return status;
1894
+ } catch (error) {
1895
+ this.log("Failed to fetch subscription status", error);
1896
+ const cached = await this.cache.get(this.cacheKey());
1897
+ if (cached) {
1898
+ return cached.data;
1899
+ }
1900
+ return this.getEmptyStatus();
1901
+ }
1902
+ }
1903
+ async restorePurchases() {
1904
+ if (!this.config) {
1905
+ throw new SessionError(
1906
+ SESSION_ERROR_CODES.NOT_INITIALIZED,
1907
+ "SubscriptionManager not initialized"
1908
+ );
1909
+ }
1910
+ await this.cache.invalidate(this.cacheKey());
1911
+ return this.getSubscriptionStatus(true);
1912
+ }
1913
+ async fetchSubscriptionStatus() {
1914
+ if (!this.config) {
1915
+ return this.getEmptyStatus();
1916
+ }
1917
+ const url = new URL(`${this.config.serverUrl}/subscriptions/status`);
1918
+ if (this.userId) {
1919
+ url.searchParams.set("userId", this.userId);
1920
+ }
1921
+ url.searchParams.set("platform", Platform4.OS);
1922
+ const response = await fetch(url.toString(), {
1923
+ method: "GET",
1924
+ headers: {
1925
+ "Content-Type": "application/json",
1926
+ "X-App-Key": this.config.appKey
1927
+ }
1928
+ });
1929
+ if (!response.ok) {
1930
+ throw new SessionError(
1931
+ SESSION_ERROR_CODES.RESTORE_FAILED,
1932
+ `Failed to fetch subscription status: ${response.status}`
1933
+ );
1934
+ }
1935
+ const data = await response.json();
1936
+ if (data.subscription) {
1937
+ data.subscription.expiresAt = new Date(data.subscription.expiresAt);
1938
+ data.subscription.originalPurchaseDate = new Date(data.subscription.originalPurchaseDate);
1939
+ data.subscription.latestPurchaseDate = new Date(data.subscription.latestPurchaseDate);
1940
+ if (data.subscription.cancellationDate) {
1941
+ data.subscription.cancellationDate = new Date(data.subscription.cancellationDate);
1942
+ }
1943
+ if (data.subscription.gracePeriodExpiresAt) {
1944
+ data.subscription.gracePeriodExpiresAt = new Date(data.subscription.gracePeriodExpiresAt);
1945
+ }
1946
+ }
1947
+ return data;
1948
+ }
1949
+ getEmptyStatus() {
1950
+ return {
1951
+ hasActiveSubscription: false,
1952
+ subscription: null,
1953
+ entitlements: []
1954
+ };
1955
+ }
1956
+ addListener(listener) {
1957
+ this.listeners.add(listener);
1958
+ return () => this.listeners.delete(listener);
1959
+ }
1960
+ notifyListeners(status) {
1961
+ for (const listener of this.listeners) {
1962
+ listener(status);
1963
+ }
1964
+ }
1965
+ async onPurchaseComplete() {
1966
+ await this.cache.invalidate(this.cacheKey());
1967
+ await this.getSubscriptionStatus(true);
1968
+ }
1969
+ log(...args) {
1970
+ if (this.config?.debug) console.log("[Paywallo:Subscription]", ...args);
1971
+ }
1972
+ };
1973
+ var subscriptionManager = new SubscriptionManagerClass();
1974
+
1975
+ // src/core/ApiClient.ts
1976
+ import { Platform as Platform5 } from "react-native";
1977
+
1978
+ // src/core/apiUrlUtils.ts
1979
+ var DEFAULT_WEB_URL = "https://paywallo.com.br";
1980
+ function deriveWebUrl(baseUrl) {
1981
+ try {
1982
+ const url = new URL(baseUrl);
1983
+ if (url.hostname.startsWith("192.168") || url.hostname === "localhost") {
1984
+ url.port = "3000";
1985
+ return url.toString().replace(/\/$/, "");
1986
+ }
1987
+ if (url.hostname.startsWith("api.")) {
1988
+ url.hostname = url.hostname.replace("api.", "app.");
1989
+ return url.toString().replace(/\/$/, "");
1990
+ }
1991
+ return DEFAULT_WEB_URL;
1992
+ } catch {
1993
+ return DEFAULT_WEB_URL;
1994
+ }
1995
+ }
1996
+ function buildConditionalFlagUrl(baseUrl, flagKey, context) {
1997
+ const url = new URL(`${baseUrl}/api/v1/conditional-flags/${flagKey}`);
1998
+ if (context.platform) url.searchParams.set("platform", context.platform);
1999
+ if (context.appVersion) url.searchParams.set("appVersion", context.appVersion);
2000
+ if (context.country) url.searchParams.set("country", context.country);
2001
+ if (context.distinctId) url.searchParams.set("distinctId", context.distinctId);
2002
+ return url.toString();
2003
+ }
2004
+
2005
+ // src/domains/iap/apiPurchaseMethods.ts
2006
+ async function validatePurchase(client, appKey, platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
2007
+ const response = await client.post(
2008
+ `/purchases/validate/${appKey}`,
2009
+ {
2010
+ platform,
2011
+ receipt,
2012
+ productId,
2013
+ transactionId,
2014
+ priceLocal,
2015
+ currency,
2016
+ country,
2017
+ distinctId,
2018
+ ...paywallPlacement !== void 0 && { paywallPlacement },
2019
+ ...variantKey !== void 0 && { variantKey }
2020
+ },
2021
+ false
2022
+ );
2023
+ if (!response.ok) {
2024
+ throw new Error(`Server validation failed: HTTP ${response.status}`);
2025
+ }
2026
+ return response.data;
2027
+ }
2028
+ async function getSubscriptionStatus(client, baseUrl, appKey, distinctId) {
1708
2029
  const url = new URL(`${baseUrl}/purchases/status/${appKey}`);
1709
2030
  url.searchParams.set("distinctId", distinctId);
1710
2031
  const response = await client.get(url.toString());
2032
+ if (!response.ok) {
2033
+ throw new Error(`Subscription status check failed: HTTP ${response.status}`);
2034
+ }
1711
2035
  return response.data;
1712
2036
  }
1713
2037
  async function getEmergencyPaywall(client) {
@@ -1887,10 +2211,8 @@ var HttpClient = class {
1887
2211
  sleep(ms) {
1888
2212
  return new Promise((resolve) => setTimeout(resolve, ms));
1889
2213
  }
1890
- log(message, data) {
1891
- if (this.config.debug) {
1892
- console.warn("[HttpClient]", message, data ?? "");
1893
- }
2214
+ log(...args) {
2215
+ if (this.config.debug) console.log("[Paywallo:Http]", ...args);
1894
2216
  }
1895
2217
  setDebug(debug) {
1896
2218
  this.config.debug = debug;
@@ -1916,8 +2238,6 @@ var NetworkMonitorClass = class {
1916
2238
  this.config = DEFAULT_NETWORK_CONFIG;
1917
2239
  this.listeners = /* @__PURE__ */ new Set();
1918
2240
  this.currentState = "unknown";
1919
- this.netInfoModule = null;
1920
- this.netInfoSubscription = null;
1921
2241
  this.appStateSubscription = null;
1922
2242
  this.initialized = false;
1923
2243
  this.checkIntervalId = null;
@@ -1933,13 +2253,6 @@ var NetworkMonitorClass = class {
1933
2253
  void this.setupFallbackDetection();
1934
2254
  this.setupAppStateListener();
1935
2255
  }
1936
- setupNetInfoListener() {
1937
- if (!this.netInfoModule) return;
1938
- this.netInfoSubscription = this.netInfoModule.addEventListener((state) => {
1939
- const newState = this.netInfoStateToNetworkState(state);
1940
- this.updateState(newState);
1941
- });
1942
- }
1943
2256
  async setupFallbackDetection() {
1944
2257
  await this.checkConnectivity();
1945
2258
  this.checkIntervalId = setInterval(() => {
@@ -1967,15 +2280,6 @@ var NetworkMonitorClass = class {
1967
2280
  this.updateState("offline");
1968
2281
  }
1969
2282
  }
1970
- netInfoStateToNetworkState(state) {
1971
- if (state.isConnected === null) {
1972
- return "unknown";
1973
- }
1974
- if (!state.isConnected) {
1975
- return "offline";
1976
- }
1977
- return "online";
1978
- }
1979
2283
  updateState(newState) {
1980
2284
  const previousState = this.currentState;
1981
2285
  if (previousState === newState) {
@@ -2018,10 +2322,6 @@ var NetworkMonitorClass = class {
2018
2322
  return this.currentState;
2019
2323
  }
2020
2324
  dispose() {
2021
- if (this.netInfoSubscription) {
2022
- this.netInfoSubscription();
2023
- this.netInfoSubscription = null;
2024
- }
2025
2325
  if (this.appStateSubscription) {
2026
2326
  this.appStateSubscription.remove();
2027
2327
  this.appStateSubscription = null;
@@ -2034,10 +2334,8 @@ var NetworkMonitorClass = class {
2034
2334
  this.initialized = false;
2035
2335
  this.currentState = "unknown";
2036
2336
  }
2037
- log(message, data) {
2038
- if (this.config.debug) {
2039
- console.warn("[NetworkMonitor]", message, data ?? "");
2040
- }
2337
+ log(...args) {
2338
+ if (this.config.debug) console.log("[Paywallo:Network]", ...args);
2041
2339
  }
2042
2340
  setDebug(debug) {
2043
2341
  this.config.debug = debug;
@@ -2047,6 +2345,7 @@ var networkMonitor = new NetworkMonitorClass();
2047
2345
 
2048
2346
  // src/core/queue/OfflineQueue.ts
2049
2347
  import AsyncStorage2 from "@react-native-async-storage/async-storage";
2348
+ init_uuid();
2050
2349
 
2051
2350
  // src/core/queue/deduplication.ts
2052
2351
  function normalizeForComparison(obj) {
@@ -2067,6 +2366,8 @@ function normalizeForComparison(obj) {
2067
2366
  return "{" + pairs.join(",") + "}";
2068
2367
  }
2069
2368
  function findDuplicateIndex(queue, item) {
2369
+ const byId = queue.findIndex((existing) => existing.id === item.id);
2370
+ if (byId !== -1) return byId;
2070
2371
  const normalizedBody = normalizeForComparison(item.body);
2071
2372
  return queue.findIndex(
2072
2373
  (existing) => existing.url === item.url && normalizeForComparison(existing.body) === normalizedBody
@@ -2135,9 +2436,9 @@ var OfflineQueueClass = class {
2135
2436
  void this.saveToStorage();
2136
2437
  }
2137
2438
  }
2138
- async enqueue(method, url, body, headers) {
2439
+ async enqueue(method, url, body, headers, id) {
2139
2440
  const item = {
2140
- id: generateUUID(),
2441
+ id: id ?? generateUUID(),
2141
2442
  method,
2142
2443
  url,
2143
2444
  body,
@@ -2221,7 +2522,6 @@ var OfflineQueueClass = class {
2221
2522
  this.emit({ type: "processing:end", queueSize: this.queue.length });
2222
2523
  return { processed, failed };
2223
2524
  }
2224
- /** Returns true if the item was permanently removed (counted as failed). */
2225
2525
  async handleItemFailure(item, error, permanent = false) {
2226
2526
  if (permanent || item.attempts >= item.maxAttempts) {
2227
2527
  await this.removeItem(item.id);
@@ -2283,10 +2583,8 @@ var OfflineQueueClass = class {
2283
2583
  }
2284
2584
  }
2285
2585
  }
2286
- log(message, data) {
2287
- if (this.config.debug) {
2288
- console.warn("[OfflineQueue]", message, data ?? "");
2289
- }
2586
+ log(...args) {
2587
+ if (this.config.debug) console.log("[Paywallo:Queue]", ...args);
2290
2588
  }
2291
2589
  setDebug(debug) {
2292
2590
  this.config.debug = debug;
@@ -2428,10 +2726,8 @@ var QueueProcessorClass = class {
2428
2726
  this.httpClient = null;
2429
2727
  this.initialized = false;
2430
2728
  }
2431
- log(message, data) {
2432
- if (this.config.debug) {
2433
- console.warn("[QueueProcessor]", message, data ?? "");
2434
- }
2729
+ log(...args) {
2730
+ if (this.config.debug) console.log("[Paywallo:QueueProcessor]", ...args);
2435
2731
  }
2436
2732
  setDebug(debug) {
2437
2733
  this.config.debug = debug;
@@ -2443,10 +2739,9 @@ var queueProcessor = new QueueProcessorClass();
2443
2739
  var ApiCache = class {
2444
2740
  constructor() {
2445
2741
  this.CACHE_TTL = 3e5;
2446
- // 5 minutes
2447
2742
  this.CACHE_NULL_TTL = 3e4;
2448
- // 30 seconds — for null/404 variants
2449
2743
  this.MAX_SIZE = 200;
2744
+ this.STALE_WINDOW_MS = 864e5;
2450
2745
  this.variantCache = /* @__PURE__ */ new Map();
2451
2746
  this.paywallCache = /* @__PURE__ */ new Map();
2452
2747
  this.campaignCache = /* @__PURE__ */ new Map();
@@ -2484,7 +2779,7 @@ var ApiCache = class {
2484
2779
  }
2485
2780
  getStaleVariant(key) {
2486
2781
  const entry = this.variantCache.get(key);
2487
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2782
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2488
2783
  return void 0;
2489
2784
  }
2490
2785
  getPaywall(key) {
@@ -2496,42 +2791,42 @@ var ApiCache = class {
2496
2791
  }
2497
2792
  getStalePaywall(key) {
2498
2793
  const entry = this.paywallCache.get(key);
2499
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2794
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2500
2795
  return void 0;
2501
2796
  }
2502
2797
  getCampaign(key) {
2503
2798
  return this.getCached(this.campaignCache, key);
2504
2799
  }
2505
- setCampaign(key, value) {
2506
- this.setCached(this.campaignCache, key, value);
2800
+ setCampaign(key, value, ttlOverride) {
2801
+ this.setCached(this.campaignCache, key, value, ttlOverride);
2507
2802
  this.evictExpiredEntries(this.campaignCache);
2508
2803
  }
2509
2804
  getStaleCampaign(key) {
2510
2805
  const entry = this.campaignCache.get(key);
2511
- if (entry && Date.now() - entry.ts < 864e5) return entry.value;
2806
+ if (entry && Date.now() - entry.ts < this.STALE_WINDOW_MS) return entry.value;
2512
2807
  return void 0;
2513
2808
  }
2514
2809
  };
2515
2810
 
2516
2811
  // src/core/ApiClient.ts
2517
- var SDK_VERSION = "1.0.0";
2518
- var DEFAULT_BASE_URL = "https://panel.lucasqueiroga.shop";
2812
+ var SDK_VERSION = "1.4.0";
2519
2813
  var _ApiClient = class _ApiClient {
2520
- constructor(appKey, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
2814
+ constructor(appKey, apiUrl, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
2521
2815
  this.cache = new ApiCache();
2522
2816
  this.appKey = appKey;
2523
- this.baseUrl = DEFAULT_BASE_URL;
2817
+ this.baseUrl = apiUrl;
2524
2818
  this.webUrl = deriveWebUrl(this.baseUrl);
2525
2819
  this.debug = debug;
2526
2820
  this.environment = environment;
2527
2821
  this.offlineQueueEnabled = offlineQueueEnabled;
2528
2822
  this.httpClient = new HttpClient(this.baseUrl, { timeout, debug });
2529
2823
  }
2530
- // ─── Infrastructure ──────────────────────────────────────────────────────────
2531
- /** Exposed only for QueueProcessor initialization. Prefer get()/post() for requests. */
2532
2824
  getHttpClient() {
2533
2825
  return this.httpClient;
2534
2826
  }
2827
+ getBaseUrl() {
2828
+ return this.baseUrl;
2829
+ }
2535
2830
  getWebUrl() {
2536
2831
  return this.webUrl;
2537
2832
  }
@@ -2557,7 +2852,6 @@ var _ApiClient = class _ApiClient {
2557
2852
  async clearQueue() {
2558
2853
  await offlineQueue.clear();
2559
2854
  }
2560
- // ─── Public HTTP facade (headers injected automatically) ─────────────────────
2561
2855
  async get(path) {
2562
2856
  return this.httpClient.get(path, { headers: { "X-App-Key": this.appKey } });
2563
2857
  }
@@ -2571,29 +2865,29 @@ var _ApiClient = class _ApiClient {
2571
2865
  message,
2572
2866
  context,
2573
2867
  sdkVersion: SDK_VERSION,
2574
- platform: Platform3.OS
2868
+ platform: Platform5.OS
2575
2869
  }, true);
2576
- } catch {
2870
+ } catch (err) {
2871
+ if (this.debug) console.warn("[Paywallo:ApiClient] reportError failed", err);
2577
2872
  }
2578
2873
  }
2579
- // ─── Business methods ────────────────────────────────────────────────────────
2580
2874
  async trackEvent(eventName, distinctId, properties, timestamp) {
2581
2875
  await this.postWithQueue("/api/v1/events", {
2582
2876
  eventName,
2583
2877
  distinctId,
2584
- properties: { ...properties, platform: Platform3.OS },
2878
+ properties: { ...properties, platform: Platform5.OS },
2585
2879
  timestamp: timestamp ?? Date.now(),
2586
2880
  environment: this.environment.toLowerCase()
2587
2881
  }, `event:${eventName}`);
2588
2882
  }
2589
- async identify(distinctId, properties, email) {
2590
- await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: Platform3.OS }, "identify");
2883
+ async identify(distinctId, properties, email, deviceId) {
2884
+ await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: Platform5.OS, ...deviceId && { deviceId } }, "identify");
2591
2885
  }
2592
2886
  async startSession(distinctId, sessionId, platform) {
2593
2887
  await this.postWithQueue("/api/v1/sessions/start", {
2594
2888
  distinctId,
2595
2889
  sessionId,
2596
- platform: platform ?? Platform3.OS,
2890
+ platform: platform ?? Platform5.OS,
2597
2891
  environment: this.environment.toLowerCase()
2598
2892
  }, `session.start:${sessionId}`);
2599
2893
  return { success: true };
@@ -2671,7 +2965,7 @@ var _ApiClient = class _ApiClient {
2671
2965
  const result = await getCampaignData(this, this.baseUrl, placement, distinctId, context);
2672
2966
  if (!result) {
2673
2967
  this.log("No campaign found for placement:", placement);
2674
- this.cache.setCampaign(key, null);
2968
+ this.cache.setCampaign(key, null, this.cache.nullTTL);
2675
2969
  return null;
2676
2970
  }
2677
2971
  this.log("Got campaign:", placement, result.variantKey);
@@ -2684,6 +2978,32 @@ var _ApiClient = class _ApiClient {
2684
2978
  return null;
2685
2979
  }
2686
2980
  }
2981
+ async getPrimaryCampaign(distinctId) {
2982
+ const key = `primary:${distinctId}`;
2983
+ const hit = this.cache.getCampaign(key);
2984
+ if (hit !== void 0) return hit;
2985
+ try {
2986
+ const res = await this.get(`/campaigns/public/primary?distinctId=${encodeURIComponent(distinctId)}`);
2987
+ if (res.status === 404) {
2988
+ this.cache.setCampaign(key, null, this.cache.nullTTL);
2989
+ return null;
2990
+ }
2991
+ if (!res.ok) {
2992
+ this.log("Non-OK response for primary campaign:", res.status);
2993
+ const stale = this.cache.getStaleCampaign(key);
2994
+ if (stale !== void 0) return stale;
2995
+ return null;
2996
+ }
2997
+ this.log("Got primary campaign:", res.data?.campaignId);
2998
+ this.cache.setCampaign(key, res.data);
2999
+ return res.data;
3000
+ } catch (error) {
3001
+ this.log("Failed to get primary campaign:", error);
3002
+ const stale = this.cache.getStaleCampaign(key);
3003
+ if (stale !== void 0) return stale;
3004
+ return null;
3005
+ }
3006
+ }
2687
3007
  async validatePurchase(platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
2688
3008
  const result = await validatePurchase(
2689
3009
  this,
@@ -2752,12 +3072,6 @@ var _ApiClient = class _ApiClient {
2752
3072
  return { value: false, flagKey };
2753
3073
  }
2754
3074
  }
2755
- // ─── Cache-only reads ────────────────────────────────────────────────────────
2756
- /**
2757
- * Reads a flag variant from cache layers only — no network call.
2758
- * Checks in-memory cache first, then SecureStorage.
2759
- * Returns null when both layers miss.
2760
- */
2761
3075
  async getVariantFromCache(flagKey, distinctId) {
2762
3076
  const key = `${flagKey}:${distinctId}`;
2763
3077
  const hit = this.cache.getVariant(key);
@@ -2786,13 +3100,10 @@ var _ApiClient = class _ApiClient {
2786
3100
  try {
2787
3101
  const value = JSON.stringify({ variant: flag.variant, payload: flag.payload, cachedAt: Date.now() });
2788
3102
  await secureStorage.set(`flag:${flagKey}:${distinctId}`, value);
2789
- } catch {
3103
+ } catch (err) {
3104
+ this.log("writeFlagToStorage failed", err);
2790
3105
  }
2791
3106
  }
2792
- /**
2793
- * POST with offline queue fallback. Enqueues when offline or on network failure.
2794
- * Throws only when offlineQueueEnabled is false and the request fails.
2795
- */
2796
3107
  async postWithQueue(url, payload, label) {
2797
3108
  if (this.offlineQueueEnabled && !networkMonitor.isOnline()) {
2798
3109
  await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
@@ -2812,10 +3123,9 @@ var _ApiClient = class _ApiClient {
2812
3123
  }
2813
3124
  }
2814
3125
  log(...args) {
2815
- if (this.debug) console.warn("[Paywallo API]", ...args);
3126
+ if (this.debug) console.log("[Paywallo:ApiClient]", ...args);
2816
3127
  }
2817
3128
  };
2818
- // ─── Private helpers ─────────────────────────────────────────────────────────
2819
3129
  _ApiClient.FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2820
3130
  var ApiClient = _ApiClient;
2821
3131
 
@@ -2836,42 +3146,72 @@ var CAMPAIGN_ERROR_CODES = {
2836
3146
 
2837
3147
  // src/domains/campaign/CampaignGateService.ts
2838
3148
  var PRELOAD_CACHE_TTL_MS = 5 * 60 * 1e3;
3149
+ var PRELOAD_WAIT_MAX_MS = 2e3;
3150
+ var PRELOAD_WAIT_INTERVAL_MS = 100;
2839
3151
  var CampaignGateService = class {
2840
3152
  constructor() {
2841
3153
  this.preloadedCampaigns = /* @__PURE__ */ new Map();
3154
+ this.pendingPreloads = /* @__PURE__ */ new Set();
2842
3155
  }
2843
3156
  async getCampaign(apiClient, placement, context) {
2844
3157
  const distinctId = identityManager.getDistinctId();
2845
3158
  if (!distinctId) return null;
2846
3159
  return apiClient.getCampaign(placement, distinctId, context);
2847
3160
  }
3161
+ async waitForPreload(placement) {
3162
+ if (!this.pendingPreloads.has(placement)) return;
3163
+ const deadline = Date.now() + PRELOAD_WAIT_MAX_MS;
3164
+ await new Promise((resolve) => {
3165
+ const check = () => {
3166
+ if (!this.pendingPreloads.has(placement) || Date.now() >= deadline) {
3167
+ resolve();
3168
+ return;
3169
+ }
3170
+ setTimeout(check, PRELOAD_WAIT_INTERVAL_MS);
3171
+ };
3172
+ check();
3173
+ });
3174
+ }
2848
3175
  async presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2849
- const campaign = await this.getCampaign(apiClient, placement, context);
3176
+ await this.waitForPreload(placement);
3177
+ const preloaded = this.getPreloadedCampaign(placement);
3178
+ const campaign = preloaded ?? await this.getCampaign(apiClient, placement, context);
2850
3179
  if (!campaign) {
2851
3180
  return { presented: false, purchased: false, cancelled: false, restored: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, "No campaign found") };
2852
3181
  }
2853
- if (!context?.forceShow && await hasActive()) {
2854
- return { presented: false, purchased: false, cancelled: false, restored: false, skippedReason: "subscriber" };
3182
+ if (!context?.forceShow) {
3183
+ const isActive = await hasActive();
3184
+ if (isActive) {
3185
+ return { presented: false, purchased: false, cancelled: false, restored: false, skippedReason: "subscriber" };
3186
+ }
2855
3187
  }
2856
3188
  if (campaignPresenter) return campaignPresenter(campaign);
2857
3189
  if (!paywallPresenter) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
2858
- return { ...await paywallPresenter(campaign.paywall.placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
3190
+ return { ...await paywallPresenter(placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
2859
3191
  }
2860
3192
  async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2861
- if (await hasActive()) return true;
3193
+ const isActive = await hasActive();
3194
+ if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo] requireSubscription \u2014 hasActive: ${isActive}`);
3195
+ if (isActive) return true;
2862
3196
  const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context);
2863
3197
  return r.purchased || r.restored;
2864
3198
  }
2865
3199
  async preloadCampaign(apiClient, placement, context) {
3200
+ this.pendingPreloads.add(placement);
2866
3201
  try {
2867
3202
  const campaign = await this.getCampaign(apiClient, placement, context);
2868
3203
  if (!campaign?.paywall) {
2869
3204
  return { success: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, `Campaign or paywall not found: ${placement}`) };
2870
3205
  }
2871
3206
  this.preloadedCampaigns.set(placement, { campaign, preloadedAt: Date.now() });
3207
+ if (PaywalloClient.getConfig()?.debug) {
3208
+ console.info(`[Paywallo PRELOAD] data cached \u2014 placement: ${placement}`);
3209
+ }
2872
3210
  return { success: true };
2873
3211
  } catch (error) {
2874
3212
  return { success: false, error: error instanceof Error ? error : new PaywalloError("panel", "PANEL_UNKNOWN", String(error)) };
3213
+ } finally {
3214
+ this.pendingPreloads.delete(placement);
2875
3215
  }
2876
3216
  }
2877
3217
  getPreloadedCampaign(placement) {
@@ -2885,12 +3225,13 @@ var CampaignGateService = class {
2885
3225
  }
2886
3226
  clearPreloaded() {
2887
3227
  this.preloadedCampaigns.clear();
3228
+ this.pendingPreloads.clear();
2888
3229
  }
2889
3230
  };
2890
3231
  var campaignGateService = new CampaignGateService();
2891
3232
 
2892
3233
  // src/domains/identity/AdvertisingIdManager.ts
2893
- import { Platform as Platform4 } from "react-native";
3234
+ import { Platform as Platform6 } from "react-native";
2894
3235
  var ZERO_IDFA = "00000000-0000-0000-0000-000000000000";
2895
3236
  var AdvertisingIdManager = class {
2896
3237
  constructor() {
@@ -2901,7 +3242,7 @@ var AdvertisingIdManager = class {
2901
3242
  const fallback = { idfv: null, idfa: null, gaid: null, attStatus: "unavailable" };
2902
3243
  try {
2903
3244
  const tracking = await import("expo-tracking-transparency");
2904
- if (Platform4.OS === "ios") {
3245
+ if (Platform6.OS === "ios") {
2905
3246
  if (!tracking.isAvailable()) {
2906
3247
  const idfv2 = await this.collectIdfv();
2907
3248
  this.cached = { ...fallback, idfv: idfv2 };
@@ -2920,7 +3261,7 @@ var AdvertisingIdManager = class {
2920
3261
  this.cached = { idfv, idfa, gaid: null, attStatus: status };
2921
3262
  return this.cached;
2922
3263
  }
2923
- if (Platform4.OS === "android") {
3264
+ if (Platform6.OS === "android") {
2924
3265
  const androidStatus = await tracking.getTrackingPermissionsAsync();
2925
3266
  const optedIn = androidStatus.granted;
2926
3267
  const rawGaid = optedIn ? tracking.getAdvertisingId() : null;
@@ -2950,43 +3291,67 @@ var AdvertisingIdManager = class {
2950
3291
  }
2951
3292
  };
2952
3293
 
2953
- // src/domains/identity/FacebookIdManager.ts
3294
+ // src/domains/identity/InstallTracker.ts
3295
+ import { Dimensions as Dimensions3, Platform as Platform8 } from "react-native";
3296
+ import AsyncStorage3 from "@react-native-async-storage/async-storage";
3297
+ import RNDeviceInfo from "react-native-device-info";
3298
+
3299
+ // src/domains/identity/MetaBridge.ts
3300
+ import { NativeModules as NativeModules4 } from "react-native";
2954
3301
  var TIMEOUT_MS = 2e3;
2955
- var FacebookIdManager = class {
2956
- async getAnonId() {
3302
+ var NativeBridge = NativeModules4.PaywalloFBBridge ?? null;
3303
+ var MetaBridge = class {
3304
+ async getAnonymousID() {
3305
+ if (!NativeBridge) return null;
2957
3306
  try {
2958
3307
  let timerId;
2959
- const timeoutPromise = new Promise((resolve) => {
3308
+ const timeout = new Promise((resolve) => {
2960
3309
  timerId = setTimeout(() => resolve(null), TIMEOUT_MS);
2961
3310
  });
2962
- const result = await Promise.race([this.fetchAnonId(), timeoutPromise]);
3311
+ const result = await Promise.race([NativeBridge.getAnonymousID(), timeout]);
2963
3312
  clearTimeout(timerId);
2964
3313
  return result;
2965
3314
  } catch {
2966
3315
  return null;
2967
3316
  }
2968
3317
  }
2969
- async fetchAnonId() {
3318
+ async logEvent(name, params = {}) {
3319
+ if (!NativeBridge) return;
2970
3320
  try {
2971
- const fbsdk = await import("react-native-fbsdk-next");
2972
- const anonId = await fbsdk.AppEventsLogger.getAnonymousID();
2973
- return anonId || null;
3321
+ await NativeBridge.logEvent(name, params);
3322
+ } catch {
3323
+ }
3324
+ }
3325
+ async logPurchase(amount, currency, params = {}) {
3326
+ if (!NativeBridge) return;
3327
+ try {
3328
+ await NativeBridge.logPurchase(amount, currency, params);
3329
+ } catch {
3330
+ }
3331
+ }
3332
+ setUserID(userId) {
3333
+ if (!NativeBridge) return;
3334
+ try {
3335
+ NativeBridge.setUserID(userId);
3336
+ } catch {
3337
+ }
3338
+ }
3339
+ flush() {
3340
+ if (!NativeBridge) return;
3341
+ try {
3342
+ NativeBridge.flush();
2974
3343
  } catch {
2975
- return null;
2976
3344
  }
2977
3345
  }
2978
3346
  };
2979
- var facebookIdManager = new FacebookIdManager();
2980
-
2981
- // src/domains/identity/InstallTracker.ts
2982
- import { Dimensions as Dimensions3, Platform as Platform6 } from "react-native";
3347
+ var metaBridge = new MetaBridge();
2983
3348
 
2984
3349
  // src/domains/identity/InstallReferrerManager.ts
2985
- import { Platform as Platform5 } from "react-native";
3350
+ import { Platform as Platform7 } from "react-native";
2986
3351
  var REFERRER_TIMEOUT_MS = 5e3;
2987
3352
  var InstallReferrerManager = class {
2988
3353
  async getReferrer() {
2989
- if (Platform5.OS !== "android") {
3354
+ if (Platform7.OS !== "android") {
2990
3355
  return this.emptyResult();
2991
3356
  }
2992
3357
  let timerId;
@@ -3047,20 +3412,7 @@ var installReferrerManager = new InstallReferrerManager();
3047
3412
 
3048
3413
  // src/domains/session/SessionManager.ts
3049
3414
  import { AppState as AppState2 } from "react-native";
3050
-
3051
- // src/domains/session/SessionError.ts
3052
- var SessionError = class extends PaywalloError {
3053
- constructor(code, message) {
3054
- super("session", code, message);
3055
- this.name = "SessionError";
3056
- }
3057
- };
3058
- var SESSION_ERROR_CODES = {
3059
- NOT_INITIALIZED: "SESSION_NOT_INITIALIZED",
3060
- START_FAILED: "SESSION_START_FAILED",
3061
- END_FAILED: "SESSION_END_FAILED",
3062
- RESTORE_FAILED: "SESSION_RESTORE_FAILED"
3063
- };
3415
+ init_uuid();
3064
3416
 
3065
3417
  // src/domains/session/sessionTracking.ts
3066
3418
  async function trackSessionStart(apiClient, sessionId, sessionStartedAt) {
@@ -3290,9 +3642,7 @@ var SessionManager = class {
3290
3642
  }
3291
3643
  }
3292
3644
  log(...args) {
3293
- if (this.debug) {
3294
- console.warn("[Paywallo Session]", ...args);
3295
- }
3645
+ if (this.debug) console.log("[Paywallo:Session]", ...args);
3296
3646
  }
3297
3647
  };
3298
3648
  var sessionManager = new SessionManager();
@@ -3305,11 +3655,10 @@ var InstallTracker = class {
3305
3655
  }
3306
3656
  async trackIfNeeded(apiClient, requestATT = false) {
3307
3657
  const storage = new SecureStorage(this.debug);
3308
- const alreadyTracked = await storage.get("install_tracked");
3658
+ const alreadyTracked = await storage.get("@panel:install_tracked");
3309
3659
  if (alreadyTracked) return;
3310
3660
  let fallbackTracked = null;
3311
3661
  try {
3312
- const AsyncStorage3 = (await import("@react-native-async-storage/async-storage")).default;
3313
3662
  fallbackTracked = await AsyncStorage3.getItem("@paywallo:install_tracked");
3314
3663
  } catch {
3315
3664
  }
@@ -3318,13 +3667,7 @@ var InstallTracker = class {
3318
3667
  const sessionId = sessionManager.getSessionId();
3319
3668
  const installedAt = Date.now();
3320
3669
  if (!distinctId) return;
3321
- let deviceInfo = null;
3322
- try {
3323
- const mod = await import("react-native-device-info");
3324
- deviceInfo = mod.default;
3325
- } catch {
3326
- deviceInfo = null;
3327
- }
3670
+ const deviceInfo = RNDeviceInfo;
3328
3671
  let deviceData = {};
3329
3672
  try {
3330
3673
  const screen = Dimensions3.get("screen");
@@ -3336,6 +3679,38 @@ var InstallTracker = class {
3336
3679
  const osVersion = deviceInfo?.getSystemVersion() || "unknown";
3337
3680
  const appVersion = deviceInfo?.getVersion() || "unknown";
3338
3681
  const buildNumber = deviceInfo?.getBuildNumber() || "0";
3682
+ let carrier = "unknown";
3683
+ try {
3684
+ carrier = await deviceInfo?.getCarrier?.() ?? "unknown";
3685
+ } catch {
3686
+ }
3687
+ let totalDisk = 0;
3688
+ try {
3689
+ totalDisk = Number(await deviceInfo?.getTotalDiskCapacity?.()) || 0;
3690
+ } catch {
3691
+ }
3692
+ let freeDisk = 0;
3693
+ try {
3694
+ freeDisk = Number(await deviceInfo?.getFreeDiskStorage?.()) || 0;
3695
+ } catch {
3696
+ }
3697
+ let totalRam = 0;
3698
+ try {
3699
+ totalRam = Number(await deviceInfo?.getTotalMemory?.()) || 0;
3700
+ } catch {
3701
+ }
3702
+ let brand = "unknown";
3703
+ try {
3704
+ brand = deviceInfo?.getBrand?.() ?? "unknown";
3705
+ } catch {
3706
+ }
3707
+ let installerPackage = null;
3708
+ try {
3709
+ if (Platform8.OS === "android") {
3710
+ installerPackage = await deviceInfo?.getInstallerPackageName?.() ?? null;
3711
+ }
3712
+ } catch {
3713
+ }
3339
3714
  deviceData = {
3340
3715
  deviceModel,
3341
3716
  osVersion,
@@ -3344,25 +3719,40 @@ var InstallTracker = class {
3344
3719
  ...screenWidth > 0 && { screenWidth },
3345
3720
  ...screenHeight > 0 && { screenHeight },
3346
3721
  timezone,
3347
- locale
3722
+ locale,
3723
+ ...carrier !== "unknown" && { carrier },
3724
+ ...totalDisk > 0 && { totalDisk },
3725
+ ...freeDisk > 0 && { freeDisk },
3726
+ ...totalRam > 0 && { totalRam },
3727
+ ...brand !== "unknown" && { brand },
3728
+ ...installerPackage && { installerPackage }
3348
3729
  };
3349
3730
  } catch {
3350
- if (this.debug) {
3351
- console.warn("[Paywallo] Failed to collect device data for install event");
3352
- }
3353
3731
  }
3354
3732
  const [fbAnonId, referrer, adIds] = await Promise.all([
3355
- facebookIdManager.getAnonId(),
3733
+ metaBridge.getAnonymousID(),
3356
3734
  installReferrerManager.getReferrer(),
3357
3735
  this.advertisingIdManager.collect(requestATT)
3358
3736
  ]);
3359
- try {
3737
+ let effectiveAnonId = fbAnonId;
3738
+ if (!effectiveAnonId) {
3739
+ const stored = await storage.get("@panel:anon_id");
3740
+ if (stored) {
3741
+ effectiveAnonId = stored;
3742
+ } else {
3743
+ const { generateUUID: generateUUID2 } = await Promise.resolve().then(() => (init_uuid(), uuid_exports));
3744
+ effectiveAnonId = `PW_${generateUUID2()}`;
3745
+ await storage.set("@panel:anon_id", effectiveAnonId).catch(() => {
3746
+ });
3747
+ }
3748
+ }
3749
+ try {
3360
3750
  await apiClient.trackEvent("$app_installed", distinctId, {
3361
3751
  installedAt,
3362
- platform: Platform6.OS,
3752
+ platform: Platform8.OS,
3363
3753
  ...sessionId && { sessionId },
3364
3754
  ...deviceData,
3365
- ...fbAnonId && { fbAnonId },
3755
+ ...effectiveAnonId && { fbAnonId: effectiveAnonId },
3366
3756
  ...referrer.rawReferrer && { installReferrer: referrer.rawReferrer },
3367
3757
  ...referrer.trackingId && { referrerTrackingId: referrer.trackingId },
3368
3758
  ...referrer.fbclid && { referrerFbclid: referrer.fbclid },
@@ -3374,19 +3764,47 @@ var InstallTracker = class {
3374
3764
  ...adIds.gaid && { gaid: adIds.gaid },
3375
3765
  attStatus: adIds.attStatus
3376
3766
  });
3377
- const stored = await storage.set("install_tracked", String(installedAt));
3767
+ if (Platform8.OS === "ios") {
3768
+ void this.callDeferredMatch(apiClient, deviceData, adIds, effectiveAnonId).catch(() => {
3769
+ });
3770
+ }
3771
+ await storage.set("@panel:install_tracked", String(installedAt));
3378
3772
  try {
3379
- const AsyncStorage3 = (await import("@react-native-async-storage/async-storage")).default;
3380
3773
  await AsyncStorage3.setItem("@paywallo:install_tracked", String(installedAt));
3381
3774
  } catch {
3382
3775
  }
3383
- if (this.debug && !stored) {
3384
- console.warn("[Paywallo] Install event tracked but flag storage failed \u2014 may re-track on next launch");
3385
- }
3386
- } catch (error) {
3387
- if (this.debug) {
3388
- console.warn("[Paywallo] Failed to track install:", error);
3389
- }
3776
+ } catch {
3777
+ }
3778
+ }
3779
+ async callDeferredMatch(apiClient, deviceData, adIds, anonId) {
3780
+ const storage = new SecureStorage(this.debug);
3781
+ const alreadyMatched = await storage.get("@panel:deferred_match_done");
3782
+ if (alreadyMatched) return;
3783
+ const controller = new AbortController();
3784
+ const timer = setTimeout(() => controller.abort(), 5e3);
3785
+ try {
3786
+ const baseUrl = apiClient.getBaseUrl();
3787
+ const appKey = apiClient.appKey;
3788
+ await fetch(`${baseUrl}/api/v1/attribution/deferred-match`, {
3789
+ method: "POST",
3790
+ headers: { "Content-Type": "application/json", "X-App-Key": appKey },
3791
+ body: JSON.stringify({
3792
+ idfv: adIds.idfv,
3793
+ deviceModel: deviceData.deviceModel,
3794
+ osVersion: deviceData.osVersion,
3795
+ screenWidth: deviceData.screenWidth,
3796
+ screenHeight: deviceData.screenHeight,
3797
+ timezone: deviceData.timezone,
3798
+ locale: deviceData.locale,
3799
+ fbAnonId: anonId
3800
+ }),
3801
+ signal: controller.signal
3802
+ });
3803
+ await storage.set("@panel:deferred_match_done", "1").catch(() => {
3804
+ });
3805
+ } catch {
3806
+ } finally {
3807
+ clearTimeout(timer);
3390
3808
  }
3391
3809
  }
3392
3810
  };
@@ -3396,25 +3814,13 @@ function isValidEventName(name) {
3396
3814
  if (name.startsWith("$")) return /^\$[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3397
3815
  return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3398
3816
  }
3399
- var OFFLINE_ACTIVE_STATUSES = ["active", "grace_period"];
3400
- async function resolveClientOfflineFromCache() {
3401
- try {
3402
- const cached = await subscriptionCache.get();
3403
- if (!cached) return false;
3404
- const sub = cached.data.subscription;
3405
- if (!sub) return false;
3406
- if (!OFFLINE_ACTIVE_STATUSES.includes(sub.status)) return false;
3407
- if (!sub.expiresAt) return true;
3408
- return sub.expiresAt > /* @__PURE__ */ new Date();
3409
- } catch {
3410
- return false;
3411
- }
3412
- }
3413
3817
  var _PaywalloClientClass = class _PaywalloClientClass {
3414
3818
  constructor() {
3415
3819
  this.config = null;
3416
3820
  this.apiClient = null;
3417
3821
  this.initPromise = null;
3822
+ this.readyPromise = null;
3823
+ this.resolveReady = null;
3418
3824
  this.paywallPresenter = null;
3419
3825
  this.campaignPresenter = null;
3420
3826
  this.subscriptionGetter = null;
@@ -3424,29 +3830,30 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3424
3830
  this.pendingConfig = null;
3425
3831
  this.networkCleanup = null;
3426
3832
  this.initAttempts = 0;
3833
+ this.autoPreloadedPlacement = null;
3834
+ this.autoPreloadPlacementPromise = null;
3835
+ this.sessionFlagsMap = /* @__PURE__ */ new Map();
3427
3836
  }
3428
3837
  async init(config) {
3429
- if (config.debug) console.warn("[Paywallo INIT]", "init() called \u2014 appKey:", config.appKey ? config.appKey.substring(0, 8) + "..." : "MISSING");
3838
+ if (!this.readyPromise) {
3839
+ this.readyPromise = new Promise((resolve) => {
3840
+ this.resolveReady = resolve;
3841
+ });
3842
+ }
3430
3843
  if (this.config && this.apiClient) {
3431
- if (config.debug) console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3432
3844
  return;
3433
3845
  }
3434
3846
  if (this.initPromise) {
3435
- if (config.debug) console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3436
3847
  await this.initPromise;
3437
- if (config.debug) console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3438
3848
  return;
3439
3849
  }
3440
3850
  if (!config.appKey) throw new ClientError(CLIENT_ERROR_CODES.MISSING_APP_KEY, "appKey is required");
3441
- if (config.debug) console.warn("[Paywallo INIT]", "Starting fresh init...");
3442
3851
  this.pendingConfig = config;
3443
3852
  this.initAttempts = 0;
3444
3853
  this.initPromise = this.initWithRetry(config);
3445
3854
  try {
3446
3855
  await this.initPromise;
3447
- if (config.debug) console.warn("[Paywallo INIT]", "init() SUCCESS");
3448
3856
  } catch (error) {
3449
- if (config.debug) console.warn("[Paywallo INIT]", "init() FAILED:", error instanceof Error ? error.message : String(error));
3450
3857
  this.initPromise = null;
3451
3858
  this.config = null;
3452
3859
  this.apiClient = null;
@@ -3455,6 +3862,18 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3455
3862
  throw error;
3456
3863
  }
3457
3864
  }
3865
+ isReady() {
3866
+ return this.config !== null && this.apiClient !== null;
3867
+ }
3868
+ waitUntilReady() {
3869
+ if (this.isReady()) return Promise.resolve();
3870
+ if (!this.readyPromise) {
3871
+ this.readyPromise = new Promise((resolve) => {
3872
+ this.resolveReady = resolve;
3873
+ });
3874
+ }
3875
+ return this.readyPromise;
3876
+ }
3458
3877
  async initWithRetry(config) {
3459
3878
  while (this.initAttempts <= _PaywalloClientClass.MAX_INIT_RETRIES) {
3460
3879
  try {
@@ -3469,7 +3888,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3469
3888
  this.initAttempts++;
3470
3889
  if (this.initAttempts > _PaywalloClientClass.MAX_INIT_RETRIES) throw error;
3471
3890
  const delay = _PaywalloClientClass.RETRY_DELAYS[this.initAttempts - 1] ?? 1e4;
3472
- if (config.debug) console.warn("[Paywallo]", `Init attempt ${this.initAttempts} failed, retrying in ${delay}ms`, error);
3473
3891
  await new Promise((resolve) => setTimeout(resolve, delay));
3474
3892
  }
3475
3893
  }
@@ -3480,12 +3898,12 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3480
3898
  this.networkCleanup = networkMonitor.addListener((state) => {
3481
3899
  if (state !== "online") return;
3482
3900
  if (this.config && this.apiClient) return;
3483
- if (config.debug) console.warn("[Paywallo]", "Network recovered \u2014 retrying init");
3901
+ if (this.initPromise) return;
3484
3902
  this.initPromise = this.initWithRetry(config);
3485
- this.initPromise.then(() => {
3486
- if (config.debug) console.warn("[Paywallo]", "Init succeeded after network recovery");
3487
- }).catch(() => {
3903
+ this.initPromise.catch(() => {
3488
3904
  this.reportInitFailure(config, null, "NETWORK_RECOVERY_FAILED");
3905
+ }).finally(() => {
3906
+ if (!this.isReady()) this.initPromise = null;
3489
3907
  });
3490
3908
  });
3491
3909
  }
@@ -3497,12 +3915,13 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3497
3915
  }
3498
3916
  reportInitFailure(config, error, reason) {
3499
3917
  try {
3500
- const tempClient = new ApiClient(config.appKey, config.debug);
3918
+ const tempClient = new ApiClient(config.appKey, config.apiUrl ?? "https://paywallo.com.br", config.debug);
3501
3919
  void tempClient.reportError("INIT_FAILED", reason, {
3502
3920
  attempts: this.initAttempts,
3503
3921
  error: error instanceof Error ? error.message : String(error)
3504
3922
  });
3505
- } catch {
3923
+ } catch (err) {
3924
+ if (config.debug) console.warn("[Paywallo] reportInitFailure error", err);
3506
3925
  }
3507
3926
  }
3508
3927
  reportInitSuccess(config, attempts) {
@@ -3510,41 +3929,83 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3510
3929
  if (this.apiClient) {
3511
3930
  void this.apiClient.reportError("INIT_RECOVERED", `Succeeded after ${attempts} retries`, { attempts });
3512
3931
  }
3513
- } catch {
3932
+ } catch (err) {
3933
+ if (config.debug) console.warn("[Paywallo] reportInitSuccess error", err);
3934
+ }
3935
+ }
3936
+ async resolveSessionFlags(keys, apiClient, distinctId) {
3937
+ try {
3938
+ const results = await apiClient.evaluateFlags(keys, distinctId);
3939
+ this.sessionFlagsMap.clear();
3940
+ for (const key of keys) {
3941
+ this.sessionFlagsMap.set(key, key in results ? results[key] : null);
3942
+ }
3943
+ } catch (error) {
3944
+ for (const key of keys) {
3945
+ this.sessionFlagsMap.set(key, null);
3946
+ }
3514
3947
  }
3515
3948
  }
3516
3949
  async doInit(config) {
3517
3950
  const environment = config.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "Sandbox" : "Production");
3518
3951
  const offlineQueueEnabled = config.offlineQueueEnabled !== false;
3519
- if (config.debug) console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3520
3952
  await Promise.all([
3521
3953
  networkMonitor.initialize({ debug: config.debug }),
3522
3954
  offlineQueue.initialize({ debug: config.debug }).then(
3523
3955
  () => offlineQueue.clearItemsWithInvalidAppKey(config.appKey)
3524
3956
  )
3525
3957
  ]);
3526
- if (config.debug) console.warn("[Paywallo INIT]", "Step 1 DONE");
3527
- if (config.debug) console.warn("[Paywallo INIT]", "Step 2: ApiClient + queueProcessor + affiliate...");
3528
- this.apiClient = new ApiClient(config.appKey, config.debug, environment, offlineQueueEnabled, config.timeout);
3958
+ this.apiClient = new ApiClient(config.appKey, config.apiUrl ?? "https://paywallo.com.br", config.debug, environment, offlineQueueEnabled, config.timeout);
3529
3959
  queueProcessor.initialize(this.apiClient.getHttpClient(), { debug: config.debug });
3530
3960
  affiliateManager.initialize(this.apiClient, config.debug);
3531
- if (config.debug) console.warn("[Paywallo INIT]", "Step 2 DONE");
3532
- if (config.debug) console.warn("[Paywallo INIT]", "Step 3: identityManager + sessionManager...");
3961
+ subscriptionManager.init({
3962
+ serverUrl: this.apiClient.getBaseUrl(),
3963
+ appKey: config.appKey,
3964
+ cacheTTL: config.subscriptionCacheTTL,
3965
+ debug: config.debug
3966
+ });
3533
3967
  await Promise.all([
3534
3968
  identityManager.initialize(this.apiClient, config.debug),
3535
3969
  sessionManager.initialize(this.apiClient, config.sessionConfig, config.debug)
3536
3970
  ]);
3537
3971
  const distinctId = identityManager.getDistinctId();
3538
- if (config.debug) console.warn("[Paywallo INIT]", "Step 3 DONE \u2014 distinctId:", distinctId ? distinctId.substring(0, 8) + "..." : "EMPTY");
3539
3972
  if (config.autoStartSession !== false) {
3540
3973
  sessionManager.startSession().catch(() => {
3541
- if (config.debug) console.warn("[Paywallo]", "Session start failed during init \u2014 continuing");
3542
3974
  });
3543
3975
  }
3544
3976
  new InstallTracker(config.debug).trackIfNeeded(this.apiClient, config.requestATT === true).catch(() => {
3545
3977
  });
3978
+ if (config.sessionFlags && config.sessionFlags.length > 0) {
3979
+ await this.resolveSessionFlags(config.sessionFlags, this.apiClient, distinctId);
3980
+ }
3546
3981
  this.config = config;
3547
- if (config.debug) console.warn("[Paywallo INIT]", "COMPLETE \u2014 SDK ready, environment:", environment);
3982
+ if (this.resolveReady) {
3983
+ this.resolveReady();
3984
+ this.resolveReady = null;
3985
+ }
3986
+ if (config.debug) console.info("[Paywallo INIT] complete");
3987
+ if (this.apiClient) {
3988
+ const apiClientRef = this.apiClient;
3989
+ if (config.autoPreloadCampaign) {
3990
+ this.autoPreloadedPlacement = config.autoPreloadCampaign;
3991
+ this.autoPreloadPlacementPromise = Promise.resolve(config.autoPreloadCampaign);
3992
+ if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${config.autoPreloadCampaign}`);
3993
+ void campaignGateService.preloadCampaign(apiClientRef, config.autoPreloadCampaign).catch(() => {
3994
+ });
3995
+ } else {
3996
+ this.autoPreloadPlacementPromise = apiClientRef.getPrimaryCampaign(distinctId).then((primary) => {
3997
+ const placement = primary?.placement ?? primary?.paywall?.placement;
3998
+ if (placement) {
3999
+ this.autoPreloadedPlacement = placement;
4000
+ if (config.debug) console.info(`[Paywallo PRELOAD] started \u2014 placement: ${placement}`);
4001
+ void campaignGateService.preloadCampaign(apiClientRef, placement).catch(() => {
4002
+ });
4003
+ return placement;
4004
+ }
4005
+ return null;
4006
+ }).catch(() => null);
4007
+ }
4008
+ }
3548
4009
  }
3549
4010
  async identify(options) {
3550
4011
  this.ensureInitialized();
@@ -3554,7 +4015,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3554
4015
  const apiClient = this.getApiClientOrThrow();
3555
4016
  const distinctId = identityManager.getDistinctId();
3556
4017
  if (!distinctId) {
3557
- if (this.config?.debug) console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3558
4018
  return;
3559
4019
  }
3560
4020
  if (!isValidEventName(eventName)) {
@@ -3562,25 +4022,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3562
4022
  throw new ClientError(CLIENT_ERROR_CODES.INVALID_EVENT_NAME, msg);
3563
4023
  }
3564
4024
  const sessionId = sessionManager.getSessionId();
3565
- if (eventName === "$purchase_completed") {
3566
- if (this.config?.debug) console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3567
- distinctId: distinctId.substring(0, 8) + "...",
3568
- productId: options?.properties?.productId,
3569
- priceUsd: options?.properties?.priceUsd,
3570
- price: options?.properties?.price,
3571
- currency: options?.properties?.currency,
3572
- placement: options?.properties?.placement
3573
- }));
3574
- } else if (eventName === "$paywall_viewed" || eventName === "$paywall_closed") {
3575
- if (this.config?.debug) console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3576
- distinctId: distinctId.substring(0, 8) + "...",
3577
- placement: options?.properties?.placement,
3578
- variantKey: options?.properties?.variantKey,
3579
- timeOnPaywall: options?.properties?.timeOnPaywall
3580
- }));
3581
- } else {
3582
- if (this.config?.debug) console.warn("[Paywallo TRACK]", eventName, "distinctId:", distinctId.substring(0, 8) + "...");
3583
- }
3584
4025
  await apiClient.trackEvent(eventName, distinctId, {
3585
4026
  ...identityManager.getProperties(),
3586
4027
  ...options?.properties,
@@ -3591,7 +4032,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3591
4032
  const apiClient = this.getApiClientOrThrow();
3592
4033
  const distinctId = identityManager.getDistinctId();
3593
4034
  if (!distinctId) {
3594
- if (this.config?.debug) console.warn("[Paywallo] Skipping event \u2014 no distinctId available");
3595
4035
  return;
3596
4036
  }
3597
4037
  if (!Number.isInteger(index) || index < 0) {
@@ -3615,7 +4055,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3615
4055
  const apiClient = this.getApiClientOrThrow();
3616
4056
  const distinctId = identityManager.getDistinctId();
3617
4057
  if (!distinctId) {
3618
- if (this.config?.debug) console.warn("[Paywallo] Skipping event \u2014 no distinctId available");
3619
4058
  return;
3620
4059
  }
3621
4060
  if (!actionName || typeof actionName !== "string") {
@@ -3644,14 +4083,10 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3644
4083
  try {
3645
4084
  const distinctId = identityManager.getDistinctId();
3646
4085
  if (!distinctId) {
3647
- if (this.config?.debug) console.warn("[Paywallo AB]", "getVariant SKIPPED \u2014 no distinctId", flagKey);
3648
4086
  return { variant: null };
3649
4087
  }
3650
- const result = await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3651
- if (this.config?.debug) console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3652
- return result;
4088
+ return await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3653
4089
  } catch (error) {
3654
- if (this.config?.debug) console.warn("[Paywallo]", "getVariant failed \u2014 returning null", error);
3655
4090
  if (this.apiClient) void this.apiClient.reportError("GET_VARIANT_FAILED", error instanceof Error ? error.message : String(error));
3656
4091
  return { variant: null };
3657
4092
  }
@@ -3667,7 +4102,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3667
4102
  const result = await this.getApiClientOrThrow().getConditionalFlag(flagKey, { distinctId, ...context });
3668
4103
  return result.value;
3669
4104
  } catch (error) {
3670
- if (this.config?.debug) console.warn("[Paywallo]", "getConditionalFlag failed \u2014 returning false", error);
3671
4105
  if (this.apiClient) void this.apiClient.reportError("GET_CONDITIONAL_FLAG_FAILED", error instanceof Error ? error.message : String(error));
3672
4106
  return false;
3673
4107
  }
@@ -3690,15 +4124,18 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3690
4124
  async hasActiveSubscription() {
3691
4125
  this.ensureInitialized();
3692
4126
  try {
3693
- if (this.activeChecker) return await this.activeChecker();
4127
+ if (this.activeChecker) {
4128
+ return await this.activeChecker();
4129
+ }
3694
4130
  const distinctId = identityManager.getDistinctId();
3695
- if (!distinctId || !this.apiClient) return false;
4131
+ if (!distinctId || !this.apiClient) {
4132
+ return false;
4133
+ }
3696
4134
  const status = await this.apiClient.getSubscriptionStatus(distinctId);
3697
4135
  return status.hasActiveSubscription;
3698
- } catch {
3699
- if (this.config?.debug) console.warn("[Paywallo]", "hasActiveSubscription failed \u2014 using persistent cache");
3700
- if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", "hasActiveSubscription failed");
3701
- return resolveClientOfflineFromCache();
4136
+ } catch (error) {
4137
+ if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", error instanceof Error ? error.message : "hasActiveSubscription failed");
4138
+ return false;
3702
4139
  }
3703
4140
  }
3704
4141
  async getSubscription() {
@@ -3711,7 +4148,6 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3711
4148
  if (!status.subscription) return null;
3712
4149
  return { ...status.subscription, expiresAt: status.subscription.expiresAt ? new Date(status.subscription.expiresAt) : null };
3713
4150
  } catch {
3714
- if (this.config?.debug) console.warn("[Paywallo]", "getSubscription failed \u2014 returning null");
3715
4151
  return null;
3716
4152
  }
3717
4153
  }
@@ -3722,20 +4158,24 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3722
4158
  }
3723
4159
  async reset() {
3724
4160
  await identityManager.reset();
4161
+ await subscriptionCache.invalidateAll();
3725
4162
  }
3726
4163
  async fullReset() {
3727
4164
  const wasDebug = this.config?.debug ?? false;
3728
4165
  await sessionManager.endSession();
3729
4166
  await identityManager.reset();
3730
- await subscriptionCache.invalidate();
4167
+ await subscriptionCache.invalidateAll();
3731
4168
  queueProcessor.dispose();
3732
4169
  offlineQueue.dispose();
3733
4170
  networkMonitor.dispose();
3734
4171
  campaignGateService.clearPreloaded();
3735
4172
  this.cleanupNetworkRecovery();
4173
+ this.sessionFlagsMap.clear();
3736
4174
  this.apiClient = null;
3737
4175
  this.config = null;
3738
4176
  this.initPromise = null;
4177
+ this.readyPromise = null;
4178
+ this.resolveReady = null;
3739
4179
  this.pendingConfig = null;
3740
4180
  this.initAttempts = 0;
3741
4181
  this.paywallPresenter = null;
@@ -3744,7 +4184,13 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3744
4184
  this.activeChecker = null;
3745
4185
  this.restoreHandler = null;
3746
4186
  this.lastOnboardingStepTimestamp = null;
3747
- if (wasDebug) console.warn("[Paywallo]", "Full reset complete");
4187
+ this.autoPreloadedPlacement = null;
4188
+ this.autoPreloadPlacementPromise = null;
4189
+ }
4190
+ getSessionFlag(key) {
4191
+ if (!this.config || !this.apiClient) return null;
4192
+ const value = this.sessionFlagsMap.get(key);
4193
+ return value !== void 0 ? value : null;
3748
4194
  }
3749
4195
  getConfig() {
3750
4196
  return this.config;
@@ -3833,6 +4279,15 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3833
4279
  getPreloadedCampaign(placement) {
3834
4280
  return campaignGateService.getPreloadedCampaign(placement);
3835
4281
  }
4282
+ isPreloaded(placement) {
4283
+ return campaignGateService.getPreloadedCampaign(placement) !== null;
4284
+ }
4285
+ getAutoPreloadedPlacement() {
4286
+ return this.autoPreloadedPlacement;
4287
+ }
4288
+ waitForAutoPreloadedPlacement() {
4289
+ return this.autoPreloadPlacementPromise ?? Promise.resolve(null);
4290
+ }
3836
4291
  isOnline() {
3837
4292
  return networkMonitor.isOnline();
3838
4293
  }
@@ -3862,48 +4317,6 @@ var PaywalloClient = new PaywalloClientClass();
3862
4317
  import { createContext as createContext2 } from "react";
3863
4318
  var PaywalloContext = createContext2(null);
3864
4319
 
3865
- // src/utils/localization.ts
3866
- import { NativeModules as NativeModules3, Platform as Platform7 } from "react-native";
3867
- var currentLanguage = "pt-BR";
3868
- var defaultLanguage = "pt-BR";
3869
- function setCurrentLanguage(language) {
3870
- currentLanguage = language;
3871
- }
3872
- function setDefaultLanguage(language) {
3873
- defaultLanguage = language;
3874
- }
3875
- function getCurrentLanguage() {
3876
- return currentLanguage;
3877
- }
3878
- function getDefaultLanguage() {
3879
- return defaultLanguage;
3880
- }
3881
- function getLocalizedString(text) {
3882
- if (!text) return "";
3883
- if (typeof text === "string") return text;
3884
- return text[currentLanguage] ?? text[defaultLanguage] ?? Object.values(text)[0] ?? "";
3885
- }
3886
- function detectDeviceLanguage() {
3887
- try {
3888
- if (Platform7.OS === "ios") {
3889
- const settings = NativeModules3.SettingsManager?.settings;
3890
- return settings?.AppleLocale ?? settings?.AppleLanguages?.[0] ?? "en-US";
3891
- }
3892
- return NativeModules3.I18nManager?.localeIdentifier ?? "en-US";
3893
- } catch {
3894
- return "en-US";
3895
- }
3896
- }
3897
- function initLocalization(options) {
3898
- if (options?.defaultLanguage) {
3899
- defaultLanguage = options.defaultLanguage;
3900
- }
3901
- if (options?.detectDevice !== false) {
3902
- const deviceLanguage = detectDeviceLanguage();
3903
- currentLanguage = deviceLanguage;
3904
- }
3905
- }
3906
-
3907
4320
  // src/hooks/usePaywallo.ts
3908
4321
  import { useContext as useContext2 } from "react";
3909
4322
  function usePaywallo() {
@@ -3929,7 +4342,8 @@ function mapToIAPProduct(product) {
3929
4342
  type: product.type === "subscription" ? "subs" : "inapp",
3930
4343
  subscriptionPeriodAndroid: product.subscriptionPeriod,
3931
4344
  introductoryPrice: product.introductoryPrice,
3932
- freeTrialPeriodAndroid: product.freeTrialPeriod
4345
+ freeTrialPeriodAndroid: product.freeTrialPeriod,
4346
+ trialDays: product.trialDays ?? 0
3933
4347
  };
3934
4348
  }
3935
4349
  function mapToFormattedProduct(product) {
@@ -3944,6 +4358,7 @@ function mapToFormattedProduct(product) {
3944
4358
  subscriptionPeriod: product.subscriptionPeriod,
3945
4359
  introductoryPrice: product.introductoryPrice,
3946
4360
  freeTrialPeriod: product.freeTrialPeriod,
4361
+ trialDays: product.trialDays ?? 0,
3947
4362
  pricePerMonth: product.pricePerMonth,
3948
4363
  pricePerWeek: product.pricePerWeek,
3949
4364
  pricePerDay: product.pricePerDay
@@ -4029,23 +4444,31 @@ function usePurchase() {
4029
4444
  if (result.success && result.purchase) {
4030
4445
  setState("idle");
4031
4446
  return {
4032
- productId: result.purchase.productId,
4033
- transactionId: result.purchase.transactionId,
4034
- transactionDate: result.purchase.transactionDate,
4035
- transactionReceipt: result.purchase.receipt
4447
+ status: "success",
4448
+ purchase: {
4449
+ productId: result.purchase.productId,
4450
+ transactionId: result.purchase.transactionId,
4451
+ transactionDate: result.purchase.transactionDate,
4452
+ transactionReceipt: result.purchase.receipt
4453
+ }
4036
4454
  };
4037
4455
  }
4038
4456
  if (result.error) {
4039
- const purchaseError = createPurchaseError("PURCHASE_FAILED", result.error.message);
4040
- setError(purchaseError);
4457
+ setError(result.error);
4041
4458
  setState("error");
4042
- throw result.error;
4459
+ return { status: "failed", error: result.error };
4043
4460
  }
4044
4461
  setState("idle");
4045
- throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, "Purchase cancelled by user", true);
4462
+ return { status: "cancelled" };
4046
4463
  } catch (err) {
4464
+ const purchaseError = err instanceof PurchaseError ? err : createPurchaseError("PURCHASE_FAILED", err instanceof Error ? err.message : String(err));
4465
+ if (purchaseError.userCancelled) {
4466
+ setState("idle");
4467
+ return { status: "cancelled" };
4468
+ }
4469
+ setError(purchaseError);
4047
4470
  setState("error");
4048
- throw err;
4471
+ return { status: "failed", error: purchaseError };
4049
4472
  }
4050
4473
  }, []);
4051
4474
  const restore = useCallback7(async () => {
@@ -4073,7 +4496,6 @@ function usePurchase() {
4073
4496
  purchase,
4074
4497
  restore,
4075
4498
  error,
4076
- isLoading: state === "loading",
4077
4499
  isPurchasing: state === "purchasing",
4078
4500
  isRestoring: state === "restoring"
4079
4501
  };
@@ -4081,139 +4503,6 @@ function usePurchase() {
4081
4503
 
4082
4504
  // src/hooks/useSubscription.ts
4083
4505
  import { useCallback as useCallback8, useEffect as useEffect6, useState as useState8 } from "react";
4084
-
4085
- // src/domains/subscription/SubscriptionManager.ts
4086
- import { Platform as Platform8 } from "react-native";
4087
- var SubscriptionManagerClass = class {
4088
- constructor() {
4089
- this.config = null;
4090
- this.listeners = /* @__PURE__ */ new Set();
4091
- this.userId = null;
4092
- this.cache = new SubscriptionCache();
4093
- }
4094
- init(config) {
4095
- this.config = config;
4096
- if (config.cacheTTL) {
4097
- this.cache.setTTL(config.cacheTTL);
4098
- }
4099
- }
4100
- setUserId(userId) {
4101
- if (this.userId !== userId) {
4102
- this.userId = userId;
4103
- void this.cache.invalidate();
4104
- }
4105
- }
4106
- async hasActiveSubscription(forceRefresh = false) {
4107
- const status = await this.getSubscriptionStatus(forceRefresh);
4108
- return status.hasActiveSubscription;
4109
- }
4110
- async getSubscription(forceRefresh = false) {
4111
- const status = await this.getSubscriptionStatus(forceRefresh);
4112
- return status.subscription;
4113
- }
4114
- async getEntitlements(forceRefresh = false) {
4115
- const status = await this.getSubscriptionStatus(forceRefresh);
4116
- return status.entitlements;
4117
- }
4118
- async getSubscriptionStatus(forceRefresh = false) {
4119
- if (!this.config) {
4120
- return this.getEmptyStatus();
4121
- }
4122
- if (!forceRefresh) {
4123
- const cached = await this.cache.get();
4124
- if (cached && !cached.isStale) {
4125
- return cached.data;
4126
- }
4127
- }
4128
- try {
4129
- const status = await this.fetchSubscriptionStatus();
4130
- await this.cache.set(status);
4131
- this.notifyListeners(status);
4132
- return status;
4133
- } catch (error) {
4134
- this.log("Failed to fetch subscription status", error);
4135
- const cached = await this.cache.get();
4136
- if (cached) {
4137
- return cached.data;
4138
- }
4139
- return this.getEmptyStatus();
4140
- }
4141
- }
4142
- async restorePurchases() {
4143
- if (!this.config) {
4144
- throw new SessionError(
4145
- SESSION_ERROR_CODES.NOT_INITIALIZED,
4146
- "SubscriptionManager not initialized"
4147
- );
4148
- }
4149
- await this.cache.invalidate();
4150
- return this.getSubscriptionStatus(true);
4151
- }
4152
- async fetchSubscriptionStatus() {
4153
- if (!this.config) {
4154
- return this.getEmptyStatus();
4155
- }
4156
- const url = new URL(`${this.config.serverUrl}/subscriptions/status`);
4157
- if (this.userId) {
4158
- url.searchParams.set("userId", this.userId);
4159
- }
4160
- url.searchParams.set("platform", Platform8.OS);
4161
- const response = await fetch(url.toString(), {
4162
- method: "GET",
4163
- headers: {
4164
- "Content-Type": "application/json",
4165
- "X-App-Key": this.config.appKey
4166
- }
4167
- });
4168
- if (!response.ok) {
4169
- throw new SessionError(
4170
- SESSION_ERROR_CODES.RESTORE_FAILED,
4171
- `Failed to fetch subscription status: ${response.status}`
4172
- );
4173
- }
4174
- const data = await response.json();
4175
- if (data.subscription) {
4176
- data.subscription.expiresAt = new Date(data.subscription.expiresAt);
4177
- data.subscription.originalPurchaseDate = new Date(data.subscription.originalPurchaseDate);
4178
- data.subscription.latestPurchaseDate = new Date(data.subscription.latestPurchaseDate);
4179
- if (data.subscription.cancellationDate) {
4180
- data.subscription.cancellationDate = new Date(data.subscription.cancellationDate);
4181
- }
4182
- if (data.subscription.gracePeriodExpiresAt) {
4183
- data.subscription.gracePeriodExpiresAt = new Date(data.subscription.gracePeriodExpiresAt);
4184
- }
4185
- }
4186
- return data;
4187
- }
4188
- getEmptyStatus() {
4189
- return {
4190
- hasActiveSubscription: false,
4191
- subscription: null,
4192
- entitlements: []
4193
- };
4194
- }
4195
- addListener(listener) {
4196
- this.listeners.add(listener);
4197
- return () => this.listeners.delete(listener);
4198
- }
4199
- notifyListeners(status) {
4200
- for (const listener of this.listeners) {
4201
- listener(status);
4202
- }
4203
- }
4204
- async onPurchaseComplete() {
4205
- await this.cache.invalidate();
4206
- await this.getSubscriptionStatus(true);
4207
- }
4208
- log(message, data) {
4209
- if (this.config?.debug) {
4210
- console.warn(`[SubscriptionManager] ${message}`, data ?? "");
4211
- }
4212
- }
4213
- };
4214
- var subscriptionManager = new SubscriptionManagerClass();
4215
-
4216
- // src/hooks/useSubscription.ts
4217
4506
  function useSubscription() {
4218
4507
  const [status, setStatus] = useState8(null);
4219
4508
  const [isLoading, setIsLoading] = useState8(true);
@@ -4248,6 +4537,9 @@ function useSubscription() {
4248
4537
  void subscriptionManager.getSubscriptionStatus().then((s) => {
4249
4538
  setStatus(s);
4250
4539
  setIsLoading(false);
4540
+ }).catch((err) => {
4541
+ setError(err instanceof Error ? err : new Error(String(err)));
4542
+ setIsLoading(false);
4251
4543
  });
4252
4544
  const removeListener = subscriptionManager.addListener((newStatus) => {
4253
4545
  setStatus(newStatus);
@@ -4268,7 +4560,7 @@ function useSubscription() {
4268
4560
  }
4269
4561
 
4270
4562
  // src/PaywalloProvider.tsx
4271
- import { useCallback as useCallback13, useEffect as useEffect7, useMemo as useMemo2, useRef as useRef8, useState as useState12 } from "react";
4563
+ import { useCallback as useCallback13, useEffect as useEffect7, useLayoutEffect, useMemo as useMemo2, useRef as useRef8, useState as useState12 } from "react";
4272
4564
  import { Animated as Animated3, Dimensions as Dimensions4, Easing as Easing2, StyleSheet as StyleSheet3 } from "react-native";
4273
4565
 
4274
4566
  // src/hooks/useCampaignPreload.ts
@@ -4330,10 +4622,9 @@ var CACHE_TTL_MS = 5 * 60 * 1e3;
4330
4622
  function isCacheEntryValid(entry) {
4331
4623
  return !!entry && Date.now() - entry.preloadedAt < CACHE_TTL_MS;
4332
4624
  }
4333
- function consumeCacheEntry(cache, placement) {
4625
+ function getCacheEntry(cache, placement) {
4334
4626
  const entry = cache.get(placement);
4335
4627
  if (!isCacheEntryValid(entry)) return null;
4336
- cache.delete(placement);
4337
4628
  return entry ?? null;
4338
4629
  }
4339
4630
 
@@ -4346,7 +4637,7 @@ function useCampaignPreload() {
4346
4637
  []
4347
4638
  );
4348
4639
  const consumePreloadedCampaign = useCallback9(
4349
- (placement) => consumeCacheEntry(preloadedCampaignsRef.current, placement),
4640
+ (placement) => getCacheEntry(preloadedCampaignsRef.current, placement),
4350
4641
  []
4351
4642
  );
4352
4643
  const preloadCampaign = useCallback9(
@@ -4394,6 +4685,7 @@ function useCampaignPreload() {
4394
4685
  campaignId: campaign.campaignId,
4395
4686
  variantKey: campaign.variantKey
4396
4687
  });
4688
+ if (PaywalloClient.getConfig()?.debug) console.info(`[Paywallo WEBVIEW] mounting hidden \u2014 placement: ${placement}`);
4397
4689
  return { success: true };
4398
4690
  } catch (error) {
4399
4691
  return {
@@ -4427,8 +4719,8 @@ function useProductLoader() {
4427
4719
  const map = /* @__PURE__ */ new Map();
4428
4720
  for (const p of loaded) map.set(p.productId, p);
4429
4721
  setProducts(map);
4430
- } catch (error) {
4431
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Product load failed:", error);
4722
+ } catch (err) {
4723
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] refreshProducts failed", err);
4432
4724
  } finally {
4433
4725
  setIsLoadingProducts(false);
4434
4726
  }
@@ -4438,7 +4730,7 @@ function useProductLoader() {
4438
4730
 
4439
4731
  // src/hooks/usePurchaseOrchestrator.ts
4440
4732
  import { useCallback as useCallback11 } from "react";
4441
- function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef) {
4733
+ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, invalidateSubscriptionCache) {
4442
4734
  const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
4443
4735
  const handlePreloadedPurchase = useCallback11(
4444
4736
  async (productId) => {
@@ -4463,17 +4755,18 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4463
4755
  await trackDismissed({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, durationSeconds, action: "purchase", variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4464
4756
  await trackPurchased({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, productId, variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4465
4757
  }
4758
+ invalidateSubscriptionCache();
4466
4759
  setShowingPreloadedWebView(false);
4467
4760
  clearPreloadedWebView();
4468
4761
  resolvePreloadedPaywall({ presented: true, purchased: true, cancelled: false, restored: false });
4469
4762
  } catch (error) {
4470
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Preloaded purchase error:", error);
4763
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] preloaded purchase failed", error);
4471
4764
  setShowingPreloadedWebView(false);
4472
4765
  clearPreloadedWebView();
4473
4766
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
4474
4767
  }
4475
4768
  },
4476
- [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased]
4769
+ [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased, invalidateSubscriptionCache]
4477
4770
  );
4478
4771
  const handlePreloadedRestore = useCallback11(async () => {
4479
4772
  try {
@@ -4483,16 +4776,17 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4483
4776
  const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
4484
4777
  await trackDismissed({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, durationSeconds, action: "restore", variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
4485
4778
  }
4779
+ invalidateSubscriptionCache();
4486
4780
  setShowingPreloadedWebView(false);
4487
4781
  clearPreloadedWebView();
4488
4782
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: true });
4489
4783
  } catch (error) {
4490
- if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Preloaded restore error:", error);
4784
+ if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] preloaded restore failed", error);
4491
4785
  setShowingPreloadedWebView(false);
4492
4786
  clearPreloadedWebView();
4493
4787
  resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
4494
4788
  }
4495
- }, [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed]);
4789
+ }, [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, invalidateSubscriptionCache]);
4496
4790
  return { handlePreloadedPurchase, handlePreloadedRestore };
4497
4791
  }
4498
4792
 
@@ -4504,7 +4798,6 @@ function toDomainSubscriptionInfo(sub) {
4504
4798
  return {
4505
4799
  id: "",
4506
4800
  productId: sub.productId,
4507
- // The hook's SubscriptionStatus is a subset of the domain's — cast is safe
4508
4801
  status: sub.status,
4509
4802
  expiresAt: sub.expiresAt ?? /* @__PURE__ */ new Date(0),
4510
4803
  originalPurchaseDate: /* @__PURE__ */ new Date(0),
@@ -4522,31 +4815,32 @@ function toDomainResponse(status) {
4522
4815
  entitlements: []
4523
4816
  };
4524
4817
  }
4525
- async function resolveOfflineFromCache() {
4818
+ function isSubscriptionStillActive(sub) {
4819
+ if (!sub) return false;
4820
+ if (!ACTIVE_STATUSES.includes(sub.status)) return false;
4821
+ if (sub.expiresAt && sub.expiresAt.getTime() !== 0 && sub.expiresAt < /* @__PURE__ */ new Date()) return false;
4822
+ return true;
4823
+ }
4824
+ async function resolveOfflineFromCache(distinctId) {
4526
4825
  try {
4527
- const cached = await subscriptionCache.get();
4826
+ const cached = await subscriptionCache.get(distinctId);
4528
4827
  if (!cached) return false;
4529
4828
  const sub = cached.data.subscription;
4530
- if (!sub) return false;
4531
- if (!ACTIVE_STATUSES.includes(sub.status)) return false;
4532
- const hasValidExpiry = sub.expiresAt.getTime() === 0 || sub.expiresAt > /* @__PURE__ */ new Date();
4533
- return hasValidExpiry;
4829
+ return isSubscriptionStillActive(sub ? { status: sub.status, expiresAt: sub.expiresAt ?? null } : null);
4534
4830
  } catch {
4535
4831
  return false;
4536
4832
  }
4537
4833
  }
4538
4834
  function isActiveCacheEntry(entry) {
4539
- if (!entry.data) return false;
4540
- if (!ACTIVE_STATUSES.includes(entry.data.status)) return false;
4541
- if (entry.data.expiresAt && entry.data.expiresAt < /* @__PURE__ */ new Date()) return false;
4542
- return true;
4835
+ return isSubscriptionStillActive(entry.data);
4543
4836
  }
4544
4837
  function useSubscriptionSync() {
4545
4838
  const [subscription, setSubscription] = useState11(null);
4546
4839
  const cacheRef = useRef7(null);
4547
4840
  const invalidateCache = useCallback12(() => {
4548
4841
  cacheRef.current = null;
4549
- void subscriptionCache.invalidate();
4842
+ const distinctId = PaywalloClient.getDistinctId();
4843
+ if (distinctId) void subscriptionCache.invalidate(distinctId);
4550
4844
  }, []);
4551
4845
  const fetchAndCache = useCallback12(async () => {
4552
4846
  const apiClient = PaywalloClient.getApiClient();
@@ -4559,7 +4853,7 @@ function useSubscriptionSync() {
4559
4853
  } : null;
4560
4854
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4561
4855
  setSubscription(sub);
4562
- void subscriptionCache.set(toDomainResponse(status));
4856
+ void subscriptionCache.set(distinctId, toDomainResponse(status));
4563
4857
  return sub;
4564
4858
  }, []);
4565
4859
  const hasActiveSubscription = useCallback12(async () => {
@@ -4571,23 +4865,23 @@ function useSubscriptionSync() {
4571
4865
  return isActiveCacheEntry(cached);
4572
4866
  }
4573
4867
  try {
4574
- const persisted = await subscriptionCache.get();
4575
- if (persisted) {
4576
- if (!persisted.isStale) {
4577
- const sub = persisted.data.subscription ? {
4578
- productId: persisted.data.subscription.productId,
4579
- status: persisted.data.subscription.status,
4580
- expiresAt: persisted.data.subscription.expiresAt ?? null,
4581
- platform: persisted.data.subscription.platform,
4582
- autoRenewEnabled: persisted.data.subscription.willRenew,
4583
- inGracePeriod: persisted.data.subscription.status === "grace_period"
4584
- } : null;
4868
+ const persisted = await subscriptionCache.get(distinctId);
4869
+ if (persisted && !persisted.isStale) {
4870
+ const persistedSub = persisted.data.subscription;
4871
+ const sub = persistedSub ? {
4872
+ productId: persistedSub.productId,
4873
+ status: persistedSub.status,
4874
+ expiresAt: persistedSub.expiresAt ?? null,
4875
+ platform: persistedSub.platform,
4876
+ autoRenewEnabled: persistedSub.willRenew,
4877
+ inGracePeriod: persistedSub.status === "grace_period"
4878
+ } : null;
4879
+ const stillActive = isSubscriptionStillActive(sub);
4880
+ if (stillActive) {
4585
4881
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4586
4882
  setSubscription(sub);
4587
- return persisted.data.hasActiveSubscription;
4883
+ return true;
4588
4884
  }
4589
- void fetchAndCache();
4590
- return persisted.data.hasActiveSubscription;
4591
4885
  }
4592
4886
  } catch {
4593
4887
  }
@@ -4599,12 +4893,12 @@ function useSubscriptionSync() {
4599
4893
  } : null;
4600
4894
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4601
4895
  setSubscription(sub);
4602
- void subscriptionCache.set(toDomainResponse(status));
4896
+ void subscriptionCache.set(distinctId, toDomainResponse(status));
4603
4897
  return status.hasActiveSubscription;
4604
4898
  } catch {
4605
- return await resolveOfflineFromCache();
4899
+ return await resolveOfflineFromCache(distinctId);
4606
4900
  }
4607
- }, [fetchAndCache]);
4901
+ }, []);
4608
4902
  const getSubscription = useCallback12(async () => {
4609
4903
  const apiClient = PaywalloClient.getApiClient();
4610
4904
  const distinctId = PaywalloClient.getDistinctId();
@@ -4615,7 +4909,7 @@ function useSubscriptionSync() {
4615
4909
  return await fetchAndCache();
4616
4910
  } catch {
4617
4911
  try {
4618
- const persisted = await subscriptionCache.get();
4912
+ const persisted = await subscriptionCache.get(distinctId);
4619
4913
  if (!persisted?.data.subscription) return null;
4620
4914
  const s = persisted.data.subscription;
4621
4915
  const normalisedStatus = s.status === "grace_period" ? "in_grace_period" : s.status;
@@ -4646,13 +4940,38 @@ function PaywalloProvider({ children, config }) {
4646
4940
  const { subscription, hasActiveSubscription, getSubscription, invalidateCache } = useSubscriptionSync();
4647
4941
  const { preloadedWebView, setPreloadedWebView, preloadCampaign, consumePreloadedCampaign, isPreloadValid } = useCampaignPreload();
4648
4942
  const clearPreloadedWebView = useCallback13(() => setPreloadedWebView(null), [setPreloadedWebView]);
4649
- const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView);
4943
+ const autoPreloadTriggeredRef = useRef8(false);
4944
+ const preloadedWebViewRef = useRef8(preloadedWebView);
4945
+ useEffect7(() => {
4946
+ preloadedWebViewRef.current = preloadedWebView;
4947
+ }, [preloadedWebView]);
4948
+ const triggerRepreload = useCallback13((placement) => {
4949
+ void PaywalloClient.preloadCampaign(placement).catch(() => {
4950
+ });
4951
+ }, []);
4952
+ const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady: baseHandlePreloadedWebViewReady, handlePreloadedClose: baseHandlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, setPreloadedWebView);
4953
+ const SCREEN_HEIGHT3 = useMemo2(() => Dimensions4.get("window").height, []);
4954
+ const slideAnim = useRef8(new Animated3.Value(SCREEN_HEIGHT3)).current;
4955
+ const handlePreloadedWebViewReady = useCallback13(() => {
4956
+ if (PaywalloClient.getConfig()?.debug) {
4957
+ console.info(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
4958
+ }
4959
+ baseHandlePreloadedWebViewReady();
4960
+ }, [config?.debug, preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
4961
+ const handlePreloadedClose = useCallback13(() => {
4962
+ const placement = preloadedWebView?.placement;
4963
+ Animated3.timing(slideAnim, { toValue: SCREEN_HEIGHT3, duration: 200, easing: Easing2.in(Easing2.cubic), useNativeDriver: true }).start(() => {
4964
+ baseHandlePreloadedClose();
4965
+ if (placement) triggerRepreload(placement);
4966
+ });
4967
+ }, [baseHandlePreloadedClose, preloadedWebView, triggerRepreload, slideAnim, SCREEN_HEIGHT3]);
4650
4968
  const { handlePreloadedPurchase: rawPreloadedPurchase, handlePreloadedRestore } = usePurchaseOrchestrator(
4651
4969
  preloadedWebView,
4652
4970
  resolvePreloadedPaywall,
4653
4971
  clearPreloadedWebView,
4654
4972
  setShowingPreloadedWebView,
4655
- presentedAtRef
4973
+ presentedAtRef,
4974
+ invalidateCache
4656
4975
  );
4657
4976
  const [isPreloadPurchasing, setIsPreloadPurchasing] = useState12(false);
4658
4977
  const handlePreloadedPurchase = useCallback13(async (productId) => {
@@ -4665,13 +4984,19 @@ function PaywalloProvider({ children, config }) {
4665
4984
  }, [rawPreloadedPurchase]);
4666
4985
  useEffect7(() => {
4667
4986
  initLocalization({ detectDevice: true });
4668
- PaywalloClient.init(config).then(() => {
4987
+ PaywalloClient.init(config).then(async () => {
4669
4988
  setDistinctId(PaywalloClient.getDistinctId());
4670
4989
  setIsInitialized(true);
4990
+ if (!autoPreloadTriggeredRef.current) {
4991
+ autoPreloadTriggeredRef.current = true;
4992
+ const placement = await PaywalloClient.waitForAutoPreloadedPlacement();
4993
+ if (placement) void preloadCampaign(placement).catch(() => {
4994
+ });
4995
+ }
4671
4996
  }).catch((err) => {
4672
4997
  setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
4673
4998
  });
4674
- }, [config]);
4999
+ }, [config, preloadCampaign]);
4675
5000
  const getPaywallConfig = useCallback13(async (p) => {
4676
5001
  const api = PaywalloClient.getApiClient();
4677
5002
  return api ? api.getPaywall(p) : null;
@@ -4770,28 +5095,54 @@ function PaywalloProvider({ children, config }) {
4770
5095
  setActivePaywall(null);
4771
5096
  }
4772
5097
  }, [activePaywall]);
5098
+ const bridgePaywallPresenter = useCallback13((placement) => {
5099
+ if (PaywalloClient.getConfig()?.debug) {
5100
+ console.info(`[Paywallo PRESENT] called \u2014 preloaded: ${preloadedWebViewRef.current?.loaded === true} placement: ${placement}`);
5101
+ }
5102
+ if (preloadedWebViewRef.current?.placement === placement) {
5103
+ if (preloadedWebViewRef.current.loaded) {
5104
+ return presentPreloadedWebView();
5105
+ }
5106
+ return new Promise((resolve) => {
5107
+ let elapsed = 0;
5108
+ const poll = () => {
5109
+ if (preloadedWebViewRef.current?.loaded) {
5110
+ resolve(presentPreloadedWebView());
5111
+ return;
5112
+ }
5113
+ elapsed += 100;
5114
+ if (elapsed >= 500) {
5115
+ resolve(presentCampaign(placement));
5116
+ return;
5117
+ }
5118
+ setTimeout(poll, 100);
5119
+ };
5120
+ setTimeout(poll, 100);
5121
+ });
5122
+ }
5123
+ return presentCampaign(placement);
5124
+ }, [presentPreloadedWebView, presentCampaign]);
4773
5125
  useEffect7(() => {
4774
5126
  if (!isInitialized) return;
4775
5127
  const onEmergency = async (id) => {
4776
5128
  try {
4777
- await presentPaywall(id);
5129
+ await bridgePaywallPresenter(id);
4778
5130
  } catch {
4779
5131
  }
4780
5132
  };
4781
- PaywalloClient.registerPaywallPresenter(presentPaywall);
5133
+ PaywalloClient.registerPaywallPresenter(bridgePaywallPresenter);
4782
5134
  PaywalloClient.registerSubscriptionGetter(getSubscription);
4783
5135
  PaywalloClient.registerActiveChecker(hasActiveSubscription);
4784
5136
  PaywalloClient.registerRestoreHandler(restorePurchases);
4785
5137
  PaywalloClient.registerEmergencyPaywallHandler(onEmergency);
4786
- }, [isInitialized, presentPaywall, getSubscription, hasActiveSubscription, restorePurchases]);
4787
- useEffect7(() => {
4788
- if (activePaywall) setPreloadedWebView(null);
4789
- }, [activePaywall, setPreloadedWebView]);
5138
+ }, [isInitialized, bridgePaywallPresenter, getSubscription, hasActiveSubscription, restorePurchases]);
5139
+ const isReady = isInitialized && !isLoadingProducts;
4790
5140
  const noOp = useCallback13(() => {
4791
5141
  }, []);
4792
5142
  const contextValue = useMemo2(() => ({
4793
5143
  config: PaywalloClient.getConfig(),
4794
5144
  isInitialized,
5145
+ isReady,
4795
5146
  initError,
4796
5147
  distinctId,
4797
5148
  subscription,
@@ -4807,6 +5158,7 @@ function PaywalloProvider({ children, config }) {
4807
5158
  refreshProducts
4808
5159
  }), [
4809
5160
  isInitialized,
5161
+ isReady,
4810
5162
  initError,
4811
5163
  distinctId,
4812
5164
  subscription,
@@ -4821,20 +5173,16 @@ function PaywalloProvider({ children, config }) {
4821
5173
  getPaywallConfig,
4822
5174
  refreshProducts
4823
5175
  ]);
4824
- const SCREEN_HEIGHT3 = useMemo2(() => Dimensions4.get("window").height, []);
4825
- const slideAnim = useRef8(new Animated3.Value(SCREEN_HEIGHT3)).current;
4826
- useEffect7(() => {
5176
+ useLayoutEffect(() => {
4827
5177
  if (showingPreloadedWebView) {
4828
5178
  slideAnim.setValue(SCREEN_HEIGHT3);
4829
- Animated3.timing(slideAnim, { toValue: 0, duration: 320, easing: Easing2.out(Easing2.cubic), useNativeDriver: true }).start();
4830
- } else {
4831
- slideAnim.setValue(SCREEN_HEIGHT3);
5179
+ Animated3.timing(slideAnim, { toValue: 0, duration: 200, easing: Easing2.out(Easing2.cubic), useNativeDriver: true }).start();
4832
5180
  }
4833
5181
  }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT3]);
4834
5182
  const preloadStyle = [
4835
5183
  styles3.preloadOverlay,
4836
5184
  showingPreloadedWebView ? styles3.visible : styles3.hidden,
4837
- { transform: [{ translateY: showingPreloadedWebView ? slideAnim : SCREEN_HEIGHT3 }] }
5185
+ showingPreloadedWebView ? { transform: [{ translateY: slideAnim }] } : void 0
4838
5186
  ];
4839
5187
  return /* @__PURE__ */ jsxs2(PaywalloContext.Provider, { value: contextValue, children: [
4840
5188
  children,
@@ -4847,6 +5195,7 @@ function PaywalloProvider({ children, config }) {
4847
5195
  primaryProductId: preloadedWebView.primaryProductId,
4848
5196
  secondaryProductId: preloadedWebView.secondaryProductId,
4849
5197
  isPurchasing: isPreloadPurchasing,
5198
+ webUrl: isInitialized ? PaywalloClient.getWebUrl() : null,
4850
5199
  onReady: handlePreloadedWebViewReady,
4851
5200
  onClose: showingPreloadedWebView ? handlePreloadedClose : noOp,
4852
5201
  onPurchase: showingPreloadedWebView ? handlePreloadedPurchase : noOp,
@@ -4869,8 +5218,8 @@ function PaywalloProvider({ children, config }) {
4869
5218
  }
4870
5219
  var styles3 = StyleSheet3.create({
4871
5220
  preloadOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#ffffff" },
4872
- hidden: { zIndex: -1 },
4873
- visible: { zIndex: 9999 }
5221
+ hidden: { zIndex: -1, opacity: 0, width: 1, height: 1, overflow: "hidden" },
5222
+ visible: { zIndex: 9999, opacity: 1 }
4874
5223
  });
4875
5224
 
4876
5225
  // src/utils/productVariableResolver.ts
@@ -5009,6 +5358,7 @@ export {
5009
5358
  IDENTITY_ERROR_CODES,
5010
5359
  IdentityError,
5011
5360
  IdentityManager,
5361
+ MetaBridge,
5012
5362
  PAYWALL_ERROR_CODES,
5013
5363
  PURCHASE_ERROR_CODES,
5014
5364
  PaywallContext,
@@ -5036,6 +5386,7 @@ export {
5036
5386
  hasVariables,
5037
5387
  identityManager,
5038
5388
  initLocalization,
5389
+ metaBridge,
5039
5390
  nativeStoreKit,
5040
5391
  networkMonitor,
5041
5392
  offlineQueue,