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