@virex-tech/paywallo-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/MonetySdk.podspec +19 -0
- package/README.md +164 -0
- package/android/build.gradle +54 -0
- package/android/src/main/java/com/monety/sdk/MonetySdkPackage.kt +16 -0
- package/android/src/main/java/com/monety/sdk/PanelStoreKitModule.kt +315 -0
- package/android/src/main/java/com/monety/sdk/PanelWebView.kt +74 -0
- package/android/src/main/java/com/monety/sdk/PanelWebViewManager.kt +52 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloSdkPackage.kt +16 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloStoreKitModule.kt +315 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloWebView.kt +74 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloWebViewManager.kt +52 -0
- package/dist/index.d.mts +1141 -0
- package/dist/index.d.ts +1141 -0
- package/dist/index.js +4793 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4709 -0
- package/dist/index.mjs.map +1 -0
- package/ios/PanelStoreKit.m +21 -0
- package/ios/PanelStoreKit.swift +245 -0
- package/ios/PanelWebView.h +15 -0
- package/ios/PanelWebView.m +182 -0
- package/ios/PanelWebViewManager.m +41 -0
- package/ios/PanelWebViewModule.h +8 -0
- package/ios/PanelWebViewModule.m +42 -0
- package/ios/PaywalloStoreKit.m +21 -0
- package/ios/PaywalloStoreKit.swift +245 -0
- package/ios/PaywalloWebView.h +15 -0
- package/ios/PaywalloWebView.m +182 -0
- package/ios/PaywalloWebViewManager.m +41 -0
- package/ios/PaywalloWebViewModule.h +8 -0
- package/ios/PaywalloWebViewModule.m +42 -0
- package/package.json +93 -0
- package/react-native.config.js +14 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,4709 @@
|
|
|
1
|
+
// src/errors/PaywalloError.ts
|
|
2
|
+
var PaywalloError = class extends Error {
|
|
3
|
+
constructor(domain, code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "PaywalloError";
|
|
6
|
+
this.domain = domain;
|
|
7
|
+
this.code = code;
|
|
8
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/domains/analytics/AnalyticsError.ts
|
|
13
|
+
var AnalyticsError = class extends PaywalloError {
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super("analytics", code, message);
|
|
16
|
+
this.name = "AnalyticsError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var ANALYTICS_ERROR_CODES = {
|
|
20
|
+
NOT_INITIALIZED: "ANALYTICS_NOT_INITIALIZED",
|
|
21
|
+
INVALID_STEP_INDEX: "ANALYTICS_INVALID_STEP_INDEX",
|
|
22
|
+
INVALID_ACTION_NAME: "ANALYTICS_INVALID_ACTION_NAME"
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/errors/ClientError.ts
|
|
26
|
+
var ClientError = class extends PaywalloError {
|
|
27
|
+
constructor(code, message) {
|
|
28
|
+
super("client", code, message);
|
|
29
|
+
this.name = "ClientError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var CLIENT_ERROR_CODES = {
|
|
33
|
+
NOT_INITIALIZED: "CLIENT_NOT_INITIALIZED",
|
|
34
|
+
MISSING_APP_KEY: "CLIENT_MISSING_APP_KEY",
|
|
35
|
+
INVALID_EVENT_NAME: "CLIENT_INVALID_EVENT_NAME",
|
|
36
|
+
PROVIDER_MISSING: "CLIENT_PROVIDER_MISSING",
|
|
37
|
+
UNKNOWN: "CLIENT_UNKNOWN"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/domains/paywall/PaywallModal.tsx
|
|
41
|
+
import { useEffect as useEffect3, useRef as useRef4 } from "react";
|
|
42
|
+
import { Animated as Animated2, Dimensions as Dimensions2, Easing, Modal, StyleSheet as StyleSheet2, Text, View as View2 } from "react-native";
|
|
43
|
+
|
|
44
|
+
// src/domains/paywall/usePaywallActions.ts
|
|
45
|
+
import { useCallback as useCallback2, useRef, useState } from "react";
|
|
46
|
+
import { Animated, Dimensions } from "react-native";
|
|
47
|
+
|
|
48
|
+
// src/domains/iap/IAPService.ts
|
|
49
|
+
import { Platform as Platform2 } from "react-native";
|
|
50
|
+
|
|
51
|
+
// src/domains/iap/PurchaseError.ts
|
|
52
|
+
var PurchaseError = class extends PaywalloError {
|
|
53
|
+
constructor(code, message, userCancelled = false) {
|
|
54
|
+
super("purchase", code, message);
|
|
55
|
+
this.name = "PurchaseError";
|
|
56
|
+
this.userCancelled = userCancelled;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var PURCHASE_ERROR_CODES = {
|
|
60
|
+
NOT_INITIALIZED: "PURCHASE_NOT_INITIALIZED",
|
|
61
|
+
PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
|
|
62
|
+
PURCHASE_FAILED: "PURCHASE_FAILED",
|
|
63
|
+
RESTORE_FAILED: "RESTORE_FAILED",
|
|
64
|
+
VALIDATION_FAILED: "VALIDATION_FAILED",
|
|
65
|
+
USER_CANCELLED: "USER_CANCELLED",
|
|
66
|
+
NETWORK_ERROR: "PURCHASE_NETWORK_ERROR",
|
|
67
|
+
STORE_ERROR: "PURCHASE_STORE_ERROR",
|
|
68
|
+
PENDING_PURCHASE: "PURCHASE_PENDING",
|
|
69
|
+
DEFERRED_PURCHASE: "PURCHASE_DEFERRED",
|
|
70
|
+
STORE_NOT_AVAILABLE: "PURCHASE_STORE_NOT_AVAILABLE"
|
|
71
|
+
};
|
|
72
|
+
var DEFAULT_MESSAGES = {
|
|
73
|
+
NOT_INITIALIZED: "Purchase controller not initialized. Call init() first.",
|
|
74
|
+
PRODUCT_NOT_FOUND: "Product not found in store.",
|
|
75
|
+
PURCHASE_FAILED: "Purchase failed. Please try again.",
|
|
76
|
+
RESTORE_FAILED: "Failed to restore purchases. Please try again.",
|
|
77
|
+
VALIDATION_FAILED: "Failed to validate purchase with server.",
|
|
78
|
+
USER_CANCELLED: "Purchase was cancelled.",
|
|
79
|
+
NETWORK_ERROR: "Network error. Please check your connection.",
|
|
80
|
+
STORE_ERROR: "Store error. Please try again later.",
|
|
81
|
+
PENDING_PURCHASE: "Purchase is pending approval.",
|
|
82
|
+
DEFERRED_PURCHASE: "Purchase is deferred (e.g., parental approval required).",
|
|
83
|
+
STORE_NOT_AVAILABLE: "In-app purchases are not available on this device."
|
|
84
|
+
};
|
|
85
|
+
function createPurchaseError(code, message) {
|
|
86
|
+
return new PurchaseError(
|
|
87
|
+
PURCHASE_ERROR_CODES[code],
|
|
88
|
+
message ?? DEFAULT_MESSAGES[code] ?? "Unknown purchase error",
|
|
89
|
+
code === "USER_CANCELLED"
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/domains/iap/NativeStoreKit.ts
|
|
94
|
+
import { NativeModules, Platform } from "react-native";
|
|
95
|
+
|
|
96
|
+
// src/domains/identity/IdentityError.ts
|
|
97
|
+
var IdentityError = class extends PaywalloError {
|
|
98
|
+
constructor(code, message) {
|
|
99
|
+
super("identity", code, message);
|
|
100
|
+
this.name = "IdentityError";
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var IDENTITY_ERROR_CODES = {
|
|
104
|
+
NOT_INITIALIZED: "IDENTITY_NOT_INITIALIZED",
|
|
105
|
+
DEVICE_ID_UNAVAILABLE: "IDENTITY_DEVICE_ID_UNAVAILABLE",
|
|
106
|
+
STORAGE_READ_FAILED: "IDENTITY_STORAGE_READ_FAILED",
|
|
107
|
+
STORAGE_WRITE_FAILED: "IDENTITY_STORAGE_WRITE_FAILED",
|
|
108
|
+
IDENTIFY_FAILED: "IDENTITY_IDENTIFY_FAILED"
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/utils/uuid.ts
|
|
112
|
+
function generateUUID() {
|
|
113
|
+
const template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
|
|
114
|
+
return template.replace(/[xy]/g, (c) => {
|
|
115
|
+
const r = Math.random() * 16 | 0;
|
|
116
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
117
|
+
return v.toString(16);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/core/storage/SecureStorage.ts
|
|
122
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
123
|
+
var _keychain = void 0;
|
|
124
|
+
async function loadKeychain() {
|
|
125
|
+
if (_keychain !== void 0) return _keychain;
|
|
126
|
+
try {
|
|
127
|
+
const mod = await import("react-native-keychain");
|
|
128
|
+
_keychain = mod.default;
|
|
129
|
+
} catch {
|
|
130
|
+
_keychain = null;
|
|
131
|
+
}
|
|
132
|
+
return _keychain;
|
|
133
|
+
}
|
|
134
|
+
var KEYCHAIN_SERVICE_PREFIX = "com.paywallo.sdk.";
|
|
135
|
+
var SecureStorage = class {
|
|
136
|
+
constructor(debug = false) {
|
|
137
|
+
this.debug = debug;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get a value, trying Keychain first (survives reinstalls on iOS), then AsyncStorage.
|
|
141
|
+
*/
|
|
142
|
+
async get(key) {
|
|
143
|
+
const keychainValue = await this.getFromKeychain(key);
|
|
144
|
+
if (keychainValue) {
|
|
145
|
+
this.log(`Retrieved ${key} from keychain`);
|
|
146
|
+
return keychainValue;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const value = await AsyncStorage.getItem(`@paywallo:${key}`);
|
|
150
|
+
if (value) {
|
|
151
|
+
this.log(`Retrieved ${key} from async storage`);
|
|
152
|
+
await this.setToKeychain(key, value);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
this.log(`Storage read failed for ${key}:`, error);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Set a value in both Keychain (if available) and AsyncStorage.
|
|
162
|
+
*/
|
|
163
|
+
async set(key, value) {
|
|
164
|
+
try {
|
|
165
|
+
await Promise.all([
|
|
166
|
+
AsyncStorage.setItem(`@paywallo:${key}`, value),
|
|
167
|
+
this.setToKeychain(key, value)
|
|
168
|
+
]);
|
|
169
|
+
this.log(`Stored ${key} in storage`);
|
|
170
|
+
return true;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
this.log(`Storage write failed for ${key}:`, error);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async remove(key) {
|
|
177
|
+
try {
|
|
178
|
+
await Promise.all([
|
|
179
|
+
AsyncStorage.removeItem(`@paywallo:${key}`),
|
|
180
|
+
this.removeFromKeychain(key)
|
|
181
|
+
]);
|
|
182
|
+
this.log(`Removed ${key} from storage`);
|
|
183
|
+
return true;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
this.log(`Storage remove failed for ${key}:`, error);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
hasKeychainSupport() {
|
|
190
|
+
return _keychain !== null && _keychain !== void 0;
|
|
191
|
+
}
|
|
192
|
+
async getFromKeychain(key) {
|
|
193
|
+
const keychain = await loadKeychain();
|
|
194
|
+
if (!keychain) return null;
|
|
195
|
+
try {
|
|
196
|
+
const result = await keychain.getGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
|
|
197
|
+
if (result && result.password) {
|
|
198
|
+
return result.password;
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async setToKeychain(key, value) {
|
|
206
|
+
const keychain = await loadKeychain();
|
|
207
|
+
if (!keychain) return false;
|
|
208
|
+
try {
|
|
209
|
+
await keychain.setGenericPassword("panel", value, { service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
|
|
210
|
+
return true;
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async removeFromKeychain(key) {
|
|
216
|
+
const keychain = await loadKeychain();
|
|
217
|
+
if (!keychain) return false;
|
|
218
|
+
try {
|
|
219
|
+
await keychain.resetGenericPassword({ service: `${KEYCHAIN_SERVICE_PREFIX}${key}` });
|
|
220
|
+
return true;
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
log(...args) {
|
|
226
|
+
if (this.debug) {
|
|
227
|
+
console.warn("[Panel Storage]", ...args);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var secureStorage = new SecureStorage();
|
|
232
|
+
|
|
233
|
+
// src/domains/identity/IdentityManager.ts
|
|
234
|
+
var DEVICE_ID_KEY = "device_id";
|
|
235
|
+
var USER_EMAIL_KEY = "user_email";
|
|
236
|
+
var USER_PROPERTIES_KEY = "user_properties";
|
|
237
|
+
var IdentityManager = class {
|
|
238
|
+
constructor() {
|
|
239
|
+
this.deviceId = null;
|
|
240
|
+
this.email = null;
|
|
241
|
+
this.properties = {};
|
|
242
|
+
this.apiClient = null;
|
|
243
|
+
this.secureStorage = null;
|
|
244
|
+
this.debug = false;
|
|
245
|
+
this.initialized = false;
|
|
246
|
+
}
|
|
247
|
+
async initialize(apiClient, debug = false) {
|
|
248
|
+
if (this.initialized) {
|
|
249
|
+
return this.deviceId ?? "";
|
|
250
|
+
}
|
|
251
|
+
this.apiClient = apiClient;
|
|
252
|
+
this.debug = debug;
|
|
253
|
+
this.secureStorage = new SecureStorage(debug);
|
|
254
|
+
await this.loadPersistedState();
|
|
255
|
+
if (!this.deviceId) {
|
|
256
|
+
let DeviceInfo = null;
|
|
257
|
+
try {
|
|
258
|
+
const mod = await import("react-native-device-info");
|
|
259
|
+
DeviceInfo = mod.default;
|
|
260
|
+
} catch {
|
|
261
|
+
DeviceInfo = null;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
if (DeviceInfo) {
|
|
265
|
+
this.deviceId = await DeviceInfo.getUniqueId();
|
|
266
|
+
this.log("Got device ID from DeviceInfo:", this.deviceId);
|
|
267
|
+
} else {
|
|
268
|
+
this.deviceId = generateUUID();
|
|
269
|
+
this.log("DeviceInfo not installed, using generated UUID:", this.deviceId);
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
this.deviceId = generateUUID();
|
|
273
|
+
this.log("DeviceInfo unavailable, using generated UUID:", this.deviceId);
|
|
274
|
+
}
|
|
275
|
+
const stored = await this.secureStorage.set(DEVICE_ID_KEY, this.deviceId);
|
|
276
|
+
if (!stored) {
|
|
277
|
+
this.log("Failed to persist device ID to secure storage, using fallback");
|
|
278
|
+
await this.secureStorage.set("device_id_fallback", this.deviceId).catch(() => {
|
|
279
|
+
this.log("Fallback also failed \u2014 device ID only in memory");
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.initialized = true;
|
|
284
|
+
return this.deviceId ?? "";
|
|
285
|
+
}
|
|
286
|
+
async identify(options) {
|
|
287
|
+
this.ensureInitialized();
|
|
288
|
+
let email;
|
|
289
|
+
let properties;
|
|
290
|
+
if (options) {
|
|
291
|
+
if ("email" in options || "properties" in options) {
|
|
292
|
+
email = options.email;
|
|
293
|
+
properties = options.properties;
|
|
294
|
+
} else {
|
|
295
|
+
properties = options;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (email) {
|
|
299
|
+
this.email = email;
|
|
300
|
+
await this.secureStorage.set(USER_EMAIL_KEY, email);
|
|
301
|
+
}
|
|
302
|
+
if (properties) {
|
|
303
|
+
this.properties = { ...this.properties, ...properties };
|
|
304
|
+
await this.secureStorage.set(USER_PROPERTIES_KEY, JSON.stringify(this.properties));
|
|
305
|
+
}
|
|
306
|
+
if (this.apiClient && this.deviceId) {
|
|
307
|
+
try {
|
|
308
|
+
await this.apiClient.identify(this.deviceId, properties, email);
|
|
309
|
+
this.log("Identity updated on server");
|
|
310
|
+
} catch (error) {
|
|
311
|
+
this.log("Failed to update identity on server:", error);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
getDistinctId() {
|
|
316
|
+
if (!this.initialized || !this.deviceId) {
|
|
317
|
+
if (this.debug) console.warn("[Paywallo Identity] getDistinctId called before initialization");
|
|
318
|
+
return "";
|
|
319
|
+
}
|
|
320
|
+
return this.deviceId;
|
|
321
|
+
}
|
|
322
|
+
getDeviceId() {
|
|
323
|
+
return this.deviceId;
|
|
324
|
+
}
|
|
325
|
+
getEmail() {
|
|
326
|
+
return this.email;
|
|
327
|
+
}
|
|
328
|
+
getProperties() {
|
|
329
|
+
return { ...this.properties };
|
|
330
|
+
}
|
|
331
|
+
async updateProperties(properties) {
|
|
332
|
+
this.ensureInitialized();
|
|
333
|
+
this.properties = { ...this.properties, ...properties };
|
|
334
|
+
await this.secureStorage.set(USER_PROPERTIES_KEY, JSON.stringify(this.properties));
|
|
335
|
+
if (this.apiClient && this.deviceId) {
|
|
336
|
+
try {
|
|
337
|
+
await this.apiClient.identify(this.deviceId, this.properties, this.email ?? void 0);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
this.log("Failed to update properties on server:", error);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async reset() {
|
|
344
|
+
this.email = null;
|
|
345
|
+
this.properties = {};
|
|
346
|
+
this.initialized = false;
|
|
347
|
+
this.apiClient = null;
|
|
348
|
+
this.deviceId = null;
|
|
349
|
+
await Promise.all([
|
|
350
|
+
this.secureStorage?.remove(USER_EMAIL_KEY) ?? Promise.resolve(false),
|
|
351
|
+
this.secureStorage?.remove(USER_PROPERTIES_KEY) ?? Promise.resolve(false),
|
|
352
|
+
this.secureStorage?.remove("device_id_fallback") ?? Promise.resolve(false)
|
|
353
|
+
]);
|
|
354
|
+
}
|
|
355
|
+
getState() {
|
|
356
|
+
return {
|
|
357
|
+
deviceId: this.deviceId ?? "",
|
|
358
|
+
email: this.email,
|
|
359
|
+
properties: this.getProperties()
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
isSecureStorageAvailable() {
|
|
363
|
+
return this.secureStorage !== null;
|
|
364
|
+
}
|
|
365
|
+
async loadPersistedState() {
|
|
366
|
+
if (!this.secureStorage) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const [storedDeviceId, storedEmail, storedPropertiesJson] = await Promise.all([
|
|
370
|
+
this.secureStorage.get(DEVICE_ID_KEY),
|
|
371
|
+
this.secureStorage.get(USER_EMAIL_KEY),
|
|
372
|
+
this.secureStorage.get(USER_PROPERTIES_KEY)
|
|
373
|
+
]);
|
|
374
|
+
if (storedDeviceId) {
|
|
375
|
+
this.deviceId = storedDeviceId;
|
|
376
|
+
this.log("Loaded device ID from storage:", this.deviceId);
|
|
377
|
+
} else {
|
|
378
|
+
const fallbackDeviceId = await this.secureStorage.get("device_id_fallback");
|
|
379
|
+
if (fallbackDeviceId) {
|
|
380
|
+
this.deviceId = fallbackDeviceId;
|
|
381
|
+
this.log("Loaded device ID from fallback storage:", this.deviceId);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (storedEmail) {
|
|
385
|
+
this.email = storedEmail;
|
|
386
|
+
}
|
|
387
|
+
if (storedPropertiesJson) {
|
|
388
|
+
try {
|
|
389
|
+
this.properties = JSON.parse(storedPropertiesJson);
|
|
390
|
+
} catch {
|
|
391
|
+
this.properties = {};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
ensureInitialized() {
|
|
396
|
+
if (!this.initialized) {
|
|
397
|
+
throw new IdentityError(
|
|
398
|
+
IDENTITY_ERROR_CODES.NOT_INITIALIZED,
|
|
399
|
+
"IdentityManager not initialized. Call initialize() first"
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
log(...args) {
|
|
404
|
+
if (this.debug) {
|
|
405
|
+
console.warn("[Paywallo Identity]", ...args);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var identityManager = new IdentityManager();
|
|
410
|
+
|
|
411
|
+
// src/domains/iap/NativeStoreKit.ts
|
|
412
|
+
var PaywalloStoreKitNative = NativeModules.PaywalloStoreKit ?? null;
|
|
413
|
+
function isAvailable() {
|
|
414
|
+
return PaywalloStoreKitNative != null;
|
|
415
|
+
}
|
|
416
|
+
function mapNativeProduct(native) {
|
|
417
|
+
const product = {
|
|
418
|
+
productId: native.productId,
|
|
419
|
+
title: native.title,
|
|
420
|
+
description: native.description,
|
|
421
|
+
price: native.price,
|
|
422
|
+
priceValue: native.priceValue,
|
|
423
|
+
currency: native.currency ?? "USD",
|
|
424
|
+
localizedPrice: native.localizedPrice,
|
|
425
|
+
type: mapProductType(native.type),
|
|
426
|
+
subscriptionPeriod: native.subscriptionPeriod,
|
|
427
|
+
introductoryPrice: native.introductoryPrice,
|
|
428
|
+
introductoryPriceValue: native.introductoryPriceValue,
|
|
429
|
+
freeTrialPeriod: native.freeTrialPeriod
|
|
430
|
+
};
|
|
431
|
+
if (native.subscriptionPeriod) {
|
|
432
|
+
const prices = calculatePerPeriodPrices(native.priceValue, native.subscriptionPeriod);
|
|
433
|
+
product.pricePerMonth = prices.perMonth;
|
|
434
|
+
product.pricePerWeek = prices.perWeek;
|
|
435
|
+
product.pricePerDay = prices.perDay;
|
|
436
|
+
}
|
|
437
|
+
return product;
|
|
438
|
+
}
|
|
439
|
+
function mapProductType(type) {
|
|
440
|
+
switch (type) {
|
|
441
|
+
case "subscription":
|
|
442
|
+
case "autoRenewable":
|
|
443
|
+
return "subscription";
|
|
444
|
+
case "consumable":
|
|
445
|
+
return "consumable";
|
|
446
|
+
default:
|
|
447
|
+
return "non_consumable";
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function calculatePerPeriodPrices(priceValue, period) {
|
|
451
|
+
const totalDays = periodToDays(period);
|
|
452
|
+
if (totalDays <= 0) return {};
|
|
453
|
+
const perDay = priceValue / totalDays;
|
|
454
|
+
const perWeek = perDay * 7;
|
|
455
|
+
const perMonth = perDay * 30;
|
|
456
|
+
return {
|
|
457
|
+
perDay: perDay.toFixed(2),
|
|
458
|
+
perWeek: perWeek.toFixed(2),
|
|
459
|
+
perMonth: perMonth.toFixed(2)
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function periodToDays(period) {
|
|
463
|
+
const match = period.match(/^P(\d+)([DWMY])$/);
|
|
464
|
+
if (!match) return 0;
|
|
465
|
+
const value = parseInt(match[1], 10);
|
|
466
|
+
switch (match[2]) {
|
|
467
|
+
case "D":
|
|
468
|
+
return value;
|
|
469
|
+
case "W":
|
|
470
|
+
return value * 7;
|
|
471
|
+
case "M":
|
|
472
|
+
return value * 30;
|
|
473
|
+
case "Y":
|
|
474
|
+
return value * 365;
|
|
475
|
+
default:
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function mapNativePurchase(native) {
|
|
480
|
+
return {
|
|
481
|
+
productId: native.productId,
|
|
482
|
+
transactionId: native.transactionId,
|
|
483
|
+
transactionDate: native.transactionDate,
|
|
484
|
+
receipt: native.receipt,
|
|
485
|
+
platform: Platform.OS
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
var nativeStoreKit = {
|
|
489
|
+
isAvailable,
|
|
490
|
+
async getProducts(productIds) {
|
|
491
|
+
if (!PaywalloStoreKitNative) {
|
|
492
|
+
if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] StoreKit native module not available");
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
|
|
496
|
+
return nativeProducts.map(mapNativeProduct);
|
|
497
|
+
},
|
|
498
|
+
async purchase(productId) {
|
|
499
|
+
if (!PaywalloStoreKitNative) {
|
|
500
|
+
throw new PurchaseError(
|
|
501
|
+
PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE,
|
|
502
|
+
"StoreKit native module not available"
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
const distinctId = identityManager.getDistinctId();
|
|
506
|
+
const result = await PaywalloStoreKitNative.purchase(productId, distinctId ?? null);
|
|
507
|
+
if (!result || result.pending) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
return mapNativePurchase(result);
|
|
511
|
+
},
|
|
512
|
+
async finishTransaction(transactionId) {
|
|
513
|
+
if (!PaywalloStoreKitNative) return;
|
|
514
|
+
await PaywalloStoreKitNative.finishTransaction(transactionId);
|
|
515
|
+
},
|
|
516
|
+
async getActiveTransactions() {
|
|
517
|
+
if (!PaywalloStoreKitNative) return [];
|
|
518
|
+
const results = await PaywalloStoreKitNative.getActiveTransactions();
|
|
519
|
+
return results.map(mapNativePurchase);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// src/domains/iap/IAPService.ts
|
|
524
|
+
var IAPService = class {
|
|
525
|
+
constructor(apiClient) {
|
|
526
|
+
this.productsCache = /* @__PURE__ */ new Map();
|
|
527
|
+
this.apiClient = null;
|
|
528
|
+
this.apiClient = apiClient ?? null;
|
|
529
|
+
}
|
|
530
|
+
get debug() {
|
|
531
|
+
return PaywalloClient.getConfig()?.debug ?? false;
|
|
532
|
+
}
|
|
533
|
+
getDeviceCountry() {
|
|
534
|
+
try {
|
|
535
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
536
|
+
const parts = locale.split("-");
|
|
537
|
+
return parts[parts.length - 1]?.toUpperCase() || "US";
|
|
538
|
+
} catch {
|
|
539
|
+
return "US";
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
getApiClient() {
|
|
543
|
+
if (this.apiClient) return this.apiClient;
|
|
544
|
+
const client = PaywalloClient.getApiClient();
|
|
545
|
+
if (!client)
|
|
546
|
+
throw new PurchaseError(PURCHASE_ERROR_CODES.NOT_INITIALIZED, "SDK not initialized");
|
|
547
|
+
return client;
|
|
548
|
+
}
|
|
549
|
+
isAvailable() {
|
|
550
|
+
return nativeStoreKit.isAvailable();
|
|
551
|
+
}
|
|
552
|
+
async loadProducts(productIds) {
|
|
553
|
+
if (!nativeStoreKit.isAvailable()) {
|
|
554
|
+
if (this.debug) console.warn("[Paywallo][Products] StoreKit not available");
|
|
555
|
+
return [];
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
if (this.debug) console.warn("[Paywallo][Products] Loading from StoreKit:", productIds);
|
|
559
|
+
const products = await nativeStoreKit.getProducts(productIds);
|
|
560
|
+
if (this.debug) console.warn("[Paywallo][Products] StoreKit returned:", products.length, "products");
|
|
561
|
+
for (const product of products) {
|
|
562
|
+
if (this.debug) console.warn("[Paywallo][Products]", product.productId, "\u2192", product.localizedPrice, "(", product.currency, ")");
|
|
563
|
+
this.productsCache.set(product.productId, product);
|
|
564
|
+
}
|
|
565
|
+
if (products.length < productIds.length) {
|
|
566
|
+
const missing = productIds.filter((id) => !products.find((p) => p.productId === id));
|
|
567
|
+
if (this.debug) console.warn("[Paywallo][Products] Missing from StoreKit:", missing);
|
|
568
|
+
}
|
|
569
|
+
return products;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (this.debug) console.warn("[Paywallo][Products] StoreKit error:", error);
|
|
572
|
+
return [];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
getProduct(productId) {
|
|
576
|
+
return this.productsCache.get(productId);
|
|
577
|
+
}
|
|
578
|
+
getCachedProducts() {
|
|
579
|
+
return new Map(this.productsCache);
|
|
580
|
+
}
|
|
581
|
+
async validateWithServer(purchase, productId, options) {
|
|
582
|
+
const apiClient = this.getApiClient();
|
|
583
|
+
const product = this.productsCache.get(productId);
|
|
584
|
+
const platform = Platform2.OS;
|
|
585
|
+
const distinctId = PaywalloClient.getDistinctId();
|
|
586
|
+
if (this.debug) console.warn("[Paywallo][Purchase] Validating with server:", purchase.productId, "tx:", purchase.transactionId, "price:", product?.priceValue, product?.currency);
|
|
587
|
+
const validation = await apiClient.validatePurchase(
|
|
588
|
+
platform,
|
|
589
|
+
purchase.receipt,
|
|
590
|
+
purchase.productId,
|
|
591
|
+
purchase.transactionId,
|
|
592
|
+
product?.priceValue ?? 0,
|
|
593
|
+
product?.currency ?? "USD",
|
|
594
|
+
this.getDeviceCountry(),
|
|
595
|
+
distinctId,
|
|
596
|
+
options?.paywallPlacement,
|
|
597
|
+
options?.variantKey
|
|
598
|
+
);
|
|
599
|
+
if (this.debug) console.warn("[Paywallo][Purchase] Server validation result:", JSON.stringify(validation));
|
|
600
|
+
return validation;
|
|
601
|
+
}
|
|
602
|
+
async finishTransaction(transactionId) {
|
|
603
|
+
try {
|
|
604
|
+
if (this.debug) console.warn("[Paywallo][Purchase] Finishing transaction:", transactionId);
|
|
605
|
+
await nativeStoreKit.finishTransaction(transactionId);
|
|
606
|
+
if (this.debug) console.warn("[Paywallo][Purchase] Transaction finished OK");
|
|
607
|
+
} catch (finishError) {
|
|
608
|
+
if (this.debug) console.warn("[Paywallo][Purchase] Failed to finish transaction:", finishError instanceof Error ? finishError.message : finishError);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async purchase(productId, options) {
|
|
612
|
+
if (!nativeStoreKit.isAvailable()) {
|
|
613
|
+
return {
|
|
614
|
+
success: false,
|
|
615
|
+
error: new PurchaseError(PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE, "In-app purchases are not available on this device")
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
if (this.debug) console.warn("[Paywallo][Purchase] StoreKit purchase starting:", productId);
|
|
620
|
+
const purchase = await nativeStoreKit.purchase(productId);
|
|
621
|
+
if (!purchase) {
|
|
622
|
+
if (this.debug) console.warn("[Paywallo][Purchase] StoreKit returned null (user cancelled or pending)");
|
|
623
|
+
return { success: false };
|
|
624
|
+
}
|
|
625
|
+
if (this.debug) console.warn("[Paywallo][Purchase] StoreKit OK \u2014 tx:", purchase.transactionId, "product:", purchase.productId, "receipt length:", purchase.receipt?.length);
|
|
626
|
+
let validation = null;
|
|
627
|
+
try {
|
|
628
|
+
validation = await this.validateWithServer(purchase, productId, options);
|
|
629
|
+
} catch (validationError) {
|
|
630
|
+
if (this.debug) console.warn("[Paywallo][Purchase] Server validation FAILED:", validationError instanceof Error ? validationError.message : validationError);
|
|
631
|
+
return {
|
|
632
|
+
success: false,
|
|
633
|
+
error: new PurchaseError(PURCHASE_ERROR_CODES.VALIDATION_FAILED, validationError instanceof Error ? validationError.message : "Server validation failed")
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
if (!validation?.success) {
|
|
637
|
+
if (this.debug) console.warn("[Paywallo][Purchase] Validation returned failure:", validation?.error || "no validation result");
|
|
638
|
+
return {
|
|
639
|
+
success: false,
|
|
640
|
+
error: new PurchaseError(PURCHASE_ERROR_CODES.VALIDATION_FAILED, validation?.error ? `Server validation: ${validation.error}` : "Server validation failed")
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
await this.finishTransaction(purchase.transactionId);
|
|
644
|
+
if (this.debug) console.warn("[Paywallo][Purchase] COMPLETE \u2014 validated & finished:", purchase.productId);
|
|
645
|
+
return { success: true, purchase };
|
|
646
|
+
} catch (error) {
|
|
647
|
+
const e = error instanceof PurchaseError ? error : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, error instanceof Error ? error.message : String(error));
|
|
648
|
+
if (this.debug) console.warn("[Paywallo][Purchase] FAILED:", e.message);
|
|
649
|
+
return { success: false, error: e };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
async restore() {
|
|
653
|
+
if (!nativeStoreKit.isAvailable()) {
|
|
654
|
+
if (this.debug) console.warn("[Paywallo][Restore] StoreKit not available");
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
if (this.debug) console.warn("[Paywallo][Restore] Getting active transactions...");
|
|
659
|
+
const transactions = await nativeStoreKit.getActiveTransactions();
|
|
660
|
+
if (this.debug) console.warn("[Paywallo][Restore] Found", transactions.length, "active transactions");
|
|
661
|
+
const apiClient = this.getApiClient();
|
|
662
|
+
const distinctId = PaywalloClient.getDistinctId();
|
|
663
|
+
const platform = Platform2.OS;
|
|
664
|
+
const results = await Promise.allSettled(
|
|
665
|
+
transactions.map((tx) => {
|
|
666
|
+
const product = this.productsCache.get(tx.productId);
|
|
667
|
+
if (this.debug) console.warn("[Paywallo][Restore] Validating tx:", tx.transactionId, "product:", tx.productId);
|
|
668
|
+
return apiClient.validatePurchase(
|
|
669
|
+
platform,
|
|
670
|
+
tx.receipt,
|
|
671
|
+
tx.productId,
|
|
672
|
+
tx.transactionId,
|
|
673
|
+
product?.priceValue ?? 0,
|
|
674
|
+
product?.currency ?? "USD",
|
|
675
|
+
this.getDeviceCountry(),
|
|
676
|
+
distinctId
|
|
677
|
+
);
|
|
678
|
+
})
|
|
679
|
+
);
|
|
680
|
+
const validated = [];
|
|
681
|
+
for (let i = 0; i < results.length; i++) {
|
|
682
|
+
const r = results[i];
|
|
683
|
+
const tx = transactions[i];
|
|
684
|
+
if (this.debug) console.warn("[Paywallo][Restore] Validation:", r.status, r.status === "fulfilled" ? JSON.stringify(r.value) : r.reason?.message);
|
|
685
|
+
if (r.status === "fulfilled" && r.value.success) {
|
|
686
|
+
try {
|
|
687
|
+
await nativeStoreKit.finishTransaction(tx.transactionId);
|
|
688
|
+
if (this.debug) console.warn("[Paywallo][Restore] Finished tx:", tx.transactionId);
|
|
689
|
+
} catch (finishError) {
|
|
690
|
+
if (this.debug) console.warn("[Paywallo][Restore] Failed to finish tx:", tx.transactionId, finishError instanceof Error ? finishError.message : finishError);
|
|
691
|
+
}
|
|
692
|
+
validated.push(tx);
|
|
693
|
+
} else {
|
|
694
|
+
if (this.debug) console.warn("[Paywallo][Restore] Skipping unvalidated tx:", tx.transactionId);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return validated;
|
|
698
|
+
} catch (error) {
|
|
699
|
+
if (this.debug) console.warn("[Paywallo][Restore] FAILED:", error instanceof Error ? error.message : error);
|
|
700
|
+
return [];
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
var _instance = null;
|
|
705
|
+
function getIAPService() {
|
|
706
|
+
if (!_instance) {
|
|
707
|
+
_instance = new IAPService();
|
|
708
|
+
}
|
|
709
|
+
return _instance;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/domains/paywall/usePaywallTracking.ts
|
|
713
|
+
import { useCallback } from "react";
|
|
714
|
+
function usePaywallTracking() {
|
|
715
|
+
const trackDismissed = useCallback(async (opts) => {
|
|
716
|
+
try {
|
|
717
|
+
const api = PaywalloClient.getApiClient();
|
|
718
|
+
if (!api) return;
|
|
719
|
+
const sessionId = PaywalloClient.getSessionId();
|
|
720
|
+
await api.trackEvent("$paywall_dismissed", PaywalloClient.getDistinctId(), {
|
|
721
|
+
placement: opts.placement,
|
|
722
|
+
paywallId: opts.paywallId,
|
|
723
|
+
durationSeconds: opts.durationSeconds,
|
|
724
|
+
action: opts.action,
|
|
725
|
+
...opts.variantKey && { variantKey: opts.variantKey },
|
|
726
|
+
...opts.campaignId && { campaignId: opts.campaignId },
|
|
727
|
+
...sessionId && { sessionId }
|
|
728
|
+
});
|
|
729
|
+
} catch {
|
|
730
|
+
}
|
|
731
|
+
}, []);
|
|
732
|
+
const trackProductSelected = useCallback((opts) => {
|
|
733
|
+
const api = PaywalloClient.getApiClient();
|
|
734
|
+
if (!api) return;
|
|
735
|
+
const sessionId = PaywalloClient.getSessionId();
|
|
736
|
+
void api.trackEvent("$paywall_product_selected", PaywalloClient.getDistinctId(), {
|
|
737
|
+
placement: opts.placement,
|
|
738
|
+
paywallId: opts.paywallId,
|
|
739
|
+
productId: opts.productId,
|
|
740
|
+
...opts.variantKey && { variantKey: opts.variantKey },
|
|
741
|
+
...opts.campaignId && { campaignId: opts.campaignId },
|
|
742
|
+
...sessionId && { sessionId }
|
|
743
|
+
}).catch(() => {
|
|
744
|
+
});
|
|
745
|
+
}, []);
|
|
746
|
+
const trackPurchased = useCallback(async (opts) => {
|
|
747
|
+
try {
|
|
748
|
+
const api = PaywalloClient.getApiClient();
|
|
749
|
+
if (!api) return;
|
|
750
|
+
const sessionId = PaywalloClient.getSessionId();
|
|
751
|
+
await api.trackEvent("$paywall_purchased", PaywalloClient.getDistinctId(), {
|
|
752
|
+
placement: opts.placement,
|
|
753
|
+
paywallId: opts.paywallId,
|
|
754
|
+
productId: opts.productId,
|
|
755
|
+
...opts.variantKey && { variantKey: opts.variantKey },
|
|
756
|
+
...opts.campaignId && { campaignId: opts.campaignId },
|
|
757
|
+
...sessionId && { sessionId }
|
|
758
|
+
});
|
|
759
|
+
} catch {
|
|
760
|
+
}
|
|
761
|
+
}, []);
|
|
762
|
+
return { trackDismissed, trackProductSelected, trackPurchased };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/domains/paywall/usePaywallActions.ts
|
|
766
|
+
var SCREEN_HEIGHT = Dimensions.get("window").height;
|
|
767
|
+
function usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId) {
|
|
768
|
+
const [isPurchasing, setIsPurchasing] = useState(false);
|
|
769
|
+
const [isClosing, setIsClosing] = useState(false);
|
|
770
|
+
const slideAnim = useRef(new Animated.Value(0)).current;
|
|
771
|
+
const presentedAtRef = useRef(Date.now());
|
|
772
|
+
const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
|
|
773
|
+
const handleClose = useCallback2(() => {
|
|
774
|
+
if (isClosing) return;
|
|
775
|
+
setIsClosing(true);
|
|
776
|
+
Animated.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 300, useNativeDriver: true }).start(() => {
|
|
777
|
+
if (paywallConfig) {
|
|
778
|
+
const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
|
|
779
|
+
void trackDismissed({ placement, paywallId: paywallConfig.id, durationSeconds, action: "close", variantKey, campaignId });
|
|
780
|
+
}
|
|
781
|
+
slideAnim.setValue(0);
|
|
782
|
+
setIsClosing(false);
|
|
783
|
+
onResult({ presented: true, purchased: false, cancelled: true, restored: false });
|
|
784
|
+
});
|
|
785
|
+
}, [isClosing, onResult, paywallConfig, placement, slideAnim, trackDismissed, variantKey, campaignId]);
|
|
786
|
+
const handlePurchase = useCallback2(async (productId) => {
|
|
787
|
+
if (isPurchasing) return;
|
|
788
|
+
setIsPurchasing(true);
|
|
789
|
+
try {
|
|
790
|
+
if (paywallConfig) trackProductSelected({ placement, paywallId: paywallConfig.id, productId, variantKey, campaignId });
|
|
791
|
+
const result = await getIAPService().purchase(productId, { paywallPlacement: placement, variantKey });
|
|
792
|
+
if (result.success) {
|
|
793
|
+
if (paywallConfig) {
|
|
794
|
+
const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
|
|
795
|
+
await trackDismissed({ placement, paywallId: paywallConfig.id, durationSeconds, action: "purchase", variantKey, campaignId });
|
|
796
|
+
await trackPurchased({ placement, paywallId: paywallConfig.id, productId, variantKey, campaignId });
|
|
797
|
+
}
|
|
798
|
+
onResult({ presented: true, purchased: true, cancelled: false, restored: false });
|
|
799
|
+
} else if (result.error) {
|
|
800
|
+
onResult({ presented: true, purchased: false, cancelled: false, restored: false });
|
|
801
|
+
}
|
|
802
|
+
} catch (error) {
|
|
803
|
+
if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Purchase error:", error);
|
|
804
|
+
onResult({ presented: true, purchased: false, cancelled: false, restored: false });
|
|
805
|
+
} finally {
|
|
806
|
+
setIsPurchasing(false);
|
|
807
|
+
}
|
|
808
|
+
}, [isPurchasing, onResult, paywallConfig, placement, trackDismissed, trackProductSelected, trackPurchased, variantKey, campaignId]);
|
|
809
|
+
const handleRestore = useCallback2(async () => {
|
|
810
|
+
if (isPurchasing) return;
|
|
811
|
+
setIsPurchasing(true);
|
|
812
|
+
try {
|
|
813
|
+
const transactions = await getIAPService().restore();
|
|
814
|
+
if (transactions.length > 0) {
|
|
815
|
+
if (paywallConfig) {
|
|
816
|
+
const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
|
|
817
|
+
await trackDismissed({ placement, paywallId: paywallConfig.id, durationSeconds, action: "restore", variantKey, campaignId });
|
|
818
|
+
}
|
|
819
|
+
onResult({ presented: true, purchased: false, cancelled: false, restored: true });
|
|
820
|
+
} else {
|
|
821
|
+
if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] No purchases to restore");
|
|
822
|
+
onResult({ presented: true, purchased: false, cancelled: false, restored: false });
|
|
823
|
+
}
|
|
824
|
+
} catch (error) {
|
|
825
|
+
if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Restore error:", error);
|
|
826
|
+
onResult({ presented: true, purchased: false, cancelled: false, restored: false });
|
|
827
|
+
} finally {
|
|
828
|
+
setIsPurchasing(false);
|
|
829
|
+
}
|
|
830
|
+
}, [isPurchasing, onResult, paywallConfig, placement, trackDismissed, variantKey, campaignId]);
|
|
831
|
+
return { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore };
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/domains/paywall/usePaywallLoader.ts
|
|
835
|
+
import { useEffect, useRef as useRef2, useState as useState2 } from "react";
|
|
836
|
+
|
|
837
|
+
// src/domains/paywall/PaywallError.ts
|
|
838
|
+
var PaywallError = class extends PaywalloError {
|
|
839
|
+
constructor(code, message) {
|
|
840
|
+
super("paywall", code, message);
|
|
841
|
+
this.name = "PaywallError";
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
var PAYWALL_ERROR_CODES = {
|
|
845
|
+
NOT_INITIALIZED: "PAYWALL_NOT_INITIALIZED",
|
|
846
|
+
NOT_FOUND: "PAYWALL_NOT_FOUND",
|
|
847
|
+
RENDER_FAILED: "PAYWALL_RENDER_FAILED",
|
|
848
|
+
LOAD_FAILED: "PAYWALL_LOAD_FAILED",
|
|
849
|
+
DISMISS_FAILED: "PAYWALL_DISMISS_FAILED",
|
|
850
|
+
INVALID_CONFIG: "PAYWALL_INVALID_CONFIG"
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
// src/domains/paywall/usePaywallLoader.ts
|
|
854
|
+
function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts, variantKey, campaignId) {
|
|
855
|
+
const [error, setError] = useState2(null);
|
|
856
|
+
const [paywallConfig, setPaywallConfig] = useState2(null);
|
|
857
|
+
const [products, setProducts] = useState2(/* @__PURE__ */ new Map());
|
|
858
|
+
const preloadedConfigRef = useRef2(preloadedConfig);
|
|
859
|
+
preloadedConfigRef.current = preloadedConfig;
|
|
860
|
+
const preloadedProductsRef = useRef2(preloadedProducts);
|
|
861
|
+
preloadedProductsRef.current = preloadedProducts;
|
|
862
|
+
const trackingDoneRef = useRef2(false);
|
|
863
|
+
useEffect(() => {
|
|
864
|
+
if (!visible) {
|
|
865
|
+
trackingDoneRef.current = false;
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const load = async () => {
|
|
869
|
+
setError(null);
|
|
870
|
+
try {
|
|
871
|
+
const apiClient = PaywalloClient.getApiClient();
|
|
872
|
+
if (!apiClient) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "SDK not initialized");
|
|
873
|
+
let config;
|
|
874
|
+
const snap = preloadedConfigRef.current;
|
|
875
|
+
if (snap) {
|
|
876
|
+
config = {
|
|
877
|
+
id: snap.id,
|
|
878
|
+
placement: snap.placement,
|
|
879
|
+
config: snap.config,
|
|
880
|
+
primaryProductId: snap.primaryProductId,
|
|
881
|
+
secondaryProductId: snap.secondaryProductId
|
|
882
|
+
};
|
|
883
|
+
} else {
|
|
884
|
+
const fetched = await apiClient.getPaywall(placement);
|
|
885
|
+
if (!fetched) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_FOUND, `Paywall not found for placement: ${placement}`);
|
|
886
|
+
config = fetched;
|
|
887
|
+
}
|
|
888
|
+
setPaywallConfig(config);
|
|
889
|
+
const productIds = [];
|
|
890
|
+
if (config.primaryProductId) productIds.push(config.primaryProductId);
|
|
891
|
+
if (config.secondaryProductId) productIds.push(config.secondaryProductId);
|
|
892
|
+
const snapProducts = preloadedProductsRef.current;
|
|
893
|
+
if (snapProducts && snapProducts.size > 0) {
|
|
894
|
+
setProducts(snapProducts);
|
|
895
|
+
} else if (productIds.length > 0) {
|
|
896
|
+
try {
|
|
897
|
+
const storeProducts = await getIAPService().loadProducts(productIds);
|
|
898
|
+
const map = /* @__PURE__ */ new Map();
|
|
899
|
+
for (const p of storeProducts) map.set(p.productId, p);
|
|
900
|
+
setProducts(map);
|
|
901
|
+
} catch {
|
|
902
|
+
setProducts(/* @__PURE__ */ new Map());
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
setProducts(/* @__PURE__ */ new Map());
|
|
906
|
+
}
|
|
907
|
+
if (!trackingDoneRef.current) {
|
|
908
|
+
trackingDoneRef.current = true;
|
|
909
|
+
const sessionId = PaywalloClient.getSessionId();
|
|
910
|
+
await apiClient.trackEvent("$paywall_viewed", PaywalloClient.getDistinctId(), {
|
|
911
|
+
placement,
|
|
912
|
+
paywallId: config.id,
|
|
913
|
+
...sessionId && { sessionId },
|
|
914
|
+
...variantKey && { variantKey },
|
|
915
|
+
...campaignId && { campaignId }
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
} catch (err) {
|
|
919
|
+
const e = err instanceof Error ? err : new PaywallError(PAYWALL_ERROR_CODES.LOAD_FAILED, String(err));
|
|
920
|
+
if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Error loading paywall:", e.message);
|
|
921
|
+
setError(e);
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
void load();
|
|
925
|
+
}, [placement, visible, variantKey, campaignId]);
|
|
926
|
+
return { paywallConfig, products, error };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/domains/paywall/PaywallWebView.tsx
|
|
930
|
+
import { useCallback as useCallback3, useEffect as useEffect2, useMemo, useRef as useRef3, useState as useState3 } from "react";
|
|
931
|
+
import {
|
|
932
|
+
Linking,
|
|
933
|
+
NativeEventEmitter,
|
|
934
|
+
NativeModules as NativeModules2,
|
|
935
|
+
requireNativeComponent,
|
|
936
|
+
StyleSheet,
|
|
937
|
+
View
|
|
938
|
+
} from "react-native";
|
|
939
|
+
|
|
940
|
+
// src/domains/paywall/parseWebViewMessage.ts
|
|
941
|
+
function parseWebViewMessage(raw) {
|
|
942
|
+
try {
|
|
943
|
+
const parsed = JSON.parse(raw);
|
|
944
|
+
if (typeof parsed?.type !== "string") {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
return parsed;
|
|
948
|
+
} catch {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/domains/paywall/paywallScripts.ts
|
|
954
|
+
function buildPaywallInjectionScript(data) {
|
|
955
|
+
const paywallData = {
|
|
956
|
+
type: "paywall",
|
|
957
|
+
data: {
|
|
958
|
+
craftData: data.craftData,
|
|
959
|
+
products: data.products,
|
|
960
|
+
primaryProductId: data.primaryProductId,
|
|
961
|
+
secondaryProductId: data.secondaryProductId
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
return `(function(){var d=${JSON.stringify(paywallData)},a=0,m=50;function t(){a++;if(window.receiveNativeMessage){window.receiveNativeMessage(d);return;}if(a<m){setTimeout(t,100);}else{console.warn('[Panel WV] FAILED to inject after '+m+' attempts');}}t();})();true;`;
|
|
965
|
+
}
|
|
966
|
+
function buildPurchaseStateScript(isPurchasing) {
|
|
967
|
+
return `if(window.receiveNativeMessage){window.receiveNativeMessage({type:"purchaseState",data:{isPurchasing:${isPurchasing}}});}true;`;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/domains/paywall/PaywallWebView.tsx
|
|
971
|
+
import { jsx } from "react/jsx-runtime";
|
|
972
|
+
var NativeWebViewComponent = null;
|
|
973
|
+
function getNativeWebView() {
|
|
974
|
+
if (!NativeWebViewComponent) {
|
|
975
|
+
NativeWebViewComponent = requireNativeComponent("PaywalloWebView");
|
|
976
|
+
}
|
|
977
|
+
return NativeWebViewComponent;
|
|
978
|
+
}
|
|
979
|
+
var webViewEmitter = null;
|
|
980
|
+
function getWebViewEmitter() {
|
|
981
|
+
if (!webViewEmitter) {
|
|
982
|
+
webViewEmitter = new NativeEventEmitter(NativeModules2.PaywalloWebViewModule);
|
|
983
|
+
}
|
|
984
|
+
return webViewEmitter;
|
|
985
|
+
}
|
|
986
|
+
function PaywallWebView({
|
|
987
|
+
paywallId,
|
|
988
|
+
craftData,
|
|
989
|
+
products,
|
|
990
|
+
primaryProductId,
|
|
991
|
+
secondaryProductId,
|
|
992
|
+
isPurchasing,
|
|
993
|
+
onPurchase,
|
|
994
|
+
onClose,
|
|
995
|
+
onRestore,
|
|
996
|
+
onReady
|
|
997
|
+
}) {
|
|
998
|
+
const paywallDataSent = useRef3(false);
|
|
999
|
+
const loadEndReceived = useRef3(false);
|
|
1000
|
+
const [scriptToInject, setScriptToInject] = useState3(void 0);
|
|
1001
|
+
const NativeWebView = useMemo(() => getNativeWebView(), []);
|
|
1002
|
+
const webUrl = useMemo(() => PaywalloClient.getWebUrl() ?? "http://localhost:3000", []);
|
|
1003
|
+
const buildAndSetInjectionScript = useCallback3(() => {
|
|
1004
|
+
if (!craftData || paywallDataSent.current) return;
|
|
1005
|
+
paywallDataSent.current = true;
|
|
1006
|
+
const script = buildPaywallInjectionScript({
|
|
1007
|
+
craftData,
|
|
1008
|
+
products: Array.from(products.values()),
|
|
1009
|
+
primaryProductId,
|
|
1010
|
+
secondaryProductId
|
|
1011
|
+
});
|
|
1012
|
+
setScriptToInject(script);
|
|
1013
|
+
onReady?.();
|
|
1014
|
+
}, [craftData, products, primaryProductId, secondaryProductId, onReady]);
|
|
1015
|
+
const lastProcessedRef = useRef3({ type: "", time: 0 });
|
|
1016
|
+
const handleParsedMessage = useCallback3(
|
|
1017
|
+
(message) => {
|
|
1018
|
+
const now = Date.now();
|
|
1019
|
+
if (message.type === lastProcessedRef.current.type && now - lastProcessedRef.current.time < 500) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
lastProcessedRef.current = { type: message.type, time: now };
|
|
1023
|
+
switch (message.type) {
|
|
1024
|
+
case "ready":
|
|
1025
|
+
if (!paywallDataSent.current) buildAndSetInjectionScript();
|
|
1026
|
+
break;
|
|
1027
|
+
case "purchase":
|
|
1028
|
+
if (message.payload?.productId) onPurchase(message.payload.productId);
|
|
1029
|
+
break;
|
|
1030
|
+
case "close":
|
|
1031
|
+
onClose();
|
|
1032
|
+
break;
|
|
1033
|
+
case "restore":
|
|
1034
|
+
onRestore();
|
|
1035
|
+
break;
|
|
1036
|
+
case "open-url":
|
|
1037
|
+
if (message.payload?.url) {
|
|
1038
|
+
const url = message.payload.url;
|
|
1039
|
+
if (/^https?:\/\//i.test(url) || /^mailto:/i.test(url)) {
|
|
1040
|
+
void Linking.openURL(url);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
[buildAndSetInjectionScript, onPurchase, onClose, onRestore]
|
|
1047
|
+
);
|
|
1048
|
+
useEffect2(() => {
|
|
1049
|
+
const emitter = getWebViewEmitter();
|
|
1050
|
+
const loadEndSub = emitter.addListener("PaywalloWebViewLoadEnd", () => {
|
|
1051
|
+
if (!loadEndReceived.current) {
|
|
1052
|
+
loadEndReceived.current = true;
|
|
1053
|
+
buildAndSetInjectionScript();
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
const messageSub = emitter.addListener("PaywalloWebViewMessage", (event) => {
|
|
1057
|
+
const message = parseWebViewMessage(event.data);
|
|
1058
|
+
if (message) handleParsedMessage(message);
|
|
1059
|
+
});
|
|
1060
|
+
return () => {
|
|
1061
|
+
loadEndSub.remove();
|
|
1062
|
+
messageSub.remove();
|
|
1063
|
+
};
|
|
1064
|
+
}, [buildAndSetInjectionScript, handleParsedMessage]);
|
|
1065
|
+
const handleLoadEnd = useCallback3(() => {
|
|
1066
|
+
if (!loadEndReceived.current) {
|
|
1067
|
+
loadEndReceived.current = true;
|
|
1068
|
+
buildAndSetInjectionScript();
|
|
1069
|
+
}
|
|
1070
|
+
}, [buildAndSetInjectionScript]);
|
|
1071
|
+
const handleMessage = useCallback3(
|
|
1072
|
+
(event) => {
|
|
1073
|
+
const message = parseWebViewMessage(event.nativeEvent.data);
|
|
1074
|
+
if (message) handleParsedMessage(message);
|
|
1075
|
+
},
|
|
1076
|
+
[handleParsedMessage]
|
|
1077
|
+
);
|
|
1078
|
+
useEffect2(() => {
|
|
1079
|
+
if (!paywallDataSent.current) return;
|
|
1080
|
+
setScriptToInject(buildPurchaseStateScript(isPurchasing));
|
|
1081
|
+
}, [isPurchasing]);
|
|
1082
|
+
const paywallUrl = `${webUrl}/paywall/${paywallId}`;
|
|
1083
|
+
return /* @__PURE__ */ jsx(View, { style: styles.container, children: /* @__PURE__ */ jsx(
|
|
1084
|
+
NativeWebView,
|
|
1085
|
+
{
|
|
1086
|
+
sourceUrl: paywallUrl,
|
|
1087
|
+
injectedJavaScript: scriptToInject,
|
|
1088
|
+
onMessage: handleMessage,
|
|
1089
|
+
onLoadEnd: handleLoadEnd,
|
|
1090
|
+
style: styles.webview
|
|
1091
|
+
}
|
|
1092
|
+
) });
|
|
1093
|
+
}
|
|
1094
|
+
var styles = StyleSheet.create({
|
|
1095
|
+
container: { flex: 1 },
|
|
1096
|
+
webview: { flex: 1, backgroundColor: "transparent" }
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// src/domains/paywall/PaywallModal.tsx
|
|
1100
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1101
|
+
var SCREEN_HEIGHT2 = Dimensions2.get("window").height;
|
|
1102
|
+
function PaywallModal({
|
|
1103
|
+
placement,
|
|
1104
|
+
visible,
|
|
1105
|
+
onResult,
|
|
1106
|
+
paywallConfig: preloadedConfig,
|
|
1107
|
+
preloadedProducts,
|
|
1108
|
+
variantKey,
|
|
1109
|
+
campaignId
|
|
1110
|
+
}) {
|
|
1111
|
+
const { paywallConfig, products, error } = usePaywallLoader(
|
|
1112
|
+
placement,
|
|
1113
|
+
visible,
|
|
1114
|
+
preloadedConfig,
|
|
1115
|
+
preloadedProducts,
|
|
1116
|
+
variantKey,
|
|
1117
|
+
campaignId
|
|
1118
|
+
);
|
|
1119
|
+
const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId);
|
|
1120
|
+
const openAnim = useRef4(new Animated2.Value(SCREEN_HEIGHT2)).current;
|
|
1121
|
+
useEffect3(() => {
|
|
1122
|
+
if (visible) {
|
|
1123
|
+
if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] PaywallModal opening \u2014 starting slide-up animation (550ms)");
|
|
1124
|
+
openAnim.setValue(SCREEN_HEIGHT2);
|
|
1125
|
+
Animated2.timing(openAnim, {
|
|
1126
|
+
toValue: 0,
|
|
1127
|
+
duration: 550,
|
|
1128
|
+
easing: Easing.out(Easing.cubic),
|
|
1129
|
+
useNativeDriver: true
|
|
1130
|
+
}).start(() => {
|
|
1131
|
+
if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] PaywallModal slide-up animation completed");
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}, [visible, openAnim]);
|
|
1135
|
+
if (!visible) return null;
|
|
1136
|
+
return /* @__PURE__ */ jsx2(
|
|
1137
|
+
Modal,
|
|
1138
|
+
{
|
|
1139
|
+
visible,
|
|
1140
|
+
animationType: "none",
|
|
1141
|
+
presentationStyle: "overFullScreen",
|
|
1142
|
+
onRequestClose: handleClose,
|
|
1143
|
+
transparent: true,
|
|
1144
|
+
statusBarTranslucent: true,
|
|
1145
|
+
children: /* @__PURE__ */ jsx2(View2, { style: styles2.overlay, children: /* @__PURE__ */ jsx2(Animated2.View, { style: [styles2.animatedContainer, { transform: [{ translateY: openAnim }] }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.container, children: [
|
|
1146
|
+
error && /* @__PURE__ */ jsx2(View2, { style: styles2.errorContainer, children: /* @__PURE__ */ jsx2(Text, { style: styles2.errorText, children: error.message }) }),
|
|
1147
|
+
!error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ jsx2(
|
|
1148
|
+
PaywallWebView,
|
|
1149
|
+
{
|
|
1150
|
+
paywallId: paywallConfig.id,
|
|
1151
|
+
craftData: paywallConfig.config.craftData,
|
|
1152
|
+
products,
|
|
1153
|
+
primaryProductId: paywallConfig.primaryProductId,
|
|
1154
|
+
secondaryProductId: paywallConfig.secondaryProductId,
|
|
1155
|
+
isPurchasing,
|
|
1156
|
+
onClose: handleClose,
|
|
1157
|
+
onPurchase: handlePurchase,
|
|
1158
|
+
onRestore: handleRestore
|
|
1159
|
+
}
|
|
1160
|
+
)
|
|
1161
|
+
] }) }) })
|
|
1162
|
+
}
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
var styles2 = StyleSheet2.create({
|
|
1166
|
+
overlay: {
|
|
1167
|
+
flex: 1,
|
|
1168
|
+
backgroundColor: "rgba(0,0,0,0.3)"
|
|
1169
|
+
},
|
|
1170
|
+
animatedContainer: {
|
|
1171
|
+
flex: 1,
|
|
1172
|
+
backgroundColor: "#fff",
|
|
1173
|
+
borderTopLeftRadius: 0,
|
|
1174
|
+
borderTopRightRadius: 0
|
|
1175
|
+
},
|
|
1176
|
+
container: {
|
|
1177
|
+
flex: 1,
|
|
1178
|
+
backgroundColor: "#fff"
|
|
1179
|
+
},
|
|
1180
|
+
errorContainer: {
|
|
1181
|
+
flex: 1,
|
|
1182
|
+
justifyContent: "center",
|
|
1183
|
+
alignItems: "center",
|
|
1184
|
+
padding: 20
|
|
1185
|
+
},
|
|
1186
|
+
errorText: {
|
|
1187
|
+
fontSize: 16,
|
|
1188
|
+
color: "#ef4444",
|
|
1189
|
+
textAlign: "center"
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
// src/domains/paywall/PaywallErrorBoundary.tsx
|
|
1194
|
+
import { Component } from "react";
|
|
1195
|
+
var PaywallErrorBoundary = class extends Component {
|
|
1196
|
+
constructor() {
|
|
1197
|
+
super(...arguments);
|
|
1198
|
+
this.state = { hasError: false };
|
|
1199
|
+
}
|
|
1200
|
+
static getDerivedStateFromError() {
|
|
1201
|
+
return { hasError: true };
|
|
1202
|
+
}
|
|
1203
|
+
componentDidCatch(error) {
|
|
1204
|
+
if (PaywalloClient.getConfig()?.debug) console.warn("[Paywallo] Paywall render error:", error.message);
|
|
1205
|
+
}
|
|
1206
|
+
render() {
|
|
1207
|
+
return this.state.hasError ? null : this.props.children;
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
// src/domains/paywall/PaywallContext.ts
|
|
1212
|
+
import { createContext, useContext } from "react";
|
|
1213
|
+
var PaywallContext = createContext(null);
|
|
1214
|
+
function usePaywallContext() {
|
|
1215
|
+
const context = useContext(PaywallContext);
|
|
1216
|
+
if (!context) {
|
|
1217
|
+
throw new PaywallError(
|
|
1218
|
+
PAYWALL_ERROR_CODES.NOT_INITIALIZED,
|
|
1219
|
+
"usePaywallContext must be used within a Paywall component"
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
return context;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/domains/paywall/usePaywallPresenter.ts
|
|
1226
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef5, useState as useState4 } from "react";
|
|
1227
|
+
function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView) {
|
|
1228
|
+
const [showingPreloadedWebView, setShowingPreloadedWebView] = useState4(false);
|
|
1229
|
+
const resolverRef = useRef5(null);
|
|
1230
|
+
const presentedAtRef = useRef5(Date.now());
|
|
1231
|
+
useEffect4(() => {
|
|
1232
|
+
return () => {
|
|
1233
|
+
if (resolverRef.current) {
|
|
1234
|
+
resolverRef.current({ presented: false, purchased: false, cancelled: true, restored: false });
|
|
1235
|
+
resolverRef.current = null;
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
}, []);
|
|
1239
|
+
const presentPreloadedWebView = useCallback4(() => {
|
|
1240
|
+
return new Promise((resolve) => {
|
|
1241
|
+
resolverRef.current = resolve;
|
|
1242
|
+
presentedAtRef.current = Date.now();
|
|
1243
|
+
setShowingPreloadedWebView(true);
|
|
1244
|
+
});
|
|
1245
|
+
}, []);
|
|
1246
|
+
const resolvePreloadedPaywall = useCallback4((result) => {
|
|
1247
|
+
if (resolverRef.current) {
|
|
1248
|
+
resolverRef.current(result);
|
|
1249
|
+
resolverRef.current = null;
|
|
1250
|
+
}
|
|
1251
|
+
}, []);
|
|
1252
|
+
const handlePreloadedWebViewReady = useCallback4(() => {
|
|
1253
|
+
setPreloadedWebView((prev) => prev ? { ...prev, loaded: true } : null);
|
|
1254
|
+
}, [setPreloadedWebView]);
|
|
1255
|
+
const handlePreloadedClose = useCallback4(() => {
|
|
1256
|
+
const track = async () => {
|
|
1257
|
+
try {
|
|
1258
|
+
const apiClient = PaywalloClient.getApiClient();
|
|
1259
|
+
if (!apiClient || !preloadedWebView) return;
|
|
1260
|
+
const sessionId = PaywalloClient.getSessionId();
|
|
1261
|
+
const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
|
|
1262
|
+
await apiClient.trackEvent("$paywall_dismissed", PaywalloClient.getDistinctId(), {
|
|
1263
|
+
placement: preloadedWebView.placement,
|
|
1264
|
+
paywallId: preloadedWebView.paywallId,
|
|
1265
|
+
durationSeconds,
|
|
1266
|
+
action: "close",
|
|
1267
|
+
...sessionId && { sessionId },
|
|
1268
|
+
...preloadedWebView.variantKey && { variantKey: preloadedWebView.variantKey },
|
|
1269
|
+
...preloadedWebView.campaignId && { campaignId: preloadedWebView.campaignId }
|
|
1270
|
+
});
|
|
1271
|
+
} catch {
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
void track();
|
|
1275
|
+
setShowingPreloadedWebView(false);
|
|
1276
|
+
clearPreloadedWebView();
|
|
1277
|
+
resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: true, restored: false });
|
|
1278
|
+
}, [preloadedWebView, clearPreloadedWebView, resolvePreloadedPaywall]);
|
|
1279
|
+
return { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef };
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/domains/affiliate/AffiliateError.ts
|
|
1283
|
+
var AffiliateError = class extends PaywalloError {
|
|
1284
|
+
constructor(code, message) {
|
|
1285
|
+
super("affiliate", code, message);
|
|
1286
|
+
this.name = "AffiliateError";
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
var AFFILIATE_ERROR_CODES = {
|
|
1290
|
+
NOT_INITIALIZED: "AFFILIATE_NOT_INITIALIZED",
|
|
1291
|
+
COUPON_REGISTER_FAILED: "AFFILIATE_COUPON_REGISTER_FAILED",
|
|
1292
|
+
COUPON_APPLY_FAILED: "AFFILIATE_COUPON_APPLY_FAILED",
|
|
1293
|
+
BALANCE_FETCH_FAILED: "AFFILIATE_BALANCE_FETCH_FAILED",
|
|
1294
|
+
WITHDRAWAL_FAILED: "AFFILIATE_WITHDRAWAL_FAILED",
|
|
1295
|
+
NETWORK_ERROR: "AFFILIATE_NETWORK_ERROR"
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
// src/domains/affiliate/affiliateHttp.ts
|
|
1299
|
+
function buildQueryString(params) {
|
|
1300
|
+
if (!params) return "";
|
|
1301
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0 && v !== null);
|
|
1302
|
+
if (entries.length === 0) return "";
|
|
1303
|
+
return `?${entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&")}`;
|
|
1304
|
+
}
|
|
1305
|
+
async function affiliateGet(apiClient, path, params) {
|
|
1306
|
+
const response = await apiClient.get(`${path}${buildQueryString(params)}`);
|
|
1307
|
+
return response.data;
|
|
1308
|
+
}
|
|
1309
|
+
async function affiliatePost(apiClient, path, body) {
|
|
1310
|
+
const response = await apiClient.post(path, body);
|
|
1311
|
+
return response.data;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/domains/affiliate/affiliateDateMappers.ts
|
|
1315
|
+
function mapRawCoupon(raw) {
|
|
1316
|
+
return { code: raw.code, status: raw.status, createdAt: new Date(raw.createdAt) };
|
|
1317
|
+
}
|
|
1318
|
+
function mapRawCommission(raw) {
|
|
1319
|
+
return { ...raw, createdAt: new Date(raw.createdAt) };
|
|
1320
|
+
}
|
|
1321
|
+
function mapRawWithdrawal(raw) {
|
|
1322
|
+
return { ...raw, createdAt: new Date(raw.createdAt) };
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// src/domains/affiliate/AffiliateManager.ts
|
|
1326
|
+
var AffiliateManager = class {
|
|
1327
|
+
constructor() {
|
|
1328
|
+
this.apiClient = null;
|
|
1329
|
+
this.debug = false;
|
|
1330
|
+
}
|
|
1331
|
+
initialize(apiClient, debug = false) {
|
|
1332
|
+
this.apiClient = apiClient;
|
|
1333
|
+
this.debug = debug;
|
|
1334
|
+
this.log("AffiliateManager initialized");
|
|
1335
|
+
}
|
|
1336
|
+
async registerCoupon(code) {
|
|
1337
|
+
this.ensureInitialized();
|
|
1338
|
+
const distinctId = identityManager.getDistinctId();
|
|
1339
|
+
try {
|
|
1340
|
+
const response = await this.post("/api/v1/affiliate/register-coupon", {
|
|
1341
|
+
code,
|
|
1342
|
+
distinctId
|
|
1343
|
+
});
|
|
1344
|
+
if (response.success && response.coupon) {
|
|
1345
|
+
this.log("Coupon registered:", code);
|
|
1346
|
+
return { success: true, coupon: mapRawCoupon(response.coupon) };
|
|
1347
|
+
}
|
|
1348
|
+
return {
|
|
1349
|
+
success: false,
|
|
1350
|
+
error: response.error
|
|
1351
|
+
};
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
this.log("Failed to register coupon:", error);
|
|
1354
|
+
return {
|
|
1355
|
+
success: false,
|
|
1356
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
async applyCoupon(code) {
|
|
1361
|
+
this.ensureInitialized();
|
|
1362
|
+
const distinctId = identityManager.getDistinctId();
|
|
1363
|
+
try {
|
|
1364
|
+
const response = await this.post("/api/v1/affiliate/apply-coupon", {
|
|
1365
|
+
code,
|
|
1366
|
+
distinctId
|
|
1367
|
+
});
|
|
1368
|
+
if (response.success) {
|
|
1369
|
+
this.log("Coupon applied:", code);
|
|
1370
|
+
}
|
|
1371
|
+
return response;
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
this.log("Failed to apply coupon:", error);
|
|
1374
|
+
return {
|
|
1375
|
+
success: false,
|
|
1376
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
async getMyCoupon() {
|
|
1381
|
+
this.ensureInitialized();
|
|
1382
|
+
const distinctId = identityManager.getDistinctId();
|
|
1383
|
+
try {
|
|
1384
|
+
const response = await this.get("/api/v1/affiliate/my-coupon", { distinctId });
|
|
1385
|
+
if (response.coupon) {
|
|
1386
|
+
return { coupon: mapRawCoupon(response.coupon) };
|
|
1387
|
+
}
|
|
1388
|
+
return { coupon: null };
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
this.log("Failed to get my coupon:", error);
|
|
1391
|
+
return { coupon: null };
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
async getAppliedCoupon() {
|
|
1395
|
+
this.ensureInitialized();
|
|
1396
|
+
const distinctId = identityManager.getDistinctId();
|
|
1397
|
+
try {
|
|
1398
|
+
const response = await this.get("/api/v1/affiliate/applied-coupon", { distinctId });
|
|
1399
|
+
return response;
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
this.log("Failed to get applied coupon:", error);
|
|
1402
|
+
return { coupon: null };
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
async getBalance() {
|
|
1406
|
+
this.ensureInitialized();
|
|
1407
|
+
const distinctId = identityManager.getDistinctId();
|
|
1408
|
+
try {
|
|
1409
|
+
const response = await this.get("/api/v1/affiliate/balance", {
|
|
1410
|
+
distinctId
|
|
1411
|
+
});
|
|
1412
|
+
return response;
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
this.log("Failed to get balance:", error);
|
|
1415
|
+
return {
|
|
1416
|
+
total: 0,
|
|
1417
|
+
pending: 0,
|
|
1418
|
+
available: 0,
|
|
1419
|
+
withdrawn: 0
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
async getCommissions() {
|
|
1424
|
+
this.ensureInitialized();
|
|
1425
|
+
const distinctId = identityManager.getDistinctId();
|
|
1426
|
+
try {
|
|
1427
|
+
const response = await this.get("/api/v1/affiliate/commissions", { distinctId });
|
|
1428
|
+
return response.commissions.map(mapRawCommission);
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
this.log("Failed to get commissions:", error);
|
|
1431
|
+
return [];
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
async requestWithdrawal(amount) {
|
|
1435
|
+
this.ensureInitialized();
|
|
1436
|
+
const distinctId = identityManager.getDistinctId();
|
|
1437
|
+
try {
|
|
1438
|
+
const response = await this.post("/api/v1/affiliate/withdraw", {
|
|
1439
|
+
amount,
|
|
1440
|
+
distinctId
|
|
1441
|
+
});
|
|
1442
|
+
if (response.success && response.withdrawal) {
|
|
1443
|
+
this.log("Withdrawal requested:", amount);
|
|
1444
|
+
return { success: true, withdrawal: mapRawWithdrawal(response.withdrawal) };
|
|
1445
|
+
}
|
|
1446
|
+
return {
|
|
1447
|
+
success: false,
|
|
1448
|
+
error: response.error
|
|
1449
|
+
};
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
this.log("Failed to request withdrawal:", error);
|
|
1452
|
+
return {
|
|
1453
|
+
success: false,
|
|
1454
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
async getWithdrawals() {
|
|
1459
|
+
this.ensureInitialized();
|
|
1460
|
+
const distinctId = identityManager.getDistinctId();
|
|
1461
|
+
try {
|
|
1462
|
+
const response = await this.get("/api/v1/affiliate/withdrawals", { distinctId });
|
|
1463
|
+
return response.withdrawals.map(mapRawWithdrawal);
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
this.log("Failed to get withdrawals:", error);
|
|
1466
|
+
return [];
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
async get(path, params) {
|
|
1470
|
+
this.ensureInitialized();
|
|
1471
|
+
return affiliateGet(this.apiClient, path, params);
|
|
1472
|
+
}
|
|
1473
|
+
async post(path, body) {
|
|
1474
|
+
this.ensureInitialized();
|
|
1475
|
+
return affiliatePost(this.apiClient, path, body);
|
|
1476
|
+
}
|
|
1477
|
+
ensureInitialized() {
|
|
1478
|
+
if (!this.apiClient) {
|
|
1479
|
+
throw new AffiliateError(
|
|
1480
|
+
AFFILIATE_ERROR_CODES.NOT_INITIALIZED,
|
|
1481
|
+
"AffiliateManager not initialized. Call initialize() first"
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
log(...args) {
|
|
1486
|
+
if (this.debug) {
|
|
1487
|
+
console.warn("[AffiliateManager]", ...args);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
var affiliateManager = new AffiliateManager();
|
|
1492
|
+
|
|
1493
|
+
// src/domains/affiliate/PaywalloAffiliate.ts
|
|
1494
|
+
var PaywalloAffiliate = {
|
|
1495
|
+
registerCoupon: (code) => affiliateManager.registerCoupon(code),
|
|
1496
|
+
applyCoupon: (code) => affiliateManager.applyCoupon(code),
|
|
1497
|
+
getMyCoupon: () => affiliateManager.getMyCoupon(),
|
|
1498
|
+
getAppliedCoupon: () => affiliateManager.getAppliedCoupon(),
|
|
1499
|
+
getBalance: () => affiliateManager.getBalance(),
|
|
1500
|
+
getCommissions: () => affiliateManager.getCommissions(),
|
|
1501
|
+
requestWithdrawal: (amount) => affiliateManager.requestWithdrawal(amount),
|
|
1502
|
+
getWithdrawals: () => affiliateManager.getWithdrawals()
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
// src/core/ApiClient.ts
|
|
1506
|
+
import { Platform as Platform3 } from "react-native";
|
|
1507
|
+
|
|
1508
|
+
// src/core/apiUrlUtils.ts
|
|
1509
|
+
var DEFAULT_WEB_URL = "https://paywallo.com.br";
|
|
1510
|
+
function deriveWebUrl(baseUrl) {
|
|
1511
|
+
try {
|
|
1512
|
+
const url = new URL(baseUrl);
|
|
1513
|
+
if (url.hostname.startsWith("192.168") || url.hostname === "localhost") {
|
|
1514
|
+
url.port = "3000";
|
|
1515
|
+
return url.toString().replace(/\/$/, "");
|
|
1516
|
+
}
|
|
1517
|
+
if (url.hostname.startsWith("api.")) {
|
|
1518
|
+
url.hostname = url.hostname.replace("api.", "app.");
|
|
1519
|
+
return url.toString().replace(/\/$/, "");
|
|
1520
|
+
}
|
|
1521
|
+
return DEFAULT_WEB_URL;
|
|
1522
|
+
} catch {
|
|
1523
|
+
return DEFAULT_WEB_URL;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function buildConditionalFlagUrl(baseUrl, flagKey, context) {
|
|
1527
|
+
const url = new URL(`${baseUrl}/api/v1/conditional-flags/${flagKey}`);
|
|
1528
|
+
if (context.platform) url.searchParams.set("platform", context.platform);
|
|
1529
|
+
if (context.appVersion) url.searchParams.set("appVersion", context.appVersion);
|
|
1530
|
+
if (context.country) url.searchParams.set("country", context.country);
|
|
1531
|
+
if (context.distinctId) url.searchParams.set("distinctId", context.distinctId);
|
|
1532
|
+
return url.toString();
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// src/domains/iap/apiPurchaseMethods.ts
|
|
1536
|
+
async function validatePurchase(client, appKey, platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey, debug) {
|
|
1537
|
+
if (debug) console.warn("[Paywallo][API] POST /purchases/validate", { platform, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey, receiptLen: receipt?.length });
|
|
1538
|
+
const response = await client.post(
|
|
1539
|
+
`/purchases/validate/${appKey}`,
|
|
1540
|
+
{
|
|
1541
|
+
platform,
|
|
1542
|
+
receipt,
|
|
1543
|
+
productId,
|
|
1544
|
+
transactionId,
|
|
1545
|
+
priceLocal,
|
|
1546
|
+
currency,
|
|
1547
|
+
country,
|
|
1548
|
+
distinctId,
|
|
1549
|
+
...paywallPlacement !== void 0 && { paywallPlacement },
|
|
1550
|
+
...variantKey !== void 0 && { variantKey }
|
|
1551
|
+
},
|
|
1552
|
+
false
|
|
1553
|
+
);
|
|
1554
|
+
if (debug) console.warn("[Paywallo][API] Validation response:", JSON.stringify(response.data));
|
|
1555
|
+
return response.data;
|
|
1556
|
+
}
|
|
1557
|
+
async function getSubscriptionStatus(client, baseUrl, appKey, distinctId) {
|
|
1558
|
+
const url = new URL(`${baseUrl}/purchases/status/${appKey}`);
|
|
1559
|
+
url.searchParams.set("distinctId", distinctId);
|
|
1560
|
+
const response = await client.get(url.toString());
|
|
1561
|
+
return response.data;
|
|
1562
|
+
}
|
|
1563
|
+
async function getEmergencyPaywall(client) {
|
|
1564
|
+
try {
|
|
1565
|
+
const response = await client.get("/api/v1/emergency-paywall");
|
|
1566
|
+
if (response.status === 404) return { enabled: false };
|
|
1567
|
+
return response.data;
|
|
1568
|
+
} catch {
|
|
1569
|
+
return { enabled: false };
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
async function getCampaignData(client, baseUrl, placement, distinctId, context) {
|
|
1573
|
+
const url = new URL(`${baseUrl}/api/v1/campaigns/${placement}`);
|
|
1574
|
+
url.searchParams.set("distinctId", distinctId);
|
|
1575
|
+
if (context) url.searchParams.set("context", JSON.stringify(context));
|
|
1576
|
+
const response = await client.get(url.toString());
|
|
1577
|
+
if (response.status === 404) return null;
|
|
1578
|
+
const data = response.data;
|
|
1579
|
+
if ("paywall" in data && data.paywall === null && !("campaignId" in data)) return null;
|
|
1580
|
+
return data;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/core/http/types.ts
|
|
1584
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
1585
|
+
maxRetries: 2,
|
|
1586
|
+
baseDelay: 500,
|
|
1587
|
+
maxDelay: 4e3,
|
|
1588
|
+
retryableStatusCodes: [408, 429, 500, 502, 503, 504]
|
|
1589
|
+
};
|
|
1590
|
+
var DEFAULT_HTTP_CONFIG = {
|
|
1591
|
+
timeout: 1e4,
|
|
1592
|
+
retry: DEFAULT_RETRY_CONFIG,
|
|
1593
|
+
debug: false
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1596
|
+
// src/core/http/HttpClient.ts
|
|
1597
|
+
var HttpClient = class {
|
|
1598
|
+
constructor(baseUrl, config) {
|
|
1599
|
+
this.config = {
|
|
1600
|
+
baseUrl,
|
|
1601
|
+
timeout: config?.timeout ?? DEFAULT_HTTP_CONFIG.timeout,
|
|
1602
|
+
retry: { ...DEFAULT_RETRY_CONFIG, ...config?.retry },
|
|
1603
|
+
debug: config?.debug ?? false
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
async get(path, options) {
|
|
1607
|
+
return this.request(path, { ...options, method: "GET" });
|
|
1608
|
+
}
|
|
1609
|
+
async post(path, body, options) {
|
|
1610
|
+
return this.request(path, { ...options, method: "POST", body });
|
|
1611
|
+
}
|
|
1612
|
+
async request(path, options) {
|
|
1613
|
+
const url = this.buildUrl(path);
|
|
1614
|
+
const fetchOptions = this.buildFetchOptions(options);
|
|
1615
|
+
const timeout = options.timeout ?? this.config.timeout;
|
|
1616
|
+
if (options.skipRetry) {
|
|
1617
|
+
return this.executeRequest(url, fetchOptions, timeout);
|
|
1618
|
+
}
|
|
1619
|
+
return this.executeWithRetry(url, fetchOptions, options);
|
|
1620
|
+
}
|
|
1621
|
+
async executeWithRetry(url, fetchOptions, options) {
|
|
1622
|
+
const retryConfig = this.config.retry;
|
|
1623
|
+
const timeout = options.timeout ?? this.config.timeout;
|
|
1624
|
+
const state = { attempt: 0, lastError: null };
|
|
1625
|
+
while (state.attempt <= retryConfig.maxRetries) {
|
|
1626
|
+
try {
|
|
1627
|
+
const response = await this.executeRequest(url, fetchOptions, timeout);
|
|
1628
|
+
if (this.shouldRetry(response.status, retryConfig)) {
|
|
1629
|
+
if (state.attempt < retryConfig.maxRetries) {
|
|
1630
|
+
state.attempt++;
|
|
1631
|
+
const delay = this.calculateDelay(state.attempt, retryConfig);
|
|
1632
|
+
this.log(`Retrying request (attempt ${state.attempt}/${retryConfig.maxRetries})`, {
|
|
1633
|
+
url,
|
|
1634
|
+
status: response.status,
|
|
1635
|
+
delay
|
|
1636
|
+
});
|
|
1637
|
+
await this.sleep(delay);
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return response;
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
state.lastError = error instanceof Error ? error : new ClientError(CLIENT_ERROR_CODES.UNKNOWN, String(error));
|
|
1644
|
+
if (this.isNetworkError(state.lastError) && state.attempt < retryConfig.maxRetries) {
|
|
1645
|
+
state.attempt++;
|
|
1646
|
+
const delay = this.calculateDelay(state.attempt, retryConfig);
|
|
1647
|
+
this.log(`Network error, retrying (attempt ${state.attempt}/${retryConfig.maxRetries})`, {
|
|
1648
|
+
url,
|
|
1649
|
+
error: state.lastError.message,
|
|
1650
|
+
delay
|
|
1651
|
+
});
|
|
1652
|
+
await this.sleep(delay);
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
throw state.lastError;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
throw state.lastError ?? new ClientError(CLIENT_ERROR_CODES.UNKNOWN, "Max retries exceeded");
|
|
1659
|
+
}
|
|
1660
|
+
async executeRequest(url, options, timeout) {
|
|
1661
|
+
const controller = new AbortController();
|
|
1662
|
+
let timeoutId = null;
|
|
1663
|
+
let isCompleted = false;
|
|
1664
|
+
timeoutId = setTimeout(() => {
|
|
1665
|
+
if (!isCompleted) {
|
|
1666
|
+
controller.abort();
|
|
1667
|
+
}
|
|
1668
|
+
}, timeout);
|
|
1669
|
+
this.log("Request starting", {
|
|
1670
|
+
url,
|
|
1671
|
+
method: options.method,
|
|
1672
|
+
headers: options.headers
|
|
1673
|
+
});
|
|
1674
|
+
try {
|
|
1675
|
+
const response = await fetch(url, {
|
|
1676
|
+
...options,
|
|
1677
|
+
signal: controller.signal
|
|
1678
|
+
});
|
|
1679
|
+
isCompleted = true;
|
|
1680
|
+
let data;
|
|
1681
|
+
const contentType = response.headers.get("content-type");
|
|
1682
|
+
if (contentType?.includes("application/json")) {
|
|
1683
|
+
data = await response.json();
|
|
1684
|
+
} else {
|
|
1685
|
+
const textData = await response.text();
|
|
1686
|
+
this.log("Non-JSON response received", {
|
|
1687
|
+
url,
|
|
1688
|
+
status: response.status,
|
|
1689
|
+
contentType,
|
|
1690
|
+
bodyPreview: textData.substring(0, 200)
|
|
1691
|
+
});
|
|
1692
|
+
data = textData;
|
|
1693
|
+
}
|
|
1694
|
+
this.log("Request completed", {
|
|
1695
|
+
url,
|
|
1696
|
+
status: response.status,
|
|
1697
|
+
ok: response.ok,
|
|
1698
|
+
contentType
|
|
1699
|
+
});
|
|
1700
|
+
const headers = {};
|
|
1701
|
+
response.headers.forEach((value, key) => {
|
|
1702
|
+
headers[key] = value;
|
|
1703
|
+
});
|
|
1704
|
+
return {
|
|
1705
|
+
ok: response.ok,
|
|
1706
|
+
status: response.status,
|
|
1707
|
+
data,
|
|
1708
|
+
headers
|
|
1709
|
+
};
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
this.log("Request failed", {
|
|
1712
|
+
url,
|
|
1713
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1714
|
+
});
|
|
1715
|
+
throw error;
|
|
1716
|
+
} finally {
|
|
1717
|
+
isCompleted = true;
|
|
1718
|
+
if (timeoutId) {
|
|
1719
|
+
clearTimeout(timeoutId);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
buildUrl(path) {
|
|
1724
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
1725
|
+
return path;
|
|
1726
|
+
}
|
|
1727
|
+
return `${this.config.baseUrl}${path}`;
|
|
1728
|
+
}
|
|
1729
|
+
buildFetchOptions(options) {
|
|
1730
|
+
const headers = {
|
|
1731
|
+
"Content-Type": "application/json",
|
|
1732
|
+
...options.headers
|
|
1733
|
+
};
|
|
1734
|
+
const fetchOptions = {
|
|
1735
|
+
method: options.method,
|
|
1736
|
+
headers
|
|
1737
|
+
};
|
|
1738
|
+
if (options.body !== void 0) {
|
|
1739
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
1740
|
+
}
|
|
1741
|
+
return fetchOptions;
|
|
1742
|
+
}
|
|
1743
|
+
shouldRetry(status, config) {
|
|
1744
|
+
return config.retryableStatusCodes.includes(status);
|
|
1745
|
+
}
|
|
1746
|
+
isNetworkError(error) {
|
|
1747
|
+
const networkErrorPatterns = [
|
|
1748
|
+
"network",
|
|
1749
|
+
"fetch",
|
|
1750
|
+
"ECONNREFUSED",
|
|
1751
|
+
"ETIMEDOUT",
|
|
1752
|
+
"ENOTFOUND",
|
|
1753
|
+
"aborted",
|
|
1754
|
+
"timeout"
|
|
1755
|
+
];
|
|
1756
|
+
const message = error.message.toLowerCase();
|
|
1757
|
+
return networkErrorPatterns.some((pattern) => message.includes(pattern.toLowerCase()));
|
|
1758
|
+
}
|
|
1759
|
+
calculateDelay(attempt, config) {
|
|
1760
|
+
const exponentialDelay = config.baseDelay * Math.pow(2, attempt - 1);
|
|
1761
|
+
const jitter = Math.random() * 0.3 * exponentialDelay;
|
|
1762
|
+
return Math.min(exponentialDelay + jitter, config.maxDelay);
|
|
1763
|
+
}
|
|
1764
|
+
sleep(ms) {
|
|
1765
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1766
|
+
}
|
|
1767
|
+
log(message, data) {
|
|
1768
|
+
if (this.config.debug) {
|
|
1769
|
+
console.warn("[HttpClient]", message, data ?? "");
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
setDebug(debug) {
|
|
1773
|
+
this.config.debug = debug;
|
|
1774
|
+
}
|
|
1775
|
+
getConfig() {
|
|
1776
|
+
return { ...this.config };
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
// src/core/network/NetworkMonitor.ts
|
|
1781
|
+
import { AppState } from "react-native";
|
|
1782
|
+
|
|
1783
|
+
// src/core/network/types.ts
|
|
1784
|
+
var DEFAULT_NETWORK_CONFIG = {
|
|
1785
|
+
debug: false,
|
|
1786
|
+
checkInterval: 3e4,
|
|
1787
|
+
checkUrl: "https://clients3.google.com/generate_204"
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1790
|
+
// src/core/network/NetworkMonitor.ts
|
|
1791
|
+
var NetworkMonitorClass = class {
|
|
1792
|
+
constructor() {
|
|
1793
|
+
this.config = DEFAULT_NETWORK_CONFIG;
|
|
1794
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1795
|
+
this.currentState = "unknown";
|
|
1796
|
+
this.netInfoModule = null;
|
|
1797
|
+
this.netInfoSubscription = null;
|
|
1798
|
+
this.appStateSubscription = null;
|
|
1799
|
+
this.initialized = false;
|
|
1800
|
+
this.checkIntervalId = null;
|
|
1801
|
+
}
|
|
1802
|
+
async initialize(config) {
|
|
1803
|
+
if (this.initialized) {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
this.config = { ...DEFAULT_NETWORK_CONFIG, ...config };
|
|
1807
|
+
this.currentState = "unknown";
|
|
1808
|
+
this.initialized = true;
|
|
1809
|
+
this.log("Initialized (state: unknown, awaiting first connectivity check)");
|
|
1810
|
+
void this.setupFallbackDetection();
|
|
1811
|
+
this.setupAppStateListener();
|
|
1812
|
+
}
|
|
1813
|
+
setupNetInfoListener() {
|
|
1814
|
+
if (!this.netInfoModule) return;
|
|
1815
|
+
this.netInfoSubscription = this.netInfoModule.addEventListener((state) => {
|
|
1816
|
+
const newState = this.netInfoStateToNetworkState(state);
|
|
1817
|
+
this.updateState(newState);
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
async setupFallbackDetection() {
|
|
1821
|
+
await this.checkConnectivity();
|
|
1822
|
+
this.checkIntervalId = setInterval(() => {
|
|
1823
|
+
void this.checkConnectivity();
|
|
1824
|
+
}, this.config.checkInterval);
|
|
1825
|
+
}
|
|
1826
|
+
setupAppStateListener() {
|
|
1827
|
+
this.appStateSubscription = AppState.addEventListener("change", (state) => {
|
|
1828
|
+
if (state === "active") {
|
|
1829
|
+
void this.checkConnectivity();
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
async checkConnectivity() {
|
|
1834
|
+
try {
|
|
1835
|
+
const controller = new AbortController();
|
|
1836
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
1837
|
+
const response = await fetch(this.config.checkUrl, {
|
|
1838
|
+
method: "HEAD",
|
|
1839
|
+
signal: controller.signal
|
|
1840
|
+
});
|
|
1841
|
+
clearTimeout(timeoutId);
|
|
1842
|
+
this.updateState(response.ok ? "online" : "offline");
|
|
1843
|
+
} catch {
|
|
1844
|
+
this.updateState("offline");
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
netInfoStateToNetworkState(state) {
|
|
1848
|
+
if (state.isConnected === null) {
|
|
1849
|
+
return "unknown";
|
|
1850
|
+
}
|
|
1851
|
+
if (!state.isConnected) {
|
|
1852
|
+
return "offline";
|
|
1853
|
+
}
|
|
1854
|
+
return "online";
|
|
1855
|
+
}
|
|
1856
|
+
updateState(newState) {
|
|
1857
|
+
const previousState = this.currentState;
|
|
1858
|
+
if (previousState === newState) {
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
this.currentState = newState;
|
|
1862
|
+
this.log("Network state changed", { from: previousState, to: newState });
|
|
1863
|
+
this.notifyListeners(newState);
|
|
1864
|
+
}
|
|
1865
|
+
notifyListeners(state) {
|
|
1866
|
+
for (const listener of this.listeners) {
|
|
1867
|
+
try {
|
|
1868
|
+
listener(state);
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
this.log("Listener error", error);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
isOnline() {
|
|
1875
|
+
if (!this.initialized || this.currentState === "unknown") {
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
return this.currentState === "online";
|
|
1879
|
+
}
|
|
1880
|
+
getState() {
|
|
1881
|
+
return this.currentState;
|
|
1882
|
+
}
|
|
1883
|
+
isInitialized() {
|
|
1884
|
+
return this.initialized;
|
|
1885
|
+
}
|
|
1886
|
+
addListener(listener) {
|
|
1887
|
+
this.listeners.add(listener);
|
|
1888
|
+
return () => this.listeners.delete(listener);
|
|
1889
|
+
}
|
|
1890
|
+
removeListener(listener) {
|
|
1891
|
+
this.listeners.delete(listener);
|
|
1892
|
+
}
|
|
1893
|
+
async forceCheck() {
|
|
1894
|
+
await this.checkConnectivity();
|
|
1895
|
+
return this.currentState;
|
|
1896
|
+
}
|
|
1897
|
+
dispose() {
|
|
1898
|
+
if (this.netInfoSubscription) {
|
|
1899
|
+
this.netInfoSubscription();
|
|
1900
|
+
this.netInfoSubscription = null;
|
|
1901
|
+
}
|
|
1902
|
+
if (this.appStateSubscription) {
|
|
1903
|
+
this.appStateSubscription.remove();
|
|
1904
|
+
this.appStateSubscription = null;
|
|
1905
|
+
}
|
|
1906
|
+
if (this.checkIntervalId) {
|
|
1907
|
+
clearInterval(this.checkIntervalId);
|
|
1908
|
+
this.checkIntervalId = null;
|
|
1909
|
+
}
|
|
1910
|
+
this.listeners.clear();
|
|
1911
|
+
this.initialized = false;
|
|
1912
|
+
this.currentState = "unknown";
|
|
1913
|
+
}
|
|
1914
|
+
log(message, data) {
|
|
1915
|
+
if (this.config.debug) {
|
|
1916
|
+
console.warn("[NetworkMonitor]", message, data ?? "");
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
setDebug(debug) {
|
|
1920
|
+
this.config.debug = debug;
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
var networkMonitor = new NetworkMonitorClass();
|
|
1924
|
+
|
|
1925
|
+
// src/core/queue/OfflineQueue.ts
|
|
1926
|
+
import AsyncStorage2 from "@react-native-async-storage/async-storage";
|
|
1927
|
+
|
|
1928
|
+
// src/core/queue/deduplication.ts
|
|
1929
|
+
function normalizeForComparison(obj) {
|
|
1930
|
+
if (obj === null || obj === void 0) {
|
|
1931
|
+
return String(obj);
|
|
1932
|
+
}
|
|
1933
|
+
if (typeof obj !== "object") {
|
|
1934
|
+
return JSON.stringify(obj);
|
|
1935
|
+
}
|
|
1936
|
+
if (Array.isArray(obj)) {
|
|
1937
|
+
return "[" + obj.map((item) => normalizeForComparison(item)).join(",") + "]";
|
|
1938
|
+
}
|
|
1939
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
1940
|
+
const pairs = sortedKeys.map((key) => {
|
|
1941
|
+
const value = obj[key];
|
|
1942
|
+
return `${JSON.stringify(key)}:${normalizeForComparison(value)}`;
|
|
1943
|
+
});
|
|
1944
|
+
return "{" + pairs.join(",") + "}";
|
|
1945
|
+
}
|
|
1946
|
+
function findDuplicateIndex(queue, item) {
|
|
1947
|
+
const normalizedBody = normalizeForComparison(item.body);
|
|
1948
|
+
return queue.findIndex(
|
|
1949
|
+
(existing) => existing.url === item.url && normalizeForComparison(existing.body) === normalizedBody
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/core/queue/types.ts
|
|
1954
|
+
var DEFAULT_QUEUE_CONFIG = {
|
|
1955
|
+
maxItems: 100,
|
|
1956
|
+
maxAge: 24 * 60 * 60 * 1e3,
|
|
1957
|
+
maxAttempts: 3,
|
|
1958
|
+
storageKey: "@paywallo:offline_queue",
|
|
1959
|
+
debug: false
|
|
1960
|
+
};
|
|
1961
|
+
|
|
1962
|
+
// src/core/queue/OfflineQueue.ts
|
|
1963
|
+
var OfflineQueueClass = class {
|
|
1964
|
+
constructor() {
|
|
1965
|
+
this.config = DEFAULT_QUEUE_CONFIG;
|
|
1966
|
+
this.queue = [];
|
|
1967
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1968
|
+
this.initialized = false;
|
|
1969
|
+
this.processing = false;
|
|
1970
|
+
this.processingLock = false;
|
|
1971
|
+
this.cleanupTimer = null;
|
|
1972
|
+
}
|
|
1973
|
+
async initialize(config) {
|
|
1974
|
+
if (this.initialized) {
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
this.config = { ...DEFAULT_QUEUE_CONFIG, ...config };
|
|
1978
|
+
await this.loadFromStorage();
|
|
1979
|
+
this.cleanupExpired();
|
|
1980
|
+
this.cleanupTimer = setInterval(() => this.cleanupExpired(), 60 * 60 * 1e3);
|
|
1981
|
+
this.initialized = true;
|
|
1982
|
+
this.log("OfflineQueue initialized", { queueSize: this.queue.length });
|
|
1983
|
+
}
|
|
1984
|
+
async loadFromStorage() {
|
|
1985
|
+
try {
|
|
1986
|
+
const stored = await AsyncStorage2.getItem(this.config.storageKey);
|
|
1987
|
+
if (stored) {
|
|
1988
|
+
this.queue = JSON.parse(stored);
|
|
1989
|
+
}
|
|
1990
|
+
} catch {
|
|
1991
|
+
this.queue = [];
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
async saveToStorage() {
|
|
1995
|
+
try {
|
|
1996
|
+
await AsyncStorage2.setItem(this.config.storageKey, JSON.stringify(this.queue));
|
|
1997
|
+
return true;
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
this.log("Failed to save queue to storage", error);
|
|
2000
|
+
this.emit({
|
|
2001
|
+
type: "item:failed",
|
|
2002
|
+
error: new ClientError(CLIENT_ERROR_CODES.UNKNOWN, "Storage save failed - items may be lost on restart"),
|
|
2003
|
+
queueSize: this.queue.length
|
|
2004
|
+
});
|
|
2005
|
+
return false;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
cleanupExpired() {
|
|
2009
|
+
const now = Date.now();
|
|
2010
|
+
const before = this.queue.length;
|
|
2011
|
+
this.queue = this.queue.filter((item) => Date.now() - item.timestamp < this.config.maxAge);
|
|
2012
|
+
if (before !== this.queue.length) {
|
|
2013
|
+
this.log("Cleaned up expired items", { removed: before - this.queue.length });
|
|
2014
|
+
void this.saveToStorage();
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
async enqueue(method, url, body, headers) {
|
|
2018
|
+
const item = {
|
|
2019
|
+
id: generateUUID(),
|
|
2020
|
+
method,
|
|
2021
|
+
url,
|
|
2022
|
+
body,
|
|
2023
|
+
headers,
|
|
2024
|
+
timestamp: Date.now(),
|
|
2025
|
+
attempts: 0,
|
|
2026
|
+
maxAttempts: this.config.maxAttempts
|
|
2027
|
+
};
|
|
2028
|
+
const existingIndex = this.findDuplicateIndex(item);
|
|
2029
|
+
if (existingIndex !== -1) {
|
|
2030
|
+
this.log("Duplicate item found, updating timestamp", { id: this.queue[existingIndex].id });
|
|
2031
|
+
this.queue[existingIndex].timestamp = Date.now();
|
|
2032
|
+
await this.saveToStorage();
|
|
2033
|
+
return this.queue[existingIndex];
|
|
2034
|
+
}
|
|
2035
|
+
if (this.queue.length >= this.config.maxItems) {
|
|
2036
|
+
const removed = this.queue.shift();
|
|
2037
|
+
if (removed) {
|
|
2038
|
+
this.log("Queue full, evicted oldest item:", removed.url);
|
|
2039
|
+
this.emit({ type: "item:evicted", item: removed, queueSize: this.queue.length });
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
this.queue.push(item);
|
|
2043
|
+
await this.saveToStorage();
|
|
2044
|
+
this.emit({ type: "item:added", item, queueSize: this.queue.length });
|
|
2045
|
+
this.log("Item enqueued", { id: item.id, url: item.url });
|
|
2046
|
+
return item;
|
|
2047
|
+
}
|
|
2048
|
+
findDuplicateIndex(item) {
|
|
2049
|
+
return findDuplicateIndex(this.queue, item);
|
|
2050
|
+
}
|
|
2051
|
+
async dequeue() {
|
|
2052
|
+
const item = this.queue.shift() ?? null;
|
|
2053
|
+
if (item) {
|
|
2054
|
+
await this.saveToStorage();
|
|
2055
|
+
this.emit({ type: "item:removed", item, queueSize: this.queue.length });
|
|
2056
|
+
}
|
|
2057
|
+
return item;
|
|
2058
|
+
}
|
|
2059
|
+
async removeItem(id) {
|
|
2060
|
+
const index = this.queue.findIndex((item2) => item2.id === id);
|
|
2061
|
+
if (index === -1) return false;
|
|
2062
|
+
const [item] = this.queue.splice(index, 1);
|
|
2063
|
+
await this.saveToStorage();
|
|
2064
|
+
this.emit({ type: "item:removed", item, queueSize: this.queue.length });
|
|
2065
|
+
return true;
|
|
2066
|
+
}
|
|
2067
|
+
async processQueue(processor, options) {
|
|
2068
|
+
if (this.processingLock || this.queue.length === 0) {
|
|
2069
|
+
return { processed: 0, failed: 0 };
|
|
2070
|
+
}
|
|
2071
|
+
this.processingLock = true;
|
|
2072
|
+
this.processing = true;
|
|
2073
|
+
this.emit({ type: "processing:start", queueSize: this.queue.length });
|
|
2074
|
+
this.log("Processing queue", { items: this.queue.length });
|
|
2075
|
+
let processed = 0;
|
|
2076
|
+
let failed = 0;
|
|
2077
|
+
const delayMs = options?.delayBetweenItems ?? 100;
|
|
2078
|
+
const itemIds = this.queue.map((item) => item.id);
|
|
2079
|
+
for (let i = 0; i < itemIds.length; i++) {
|
|
2080
|
+
const item = this.queue.find((q) => q.id === itemIds[i]);
|
|
2081
|
+
if (!item) continue;
|
|
2082
|
+
item.attempts++;
|
|
2083
|
+
try {
|
|
2084
|
+
const result = await processor(item);
|
|
2085
|
+
const success = typeof result === "boolean" ? result : result.success;
|
|
2086
|
+
const permanent = typeof result === "object" ? result.permanent ?? false : false;
|
|
2087
|
+
if (success) {
|
|
2088
|
+
await this.removeItem(item.id);
|
|
2089
|
+
processed++;
|
|
2090
|
+
this.emit({ type: "item:success", item, queueSize: this.queue.length });
|
|
2091
|
+
this.log("Item processed successfully", { id: item.id });
|
|
2092
|
+
} else {
|
|
2093
|
+
const didFail = await this.handleItemFailure(item, void 0, permanent);
|
|
2094
|
+
if (didFail) failed++;
|
|
2095
|
+
}
|
|
2096
|
+
} catch (error) {
|
|
2097
|
+
const err = error instanceof Error ? error : new ClientError(CLIENT_ERROR_CODES.UNKNOWN, String(error));
|
|
2098
|
+
const didFail = await this.handleItemFailure(item, err, false);
|
|
2099
|
+
if (didFail) failed++;
|
|
2100
|
+
}
|
|
2101
|
+
if (i < itemIds.length - 1 && delayMs > 0) await this.sleep(delayMs);
|
|
2102
|
+
}
|
|
2103
|
+
this.processing = false;
|
|
2104
|
+
this.processingLock = false;
|
|
2105
|
+
this.emit({ type: "processing:end", queueSize: this.queue.length });
|
|
2106
|
+
this.log("Queue processing complete", { processed, failed, remaining: this.queue.length });
|
|
2107
|
+
return { processed, failed };
|
|
2108
|
+
}
|
|
2109
|
+
/** Returns true if the item was permanently removed (counted as failed). */
|
|
2110
|
+
async handleItemFailure(item, error, permanent = false) {
|
|
2111
|
+
if (permanent || item.attempts >= item.maxAttempts) {
|
|
2112
|
+
await this.removeItem(item.id);
|
|
2113
|
+
this.emit({
|
|
2114
|
+
type: "item:failed",
|
|
2115
|
+
item,
|
|
2116
|
+
error: error ?? new ClientError(CLIENT_ERROR_CODES.UNKNOWN, permanent ? "Permanent failure" : "Max attempts reached"),
|
|
2117
|
+
queueSize: this.queue.length
|
|
2118
|
+
});
|
|
2119
|
+
this.log(
|
|
2120
|
+
permanent ? "Item failed permanently - removed" : "Item failed after max attempts",
|
|
2121
|
+
{ id: item.id, attempts: item.attempts }
|
|
2122
|
+
);
|
|
2123
|
+
return true;
|
|
2124
|
+
}
|
|
2125
|
+
await this.saveToStorage();
|
|
2126
|
+
this.log("Item will be retried", { id: item.id, attempts: item.attempts });
|
|
2127
|
+
return false;
|
|
2128
|
+
}
|
|
2129
|
+
sleep(ms) {
|
|
2130
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2131
|
+
}
|
|
2132
|
+
getQueue() {
|
|
2133
|
+
return [...this.queue];
|
|
2134
|
+
}
|
|
2135
|
+
getSize() {
|
|
2136
|
+
return this.queue.length;
|
|
2137
|
+
}
|
|
2138
|
+
isEmpty() {
|
|
2139
|
+
return this.queue.length === 0;
|
|
2140
|
+
}
|
|
2141
|
+
isProcessing() {
|
|
2142
|
+
return this.processing;
|
|
2143
|
+
}
|
|
2144
|
+
async clear() {
|
|
2145
|
+
this.queue = [];
|
|
2146
|
+
await this.saveToStorage();
|
|
2147
|
+
this.log("Queue cleared");
|
|
2148
|
+
}
|
|
2149
|
+
async clearItemsWithInvalidAppKey(validAppKey) {
|
|
2150
|
+
const before = this.queue.length;
|
|
2151
|
+
this.queue = this.queue.filter((item) => {
|
|
2152
|
+
const itemAppKey = item.headers?.["X-App-Key"];
|
|
2153
|
+
return itemAppKey === validAppKey || !itemAppKey;
|
|
2154
|
+
});
|
|
2155
|
+
const removed = before - this.queue.length;
|
|
2156
|
+
if (removed > 0) {
|
|
2157
|
+
await this.saveToStorage();
|
|
2158
|
+
this.log("Cleared items with invalid app key", { removed, validAppKey });
|
|
2159
|
+
}
|
|
2160
|
+
return removed;
|
|
2161
|
+
}
|
|
2162
|
+
addListener(listener) {
|
|
2163
|
+
this.listeners.add(listener);
|
|
2164
|
+
return () => this.listeners.delete(listener);
|
|
2165
|
+
}
|
|
2166
|
+
removeListener(listener) {
|
|
2167
|
+
this.listeners.delete(listener);
|
|
2168
|
+
}
|
|
2169
|
+
emit(event) {
|
|
2170
|
+
for (const listener of this.listeners) {
|
|
2171
|
+
try {
|
|
2172
|
+
listener(event);
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
this.log("Listener error", error);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
log(message, data) {
|
|
2179
|
+
if (this.config.debug) {
|
|
2180
|
+
console.warn("[OfflineQueue]", message, data ?? "");
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
setDebug(debug) {
|
|
2184
|
+
this.config.debug = debug;
|
|
2185
|
+
}
|
|
2186
|
+
dispose() {
|
|
2187
|
+
if (this.cleanupTimer) {
|
|
2188
|
+
clearInterval(this.cleanupTimer);
|
|
2189
|
+
this.cleanupTimer = null;
|
|
2190
|
+
}
|
|
2191
|
+
this.listeners.clear();
|
|
2192
|
+
this.queue = [];
|
|
2193
|
+
this.initialized = false;
|
|
2194
|
+
this.processing = false;
|
|
2195
|
+
this.processingLock = false;
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
var offlineQueue = new OfflineQueueClass();
|
|
2199
|
+
|
|
2200
|
+
// src/core/queue/QueueProcessor.ts
|
|
2201
|
+
var DEFAULT_PROCESSOR_CONFIG = {
|
|
2202
|
+
debounceMs: 2e3,
|
|
2203
|
+
debug: false
|
|
2204
|
+
};
|
|
2205
|
+
var QueueProcessorClass = class {
|
|
2206
|
+
constructor() {
|
|
2207
|
+
this.config = DEFAULT_PROCESSOR_CONFIG;
|
|
2208
|
+
this.httpClient = null;
|
|
2209
|
+
this.networkUnsubscribe = null;
|
|
2210
|
+
this.debounceTimeout = null;
|
|
2211
|
+
this.retryIntervalId = null;
|
|
2212
|
+
this.initialized = false;
|
|
2213
|
+
}
|
|
2214
|
+
initialize(httpClient, config) {
|
|
2215
|
+
if (this.initialized) {
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
this.httpClient = httpClient;
|
|
2219
|
+
this.config = { ...DEFAULT_PROCESSOR_CONFIG, ...config };
|
|
2220
|
+
this.networkUnsubscribe = networkMonitor.addListener((state) => {
|
|
2221
|
+
if (state === "online") {
|
|
2222
|
+
this.scheduleProcessing();
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
if (networkMonitor.isOnline() && !offlineQueue.isEmpty()) {
|
|
2226
|
+
this.scheduleProcessing();
|
|
2227
|
+
}
|
|
2228
|
+
this.retryIntervalId = setInterval(() => {
|
|
2229
|
+
if (networkMonitor.isOnline() && !offlineQueue.isEmpty()) {
|
|
2230
|
+
this.scheduleProcessing();
|
|
2231
|
+
}
|
|
2232
|
+
}, 6e4);
|
|
2233
|
+
this.initialized = true;
|
|
2234
|
+
this.log("QueueProcessor initialized");
|
|
2235
|
+
}
|
|
2236
|
+
scheduleProcessing() {
|
|
2237
|
+
if (this.debounceTimeout) {
|
|
2238
|
+
clearTimeout(this.debounceTimeout);
|
|
2239
|
+
}
|
|
2240
|
+
this.debounceTimeout = setTimeout(() => {
|
|
2241
|
+
void this.processQueue();
|
|
2242
|
+
}, this.config.debounceMs);
|
|
2243
|
+
}
|
|
2244
|
+
async processQueue() {
|
|
2245
|
+
if (!this.httpClient) {
|
|
2246
|
+
this.log("HttpClient not initialized");
|
|
2247
|
+
return { processed: 0, failed: 0 };
|
|
2248
|
+
}
|
|
2249
|
+
if (!networkMonitor.isOnline()) {
|
|
2250
|
+
this.log("Offline, skipping queue processing");
|
|
2251
|
+
return { processed: 0, failed: 0 };
|
|
2252
|
+
}
|
|
2253
|
+
return offlineQueue.processQueue(async (item) => {
|
|
2254
|
+
return this.processItem(item);
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
async processItem(item) {
|
|
2258
|
+
if (!this.httpClient) {
|
|
2259
|
+
return { success: false, permanent: false };
|
|
2260
|
+
}
|
|
2261
|
+
this.log("Processing queue item", {
|
|
2262
|
+
id: item.id,
|
|
2263
|
+
url: item.url,
|
|
2264
|
+
method: item.method,
|
|
2265
|
+
headers: item.headers,
|
|
2266
|
+
attempts: item.attempts
|
|
2267
|
+
});
|
|
2268
|
+
const response = await this.httpClient.request(item.url, {
|
|
2269
|
+
method: item.method,
|
|
2270
|
+
body: item.body,
|
|
2271
|
+
headers: item.headers,
|
|
2272
|
+
skipRetry: false
|
|
2273
|
+
});
|
|
2274
|
+
if (response.ok) {
|
|
2275
|
+
this.log("Queue item processed successfully", { id: item.id });
|
|
2276
|
+
return { success: true, permanent: false };
|
|
2277
|
+
}
|
|
2278
|
+
if (response.status >= 400 && response.status < 500) {
|
|
2279
|
+
this.log("Request failed with permanent client error - removing from queue", {
|
|
2280
|
+
id: item.id,
|
|
2281
|
+
url: item.url,
|
|
2282
|
+
status: response.status,
|
|
2283
|
+
appKeyInHeaders: item.headers?.["X-App-Key"]
|
|
2284
|
+
});
|
|
2285
|
+
return { success: false, permanent: true, status: response.status };
|
|
2286
|
+
}
|
|
2287
|
+
this.log("Request failed with server error - will retry", {
|
|
2288
|
+
id: item.id,
|
|
2289
|
+
status: response.status
|
|
2290
|
+
});
|
|
2291
|
+
return { success: false, permanent: false, status: response.status };
|
|
2292
|
+
}
|
|
2293
|
+
async enqueueIfOffline(method, url, body, headers) {
|
|
2294
|
+
if (networkMonitor.isOnline()) {
|
|
2295
|
+
return { queued: false };
|
|
2296
|
+
}
|
|
2297
|
+
const item = await offlineQueue.enqueue(method, url, body, headers);
|
|
2298
|
+
this.log("Request queued for offline", { id: item.id, url });
|
|
2299
|
+
return { queued: true, item };
|
|
2300
|
+
}
|
|
2301
|
+
getQueueSize() {
|
|
2302
|
+
return offlineQueue.getSize();
|
|
2303
|
+
}
|
|
2304
|
+
async clearQueue() {
|
|
2305
|
+
await offlineQueue.clear();
|
|
2306
|
+
}
|
|
2307
|
+
dispose() {
|
|
2308
|
+
if (this.debounceTimeout) {
|
|
2309
|
+
clearTimeout(this.debounceTimeout);
|
|
2310
|
+
this.debounceTimeout = null;
|
|
2311
|
+
}
|
|
2312
|
+
if (this.retryIntervalId) {
|
|
2313
|
+
clearInterval(this.retryIntervalId);
|
|
2314
|
+
this.retryIntervalId = null;
|
|
2315
|
+
}
|
|
2316
|
+
if (this.networkUnsubscribe) {
|
|
2317
|
+
this.networkUnsubscribe();
|
|
2318
|
+
this.networkUnsubscribe = null;
|
|
2319
|
+
}
|
|
2320
|
+
this.httpClient = null;
|
|
2321
|
+
this.initialized = false;
|
|
2322
|
+
}
|
|
2323
|
+
log(message, data) {
|
|
2324
|
+
if (this.config.debug) {
|
|
2325
|
+
console.warn("[QueueProcessor]", message, data ?? "");
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
setDebug(debug) {
|
|
2329
|
+
this.config.debug = debug;
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
var queueProcessor = new QueueProcessorClass();
|
|
2333
|
+
|
|
2334
|
+
// src/core/ApiCache.ts
|
|
2335
|
+
var ApiCache = class {
|
|
2336
|
+
constructor() {
|
|
2337
|
+
this.CACHE_TTL = 3e5;
|
|
2338
|
+
// 5 minutes
|
|
2339
|
+
this.MAX_SIZE = 200;
|
|
2340
|
+
this.variantCache = /* @__PURE__ */ new Map();
|
|
2341
|
+
this.paywallCache = /* @__PURE__ */ new Map();
|
|
2342
|
+
this.campaignCache = /* @__PURE__ */ new Map();
|
|
2343
|
+
}
|
|
2344
|
+
getCached(cache, key) {
|
|
2345
|
+
const entry = cache.get(key);
|
|
2346
|
+
if (!entry) return void 0;
|
|
2347
|
+
if (Date.now() - entry.ts < this.CACHE_TTL) return entry.value;
|
|
2348
|
+
cache.delete(key);
|
|
2349
|
+
return void 0;
|
|
2350
|
+
}
|
|
2351
|
+
setCached(cache, key, value) {
|
|
2352
|
+
cache.set(key, { value, ts: Date.now() });
|
|
2353
|
+
}
|
|
2354
|
+
evictExpiredEntries(cache) {
|
|
2355
|
+
if (cache.size <= this.MAX_SIZE) return;
|
|
2356
|
+
const now = Date.now();
|
|
2357
|
+
for (const [key, entry] of cache) {
|
|
2358
|
+
if (now - entry.ts > this.CACHE_TTL) cache.delete(key);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
getVariant(key) {
|
|
2362
|
+
return this.getCached(this.variantCache, key);
|
|
2363
|
+
}
|
|
2364
|
+
setVariant(key, value) {
|
|
2365
|
+
this.setCached(this.variantCache, key, value);
|
|
2366
|
+
this.evictExpiredEntries(this.variantCache);
|
|
2367
|
+
}
|
|
2368
|
+
getStaleVariant(key) {
|
|
2369
|
+
const entry = this.variantCache.get(key);
|
|
2370
|
+
if (entry && Date.now() - entry.ts < 864e5) return entry.value;
|
|
2371
|
+
return void 0;
|
|
2372
|
+
}
|
|
2373
|
+
getPaywall(key) {
|
|
2374
|
+
return this.getCached(this.paywallCache, key);
|
|
2375
|
+
}
|
|
2376
|
+
setPaywall(key, value) {
|
|
2377
|
+
this.setCached(this.paywallCache, key, value);
|
|
2378
|
+
this.evictExpiredEntries(this.paywallCache);
|
|
2379
|
+
}
|
|
2380
|
+
getStalePaywall(key) {
|
|
2381
|
+
const entry = this.paywallCache.get(key);
|
|
2382
|
+
if (entry && Date.now() - entry.ts < 864e5) return entry.value;
|
|
2383
|
+
return void 0;
|
|
2384
|
+
}
|
|
2385
|
+
getCampaign(key) {
|
|
2386
|
+
return this.getCached(this.campaignCache, key);
|
|
2387
|
+
}
|
|
2388
|
+
setCampaign(key, value) {
|
|
2389
|
+
this.setCached(this.campaignCache, key, value);
|
|
2390
|
+
this.evictExpiredEntries(this.campaignCache);
|
|
2391
|
+
}
|
|
2392
|
+
getStaleCampaign(key) {
|
|
2393
|
+
const entry = this.campaignCache.get(key);
|
|
2394
|
+
if (entry && Date.now() - entry.ts < 864e5) return entry.value;
|
|
2395
|
+
return void 0;
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
|
|
2399
|
+
// src/core/ApiClient.ts
|
|
2400
|
+
var SDK_VERSION = "1.0.0";
|
|
2401
|
+
var DEFAULT_BASE_URL = "https://panel.lucasqueiroga.shop";
|
|
2402
|
+
var ApiClient = class {
|
|
2403
|
+
constructor(appKey, baseUrl, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
|
|
2404
|
+
this.cache = new ApiCache();
|
|
2405
|
+
this.appKey = appKey;
|
|
2406
|
+
this.baseUrl = baseUrl ?? DEFAULT_BASE_URL;
|
|
2407
|
+
this.webUrl = deriveWebUrl(this.baseUrl);
|
|
2408
|
+
this.debug = debug;
|
|
2409
|
+
this.environment = environment;
|
|
2410
|
+
this.offlineQueueEnabled = offlineQueueEnabled;
|
|
2411
|
+
this.httpClient = new HttpClient(this.baseUrl, { timeout, debug });
|
|
2412
|
+
this.log("ApiClient initialized", { baseUrl: this.baseUrl, environment, offlineQueueEnabled });
|
|
2413
|
+
}
|
|
2414
|
+
// ─── Infrastructure ──────────────────────────────────────────────────────────
|
|
2415
|
+
/** Exposed only for QueueProcessor initialization. Prefer get()/post() for requests. */
|
|
2416
|
+
getHttpClient() {
|
|
2417
|
+
return this.httpClient;
|
|
2418
|
+
}
|
|
2419
|
+
getWebUrl() {
|
|
2420
|
+
return this.webUrl;
|
|
2421
|
+
}
|
|
2422
|
+
getEnvironment() {
|
|
2423
|
+
return this.environment;
|
|
2424
|
+
}
|
|
2425
|
+
getQueueSize() {
|
|
2426
|
+
return offlineQueue.getSize();
|
|
2427
|
+
}
|
|
2428
|
+
setofflineQueueEnabled(enabled) {
|
|
2429
|
+
this.offlineQueueEnabled = enabled;
|
|
2430
|
+
}
|
|
2431
|
+
isofflineQueueEnabled() {
|
|
2432
|
+
return this.offlineQueueEnabled;
|
|
2433
|
+
}
|
|
2434
|
+
setEnvironment(environment) {
|
|
2435
|
+
this.environment = environment;
|
|
2436
|
+
this.log("Environment set to:", environment);
|
|
2437
|
+
}
|
|
2438
|
+
setDebug(debug) {
|
|
2439
|
+
this.debug = debug;
|
|
2440
|
+
this.httpClient.setDebug(debug);
|
|
2441
|
+
}
|
|
2442
|
+
async clearQueue() {
|
|
2443
|
+
await offlineQueue.clear();
|
|
2444
|
+
}
|
|
2445
|
+
// ─── Public HTTP facade (headers injected automatically) ─────────────────────
|
|
2446
|
+
async get(path) {
|
|
2447
|
+
return this.httpClient.get(path, { headers: { "X-App-Key": this.appKey } });
|
|
2448
|
+
}
|
|
2449
|
+
async post(path, body, skipRetry = false) {
|
|
2450
|
+
return this.httpClient.post(path, body, { headers: { "X-App-Key": this.appKey }, skipRetry });
|
|
2451
|
+
}
|
|
2452
|
+
async reportError(errorType, message, context) {
|
|
2453
|
+
try {
|
|
2454
|
+
await this.post("/api/v1/sdk/errors", {
|
|
2455
|
+
errorType,
|
|
2456
|
+
message,
|
|
2457
|
+
context,
|
|
2458
|
+
sdkVersion: SDK_VERSION,
|
|
2459
|
+
platform: Platform3.OS
|
|
2460
|
+
}, true);
|
|
2461
|
+
} catch {
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
// ─── Business methods ────────────────────────────────────────────────────────
|
|
2465
|
+
async trackEvent(eventName, distinctId, properties, timestamp) {
|
|
2466
|
+
this.log("Tracking event:", eventName);
|
|
2467
|
+
await this.postWithQueue("/api/v1/events", {
|
|
2468
|
+
eventName,
|
|
2469
|
+
distinctId,
|
|
2470
|
+
properties: { ...properties, platform: Platform3.OS },
|
|
2471
|
+
timestamp: timestamp ?? Date.now(),
|
|
2472
|
+
environment: this.environment.toLowerCase()
|
|
2473
|
+
}, `event:${eventName}`);
|
|
2474
|
+
}
|
|
2475
|
+
async identify(distinctId, properties, email) {
|
|
2476
|
+
await this.postWithQueue("/api/v1/identify", { distinctId, properties: properties ?? {}, email, platform: Platform3.OS }, "identify");
|
|
2477
|
+
this.log("Identity updated:", distinctId);
|
|
2478
|
+
}
|
|
2479
|
+
async startSession(distinctId, sessionId, platform) {
|
|
2480
|
+
await this.postWithQueue("/api/v1/sessions/start", {
|
|
2481
|
+
distinctId,
|
|
2482
|
+
sessionId,
|
|
2483
|
+
platform: platform ?? Platform3.OS,
|
|
2484
|
+
environment: this.environment.toLowerCase()
|
|
2485
|
+
}, `session.start:${sessionId}`);
|
|
2486
|
+
this.log("Session started:", sessionId);
|
|
2487
|
+
return { success: true };
|
|
2488
|
+
}
|
|
2489
|
+
async endSession(sessionId, durationSeconds) {
|
|
2490
|
+
await this.postWithQueue("/api/v1/sessions/end", { sessionId, durationSeconds }, `session.end:${sessionId}`);
|
|
2491
|
+
this.log("Session ended:", sessionId);
|
|
2492
|
+
return { success: true };
|
|
2493
|
+
}
|
|
2494
|
+
async getVariant(flagKey, distinctId) {
|
|
2495
|
+
const key = `${flagKey}:${distinctId}`;
|
|
2496
|
+
const hit = this.cache.getVariant(key);
|
|
2497
|
+
if (hit !== void 0) return hit;
|
|
2498
|
+
try {
|
|
2499
|
+
const url = new URL(`${this.baseUrl}/api/v1/flags/${flagKey}`);
|
|
2500
|
+
url.searchParams.set("distinctId", distinctId);
|
|
2501
|
+
const res = await this.get(url.toString());
|
|
2502
|
+
if (res.status === 404) {
|
|
2503
|
+
const v = { variant: null };
|
|
2504
|
+
this.cache.setVariant(key, v);
|
|
2505
|
+
return v;
|
|
2506
|
+
}
|
|
2507
|
+
this.log("Got variant:", flagKey, res.data);
|
|
2508
|
+
this.cache.setVariant(key, res.data);
|
|
2509
|
+
return res.data;
|
|
2510
|
+
} catch (error) {
|
|
2511
|
+
this.log("Failed to get variant:", flagKey, error);
|
|
2512
|
+
const stale = this.cache.getStaleVariant(key);
|
|
2513
|
+
if (stale !== void 0) return stale;
|
|
2514
|
+
return { variant: null };
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
async getPaywall(placement) {
|
|
2518
|
+
const hit = this.cache.getPaywall(placement);
|
|
2519
|
+
if (hit !== void 0) return hit;
|
|
2520
|
+
try {
|
|
2521
|
+
const res = await this.get(`/api/v1/paywalls/${placement}`);
|
|
2522
|
+
if (res.status === 404) {
|
|
2523
|
+
this.cache.setPaywall(placement, null);
|
|
2524
|
+
return null;
|
|
2525
|
+
}
|
|
2526
|
+
this.log("Got paywall:", placement);
|
|
2527
|
+
this.cache.setPaywall(placement, res.data);
|
|
2528
|
+
return res.data;
|
|
2529
|
+
} catch (error) {
|
|
2530
|
+
this.log("Failed to get paywall:", placement, error);
|
|
2531
|
+
const stale = this.cache.getStalePaywall(placement);
|
|
2532
|
+
if (stale !== void 0) return stale;
|
|
2533
|
+
return null;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
async getEmergencyPaywall() {
|
|
2537
|
+
const result = await getEmergencyPaywall(this);
|
|
2538
|
+
this.log("Got emergency paywall:", result.enabled);
|
|
2539
|
+
return result;
|
|
2540
|
+
}
|
|
2541
|
+
async getCampaign(placement, distinctId, context) {
|
|
2542
|
+
const key = `${placement}:${distinctId}`;
|
|
2543
|
+
const hit = this.cache.getCampaign(key);
|
|
2544
|
+
if (hit !== void 0) return hit;
|
|
2545
|
+
try {
|
|
2546
|
+
const result = await getCampaignData(this, this.baseUrl, placement, distinctId, context);
|
|
2547
|
+
if (!result) {
|
|
2548
|
+
this.log("No campaign found for placement:", placement);
|
|
2549
|
+
this.cache.setCampaign(key, null);
|
|
2550
|
+
return null;
|
|
2551
|
+
}
|
|
2552
|
+
this.log("Got campaign:", placement, result.variantKey);
|
|
2553
|
+
this.cache.setCampaign(key, result);
|
|
2554
|
+
return result;
|
|
2555
|
+
} catch (error) {
|
|
2556
|
+
this.log("Failed to get campaign:", placement, error);
|
|
2557
|
+
const stale = this.cache.getStaleCampaign(key);
|
|
2558
|
+
if (stale !== void 0) return stale;
|
|
2559
|
+
return null;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
async validatePurchase(platform, receipt, productId, transactionId, priceLocal, currency, country, distinctId, paywallPlacement, variantKey) {
|
|
2563
|
+
const result = await validatePurchase(
|
|
2564
|
+
this,
|
|
2565
|
+
this.appKey,
|
|
2566
|
+
platform,
|
|
2567
|
+
receipt,
|
|
2568
|
+
productId,
|
|
2569
|
+
transactionId,
|
|
2570
|
+
priceLocal,
|
|
2571
|
+
currency,
|
|
2572
|
+
country,
|
|
2573
|
+
distinctId,
|
|
2574
|
+
paywallPlacement,
|
|
2575
|
+
variantKey,
|
|
2576
|
+
this.debug
|
|
2577
|
+
);
|
|
2578
|
+
this.log("Purchase validated:", result.success);
|
|
2579
|
+
return result;
|
|
2580
|
+
}
|
|
2581
|
+
async getSubscriptionStatus(distinctId) {
|
|
2582
|
+
const result = await getSubscriptionStatus(this, this.baseUrl, this.appKey, distinctId);
|
|
2583
|
+
this.log("Subscription status:", result.hasActiveSubscription);
|
|
2584
|
+
return result;
|
|
2585
|
+
}
|
|
2586
|
+
async getConditionalFlag(flagKey, context) {
|
|
2587
|
+
try {
|
|
2588
|
+
const url = buildConditionalFlagUrl(this.baseUrl, flagKey, context);
|
|
2589
|
+
const response = await this.get(url);
|
|
2590
|
+
if (response.status === 404) return { value: false, flagKey };
|
|
2591
|
+
this.log("Got conditional flag:", flagKey, response.data.value);
|
|
2592
|
+
return { value: response.data.value, flagKey };
|
|
2593
|
+
} catch {
|
|
2594
|
+
this.log("Failed to get conditional flag:", flagKey);
|
|
2595
|
+
return { value: false, flagKey };
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
// ─── Private helpers ─────────────────────────────────────────────────────────
|
|
2599
|
+
/**
|
|
2600
|
+
* POST with offline queue fallback. Enqueues when offline or on network failure.
|
|
2601
|
+
* Throws only when offlineQueueEnabled is false and the request fails.
|
|
2602
|
+
*/
|
|
2603
|
+
async postWithQueue(url, payload, label) {
|
|
2604
|
+
if (this.offlineQueueEnabled && !networkMonitor.isOnline()) {
|
|
2605
|
+
await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
|
|
2606
|
+
this.log("Queued for offline:", label);
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
try {
|
|
2610
|
+
await this.post(url, payload);
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
this.log("Request failed:", label, error);
|
|
2613
|
+
if (this.offlineQueueEnabled) {
|
|
2614
|
+
await offlineQueue.enqueue("POST", url, payload, { "X-App-Key": this.appKey });
|
|
2615
|
+
this.log("Queued after failure:", label);
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
throw error;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
log(...args) {
|
|
2622
|
+
if (this.debug) console.warn("[Panel API]", ...args);
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
|
|
2626
|
+
// src/domains/campaign/CampaignError.ts
|
|
2627
|
+
var CampaignError = class extends PaywalloError {
|
|
2628
|
+
constructor(code, message) {
|
|
2629
|
+
super("campaign", code, message);
|
|
2630
|
+
this.name = "CampaignError";
|
|
2631
|
+
}
|
|
2632
|
+
};
|
|
2633
|
+
var CAMPAIGN_ERROR_CODES = {
|
|
2634
|
+
NOT_INITIALIZED: "CAMPAIGN_NOT_INITIALIZED",
|
|
2635
|
+
FETCH_FAILED: "CAMPAIGN_FETCH_FAILED",
|
|
2636
|
+
PRELOAD_FAILED: "CAMPAIGN_PRELOAD_FAILED",
|
|
2637
|
+
NOT_FOUND: "CAMPAIGN_NOT_FOUND",
|
|
2638
|
+
PRESENT_FAILED: "CAMPAIGN_PRESENT_FAILED"
|
|
2639
|
+
};
|
|
2640
|
+
|
|
2641
|
+
// src/domains/campaign/CampaignGateService.ts
|
|
2642
|
+
var PRELOAD_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2643
|
+
var CampaignGateService = class {
|
|
2644
|
+
constructor() {
|
|
2645
|
+
this.preloadedCampaigns = /* @__PURE__ */ new Map();
|
|
2646
|
+
}
|
|
2647
|
+
async getCampaign(apiClient, placement, context) {
|
|
2648
|
+
const distinctId = identityManager.getDistinctId();
|
|
2649
|
+
if (!distinctId) return null;
|
|
2650
|
+
return apiClient.getCampaign(placement, distinctId, context);
|
|
2651
|
+
}
|
|
2652
|
+
async presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context) {
|
|
2653
|
+
const campaign = await this.getCampaign(apiClient, placement, context);
|
|
2654
|
+
if (!campaign) {
|
|
2655
|
+
return { presented: false, purchased: false, cancelled: false, restored: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, "No campaign found") };
|
|
2656
|
+
}
|
|
2657
|
+
if (campaignPresenter) return campaignPresenter(campaign);
|
|
2658
|
+
if (!paywallPresenter) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
|
|
2659
|
+
return { ...await paywallPresenter(campaign.paywall.placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
|
|
2660
|
+
}
|
|
2661
|
+
async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
|
|
2662
|
+
if (await hasActive()) return true;
|
|
2663
|
+
const r = await this.presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context);
|
|
2664
|
+
return r.purchased || r.restored;
|
|
2665
|
+
}
|
|
2666
|
+
async preloadCampaign(apiClient, placement, context) {
|
|
2667
|
+
try {
|
|
2668
|
+
const campaign = await this.getCampaign(apiClient, placement, context);
|
|
2669
|
+
if (!campaign?.paywall) {
|
|
2670
|
+
return { success: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, `Campaign or paywall not found: ${placement}`) };
|
|
2671
|
+
}
|
|
2672
|
+
this.preloadedCampaigns.set(placement, { campaign, preloadedAt: Date.now() });
|
|
2673
|
+
return { success: true };
|
|
2674
|
+
} catch (error) {
|
|
2675
|
+
return { success: false, error: error instanceof Error ? error : new PaywalloError("panel", "PANEL_UNKNOWN", String(error)) };
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
getPreloadedCampaign(placement) {
|
|
2679
|
+
const cached = this.preloadedCampaigns.get(placement);
|
|
2680
|
+
if (!cached) return null;
|
|
2681
|
+
if (Date.now() - cached.preloadedAt >= PRELOAD_CACHE_TTL_MS) {
|
|
2682
|
+
this.preloadedCampaigns.delete(placement);
|
|
2683
|
+
return null;
|
|
2684
|
+
}
|
|
2685
|
+
return cached.campaign;
|
|
2686
|
+
}
|
|
2687
|
+
clearPreloaded() {
|
|
2688
|
+
this.preloadedCampaigns.clear();
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
var campaignGateService = new CampaignGateService();
|
|
2692
|
+
|
|
2693
|
+
// src/domains/identity/AdvertisingIdManager.ts
|
|
2694
|
+
import { Platform as Platform4 } from "react-native";
|
|
2695
|
+
var ZERO_IDFA = "00000000-0000-0000-0000-000000000000";
|
|
2696
|
+
var AdvertisingIdManager = class {
|
|
2697
|
+
constructor() {
|
|
2698
|
+
this.cached = null;
|
|
2699
|
+
}
|
|
2700
|
+
async collect(requestATT = false) {
|
|
2701
|
+
if (this.cached) return this.cached;
|
|
2702
|
+
const fallback = { idfv: null, idfa: null, gaid: null, attStatus: "unavailable" };
|
|
2703
|
+
try {
|
|
2704
|
+
const tracking = await import("expo-tracking-transparency");
|
|
2705
|
+
if (Platform4.OS === "ios") {
|
|
2706
|
+
if (!tracking.isAvailable()) {
|
|
2707
|
+
const idfv2 = await this.collectIdfv();
|
|
2708
|
+
this.cached = { ...fallback, idfv: idfv2 };
|
|
2709
|
+
return this.cached;
|
|
2710
|
+
}
|
|
2711
|
+
const current = await tracking.getTrackingPermissionsAsync();
|
|
2712
|
+
let status = current.status;
|
|
2713
|
+
let granted = current.granted;
|
|
2714
|
+
if (status === "undetermined" && requestATT) {
|
|
2715
|
+
const result = await tracking.requestTrackingPermissionsAsync();
|
|
2716
|
+
status = result.status;
|
|
2717
|
+
granted = result.granted;
|
|
2718
|
+
}
|
|
2719
|
+
const idfv = await this.collectIdfv();
|
|
2720
|
+
const idfa = granted ? this.sanitizeId(tracking.getAdvertisingId()) : null;
|
|
2721
|
+
this.cached = { idfv, idfa, gaid: null, attStatus: status };
|
|
2722
|
+
return this.cached;
|
|
2723
|
+
}
|
|
2724
|
+
if (Platform4.OS === "android") {
|
|
2725
|
+
const androidStatus = await tracking.getTrackingPermissionsAsync();
|
|
2726
|
+
const optedIn = androidStatus.granted;
|
|
2727
|
+
const rawGaid = optedIn ? tracking.getAdvertisingId() : null;
|
|
2728
|
+
const gaid = this.sanitizeId(rawGaid);
|
|
2729
|
+
this.cached = { idfv: null, idfa: null, gaid, attStatus: "unavailable" };
|
|
2730
|
+
return this.cached;
|
|
2731
|
+
}
|
|
2732
|
+
this.cached = fallback;
|
|
2733
|
+
return this.cached;
|
|
2734
|
+
} catch {
|
|
2735
|
+
this.cached = fallback;
|
|
2736
|
+
return this.cached;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
async collectIdfv() {
|
|
2740
|
+
try {
|
|
2741
|
+
const app = await import("expo-application");
|
|
2742
|
+
const idfv = await app.getIosIdForVendorAsync();
|
|
2743
|
+
return this.sanitizeId(idfv);
|
|
2744
|
+
} catch {
|
|
2745
|
+
return null;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
sanitizeId(id) {
|
|
2749
|
+
if (!id || id === ZERO_IDFA) return null;
|
|
2750
|
+
return id;
|
|
2751
|
+
}
|
|
2752
|
+
};
|
|
2753
|
+
|
|
2754
|
+
// src/domains/identity/FacebookIdManager.ts
|
|
2755
|
+
var TIMEOUT_MS = 2e3;
|
|
2756
|
+
var FacebookIdManager = class {
|
|
2757
|
+
async getAnonId() {
|
|
2758
|
+
try {
|
|
2759
|
+
let timerId;
|
|
2760
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
2761
|
+
timerId = setTimeout(() => resolve(null), TIMEOUT_MS);
|
|
2762
|
+
});
|
|
2763
|
+
const result = await Promise.race([this.fetchAnonId(), timeoutPromise]);
|
|
2764
|
+
clearTimeout(timerId);
|
|
2765
|
+
return result;
|
|
2766
|
+
} catch {
|
|
2767
|
+
return null;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
async fetchAnonId() {
|
|
2771
|
+
try {
|
|
2772
|
+
const fbsdk = await import("react-native-fbsdk-next");
|
|
2773
|
+
const anonId = await fbsdk.AppEventsLogger.getAnonymousID();
|
|
2774
|
+
return anonId || null;
|
|
2775
|
+
} catch {
|
|
2776
|
+
return null;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
};
|
|
2780
|
+
var facebookIdManager = new FacebookIdManager();
|
|
2781
|
+
|
|
2782
|
+
// src/domains/identity/InstallTracker.ts
|
|
2783
|
+
import { Dimensions as Dimensions3, Platform as Platform6 } from "react-native";
|
|
2784
|
+
|
|
2785
|
+
// src/domains/identity/InstallReferrerManager.ts
|
|
2786
|
+
import { Platform as Platform5 } from "react-native";
|
|
2787
|
+
var REFERRER_TIMEOUT_MS = 5e3;
|
|
2788
|
+
var InstallReferrerManager = class {
|
|
2789
|
+
async getReferrer() {
|
|
2790
|
+
if (Platform5.OS !== "android") {
|
|
2791
|
+
return this.emptyResult();
|
|
2792
|
+
}
|
|
2793
|
+
let timerId;
|
|
2794
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
2795
|
+
timerId = setTimeout(() => resolve(this.emptyResult()), REFERRER_TIMEOUT_MS);
|
|
2796
|
+
});
|
|
2797
|
+
const fetchPromise = this.fetchReferrer();
|
|
2798
|
+
const result = await Promise.race([fetchPromise, timeoutPromise]);
|
|
2799
|
+
clearTimeout(timerId);
|
|
2800
|
+
return result;
|
|
2801
|
+
}
|
|
2802
|
+
async fetchReferrer() {
|
|
2803
|
+
try {
|
|
2804
|
+
const Application = await import("expo-application");
|
|
2805
|
+
const referrer = await Application.getInstallReferrerAsync();
|
|
2806
|
+
if (!referrer) return this.emptyResult();
|
|
2807
|
+
const capped = referrer.slice(0, 2048);
|
|
2808
|
+
const params = this.parseReferrer(capped);
|
|
2809
|
+
return {
|
|
2810
|
+
rawReferrer: capped,
|
|
2811
|
+
trackingId: params["tracking_id"] || null,
|
|
2812
|
+
fbclid: params["fbclid"] || null,
|
|
2813
|
+
utmSource: params["utm_source"] || null,
|
|
2814
|
+
utmMedium: params["utm_medium"] || null,
|
|
2815
|
+
utmCampaign: params["utm_campaign"] || null
|
|
2816
|
+
};
|
|
2817
|
+
} catch {
|
|
2818
|
+
return this.emptyResult();
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
parseReferrer(referrer) {
|
|
2822
|
+
const result = {};
|
|
2823
|
+
for (const pair of referrer.split("&")) {
|
|
2824
|
+
const eqIndex = pair.indexOf("=");
|
|
2825
|
+
if (eqIndex === -1) continue;
|
|
2826
|
+
try {
|
|
2827
|
+
const key = decodeURIComponent(pair.slice(0, eqIndex).replace(/\+/g, " "));
|
|
2828
|
+
const raw = pair.slice(eqIndex + 1).replace(/\+/g, " ");
|
|
2829
|
+
const value = decodeURIComponent(raw);
|
|
2830
|
+
if (key) result[key] = value;
|
|
2831
|
+
} catch {
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
return result;
|
|
2835
|
+
}
|
|
2836
|
+
emptyResult() {
|
|
2837
|
+
return {
|
|
2838
|
+
rawReferrer: null,
|
|
2839
|
+
trackingId: null,
|
|
2840
|
+
fbclid: null,
|
|
2841
|
+
utmSource: null,
|
|
2842
|
+
utmMedium: null,
|
|
2843
|
+
utmCampaign: null
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
var installReferrerManager = new InstallReferrerManager();
|
|
2848
|
+
|
|
2849
|
+
// src/domains/session/SessionManager.ts
|
|
2850
|
+
import { AppState as AppState2 } from "react-native";
|
|
2851
|
+
|
|
2852
|
+
// src/domains/session/SessionError.ts
|
|
2853
|
+
var SessionError = class extends PaywalloError {
|
|
2854
|
+
constructor(code, message) {
|
|
2855
|
+
super("session", code, message);
|
|
2856
|
+
this.name = "SessionError";
|
|
2857
|
+
}
|
|
2858
|
+
};
|
|
2859
|
+
var SESSION_ERROR_CODES = {
|
|
2860
|
+
NOT_INITIALIZED: "SESSION_NOT_INITIALIZED",
|
|
2861
|
+
START_FAILED: "SESSION_START_FAILED",
|
|
2862
|
+
END_FAILED: "SESSION_END_FAILED",
|
|
2863
|
+
RESTORE_FAILED: "SESSION_RESTORE_FAILED"
|
|
2864
|
+
};
|
|
2865
|
+
|
|
2866
|
+
// src/domains/session/sessionTracking.ts
|
|
2867
|
+
async function trackSessionStart(apiClient, sessionId, sessionStartedAt) {
|
|
2868
|
+
const distinctId = identityManager.getDistinctId();
|
|
2869
|
+
await apiClient.trackEvent("$session_start", distinctId, {
|
|
2870
|
+
sessionId,
|
|
2871
|
+
timestamp: sessionStartedAt?.toISOString() ?? null
|
|
2872
|
+
}).catch(() => {
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
async function trackSessionEnd(apiClient, sessionId, durationSeconds, sessionStartedAt) {
|
|
2876
|
+
const distinctId = identityManager.getDistinctId();
|
|
2877
|
+
await apiClient.trackEvent("$session_end", distinctId, {
|
|
2878
|
+
sessionId,
|
|
2879
|
+
durationSeconds,
|
|
2880
|
+
startedAt: sessionStartedAt?.toISOString() ?? null,
|
|
2881
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2882
|
+
}).catch(() => {
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
async function trackAppOpen(apiClient, sessionId) {
|
|
2886
|
+
const distinctId = identityManager.getDistinctId();
|
|
2887
|
+
await apiClient.trackEvent("$app_open", distinctId, {
|
|
2888
|
+
sessionId,
|
|
2889
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2890
|
+
}).catch(() => {
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
async function trackAppBackground(apiClient, sessionId, durationSeconds) {
|
|
2894
|
+
const distinctId = identityManager.getDistinctId();
|
|
2895
|
+
await apiClient.trackEvent("$app_background", distinctId, {
|
|
2896
|
+
sessionId,
|
|
2897
|
+
durationSeconds,
|
|
2898
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2899
|
+
}).catch(() => {
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
// src/domains/session/SessionManager.ts
|
|
2904
|
+
var SESSION_ID_KEY = "current_session_id";
|
|
2905
|
+
var SESSION_START_KEY = "session_start";
|
|
2906
|
+
var EMERGENCY_PAYWALL_SHOWN_KEY = "emergency_paywall_shown";
|
|
2907
|
+
var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
2908
|
+
var SessionManager = class {
|
|
2909
|
+
constructor() {
|
|
2910
|
+
this.sessionId = null;
|
|
2911
|
+
this.sessionStartedAt = null;
|
|
2912
|
+
this.appState = "active";
|
|
2913
|
+
this.apiClient = null;
|
|
2914
|
+
this.debug = false;
|
|
2915
|
+
this.initialized = false;
|
|
2916
|
+
this.config = {};
|
|
2917
|
+
this.backgroundTimestamp = null;
|
|
2918
|
+
this.appStateSubscription = null;
|
|
2919
|
+
this.emergencyPaywallHandler = null;
|
|
2920
|
+
this.emergencyPaywallShownInSession = false;
|
|
2921
|
+
this.secureStorage = null;
|
|
2922
|
+
this.handleAppStateChange = async (nextAppState) => {
|
|
2923
|
+
const previousState = this.appState;
|
|
2924
|
+
this.appState = nextAppState;
|
|
2925
|
+
if (nextAppState === "active" && previousState !== "active") {
|
|
2926
|
+
await this.handleForeground();
|
|
2927
|
+
} else if (nextAppState === "background" && previousState === "active") {
|
|
2928
|
+
await this.handleBackground();
|
|
2929
|
+
}
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
async initialize(apiClient, config = {}, debug = false) {
|
|
2933
|
+
if (this.initialized) {
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
this.apiClient = apiClient;
|
|
2937
|
+
this.debug = debug;
|
|
2938
|
+
this.secureStorage = new SecureStorage(debug);
|
|
2939
|
+
this.config = {
|
|
2940
|
+
sessionTimeoutMs: config.sessionTimeoutMs ?? SESSION_TIMEOUT_MS,
|
|
2941
|
+
trackAppState: config.trackAppState ?? true
|
|
2942
|
+
};
|
|
2943
|
+
await this.restoreSession();
|
|
2944
|
+
if (this.config.trackAppState) {
|
|
2945
|
+
this.setupAppStateListener();
|
|
2946
|
+
}
|
|
2947
|
+
this.initialized = true;
|
|
2948
|
+
this.log("SessionManager initialized");
|
|
2949
|
+
}
|
|
2950
|
+
async startSession() {
|
|
2951
|
+
this.ensureInitialized();
|
|
2952
|
+
if (this.sessionId) {
|
|
2953
|
+
await this.endSession();
|
|
2954
|
+
}
|
|
2955
|
+
this.sessionId = generateUUID();
|
|
2956
|
+
this.sessionStartedAt = /* @__PURE__ */ new Date();
|
|
2957
|
+
this.emergencyPaywallShownInSession = false;
|
|
2958
|
+
await Promise.all([
|
|
2959
|
+
this.secureStorage.set(SESSION_ID_KEY, this.sessionId),
|
|
2960
|
+
this.secureStorage.set(SESSION_START_KEY, this.sessionStartedAt.toISOString()),
|
|
2961
|
+
this.secureStorage.remove(EMERGENCY_PAYWALL_SHOWN_KEY)
|
|
2962
|
+
]);
|
|
2963
|
+
await this.trackSessionStart();
|
|
2964
|
+
this.log("Session started:", this.sessionId);
|
|
2965
|
+
return this.sessionId;
|
|
2966
|
+
}
|
|
2967
|
+
async endSession() {
|
|
2968
|
+
this.ensureInitialized();
|
|
2969
|
+
if (!this.sessionId || !this.sessionStartedAt) {
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
await this.trackSessionEnd();
|
|
2973
|
+
this.sessionId = null;
|
|
2974
|
+
this.sessionStartedAt = null;
|
|
2975
|
+
await Promise.all([
|
|
2976
|
+
this.secureStorage.remove(SESSION_ID_KEY),
|
|
2977
|
+
this.secureStorage.remove(SESSION_START_KEY)
|
|
2978
|
+
]);
|
|
2979
|
+
this.log("Session ended");
|
|
2980
|
+
}
|
|
2981
|
+
getSessionId() {
|
|
2982
|
+
return this.sessionId;
|
|
2983
|
+
}
|
|
2984
|
+
getSessionDuration() {
|
|
2985
|
+
if (!this.sessionStartedAt) {
|
|
2986
|
+
return 0;
|
|
2987
|
+
}
|
|
2988
|
+
return Math.floor((Date.now() - this.sessionStartedAt.getTime()) / 1e3);
|
|
2989
|
+
}
|
|
2990
|
+
isSessionActive() {
|
|
2991
|
+
return this.sessionId !== null;
|
|
2992
|
+
}
|
|
2993
|
+
getState() {
|
|
2994
|
+
return {
|
|
2995
|
+
sessionId: this.sessionId,
|
|
2996
|
+
startedAt: this.sessionStartedAt,
|
|
2997
|
+
isActive: this.isSessionActive(),
|
|
2998
|
+
duration: this.getSessionDuration()
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
destroy() {
|
|
3002
|
+
if (this.appStateSubscription) {
|
|
3003
|
+
this.appStateSubscription.remove();
|
|
3004
|
+
this.appStateSubscription = null;
|
|
3005
|
+
}
|
|
3006
|
+
this.initialized = false;
|
|
3007
|
+
}
|
|
3008
|
+
registerEmergencyPaywallHandler(handler) {
|
|
3009
|
+
this.emergencyPaywallHandler = handler;
|
|
3010
|
+
}
|
|
3011
|
+
async restoreSession() {
|
|
3012
|
+
const [sessionId, sessionStart] = await Promise.all([
|
|
3013
|
+
this.secureStorage.get(SESSION_ID_KEY),
|
|
3014
|
+
this.secureStorage.get(SESSION_START_KEY)
|
|
3015
|
+
]).catch(() => [null, null]);
|
|
3016
|
+
if (sessionId && sessionStart) {
|
|
3017
|
+
const startedAt = new Date(sessionStart);
|
|
3018
|
+
const elapsed = Date.now() - startedAt.getTime();
|
|
3019
|
+
if (elapsed < (this.config.sessionTimeoutMs ?? SESSION_TIMEOUT_MS)) {
|
|
3020
|
+
this.sessionId = sessionId;
|
|
3021
|
+
this.sessionStartedAt = startedAt;
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
await Promise.all([
|
|
3025
|
+
this.secureStorage.remove(SESSION_ID_KEY),
|
|
3026
|
+
this.secureStorage.remove(SESSION_START_KEY)
|
|
3027
|
+
]).catch(() => {
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
setupAppStateListener() {
|
|
3032
|
+
if (this.appStateSubscription) {
|
|
3033
|
+
this.appStateSubscription.remove();
|
|
3034
|
+
}
|
|
3035
|
+
this.appStateSubscription = AppState2.addEventListener("change", this.handleAppStateChange);
|
|
3036
|
+
}
|
|
3037
|
+
async handleForeground() {
|
|
3038
|
+
if (this.backgroundTimestamp) {
|
|
3039
|
+
const elapsed = Date.now() - this.backgroundTimestamp;
|
|
3040
|
+
this.backgroundTimestamp = null;
|
|
3041
|
+
if (elapsed > (this.config.sessionTimeoutMs ?? SESSION_TIMEOUT_MS)) {
|
|
3042
|
+
await this.startSession();
|
|
3043
|
+
await this.trackAppOpen();
|
|
3044
|
+
await this.checkEmergencyPaywall();
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
if (!this.sessionId) {
|
|
3049
|
+
await this.startSession();
|
|
3050
|
+
}
|
|
3051
|
+
await this.trackAppOpen();
|
|
3052
|
+
await this.checkEmergencyPaywall();
|
|
3053
|
+
}
|
|
3054
|
+
async handleBackground() {
|
|
3055
|
+
this.backgroundTimestamp = Date.now();
|
|
3056
|
+
await this.trackAppBackground();
|
|
3057
|
+
}
|
|
3058
|
+
async checkEmergencyPaywall() {
|
|
3059
|
+
if (!this.apiClient || !this.emergencyPaywallHandler) {
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
if (this.emergencyPaywallShownInSession) {
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
const response = await this.apiClient.getEmergencyPaywall().catch(() => null);
|
|
3066
|
+
if (response?.enabled && response.paywallId) {
|
|
3067
|
+
this.emergencyPaywallShownInSession = true;
|
|
3068
|
+
await this.secureStorage.set(EMERGENCY_PAYWALL_SHOWN_KEY, "true");
|
|
3069
|
+
await this.emergencyPaywallHandler(response.paywallId);
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
async trackSessionStart() {
|
|
3073
|
+
if (!this.apiClient || !this.sessionId) return;
|
|
3074
|
+
await trackSessionStart(this.apiClient, this.sessionId, this.sessionStartedAt);
|
|
3075
|
+
}
|
|
3076
|
+
async trackSessionEnd() {
|
|
3077
|
+
if (!this.apiClient || !this.sessionId) return;
|
|
3078
|
+
await trackSessionEnd(this.apiClient, this.sessionId, this.getSessionDuration(), this.sessionStartedAt);
|
|
3079
|
+
}
|
|
3080
|
+
async trackAppOpen() {
|
|
3081
|
+
if (!this.apiClient) return;
|
|
3082
|
+
await trackAppOpen(this.apiClient, this.sessionId);
|
|
3083
|
+
}
|
|
3084
|
+
async trackAppBackground() {
|
|
3085
|
+
if (!this.apiClient) return;
|
|
3086
|
+
await trackAppBackground(this.apiClient, this.sessionId, this.getSessionDuration());
|
|
3087
|
+
}
|
|
3088
|
+
ensureInitialized() {
|
|
3089
|
+
if (!this.initialized) {
|
|
3090
|
+
throw new SessionError(
|
|
3091
|
+
SESSION_ERROR_CODES.NOT_INITIALIZED,
|
|
3092
|
+
"SessionManager not initialized. Call initialize() first"
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
log(...args) {
|
|
3097
|
+
if (this.debug) {
|
|
3098
|
+
console.warn("[Paywallo Session]", ...args);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
var sessionManager = new SessionManager();
|
|
3103
|
+
|
|
3104
|
+
// src/domains/identity/InstallTracker.ts
|
|
3105
|
+
var InstallTracker = class {
|
|
3106
|
+
constructor(debug = false, advertisingIdManager) {
|
|
3107
|
+
this.debug = debug;
|
|
3108
|
+
this.advertisingIdManager = advertisingIdManager ?? new AdvertisingIdManager();
|
|
3109
|
+
}
|
|
3110
|
+
async trackIfNeeded(apiClient, requestATT = false) {
|
|
3111
|
+
const storage = new SecureStorage(this.debug);
|
|
3112
|
+
const alreadyTracked = await storage.get("install_tracked");
|
|
3113
|
+
if (alreadyTracked) return;
|
|
3114
|
+
const distinctId = identityManager.getDistinctId();
|
|
3115
|
+
const sessionId = sessionManager.getSessionId();
|
|
3116
|
+
const installedAt = Date.now();
|
|
3117
|
+
if (!distinctId) return;
|
|
3118
|
+
let deviceInfo = null;
|
|
3119
|
+
try {
|
|
3120
|
+
const mod = await import("react-native-device-info");
|
|
3121
|
+
deviceInfo = mod.default;
|
|
3122
|
+
} catch {
|
|
3123
|
+
deviceInfo = null;
|
|
3124
|
+
}
|
|
3125
|
+
let deviceData = {};
|
|
3126
|
+
try {
|
|
3127
|
+
const screen = Dimensions3.get("screen");
|
|
3128
|
+
const screenWidth = Math.round(screen.width ?? 0);
|
|
3129
|
+
const screenHeight = Math.round(screen.height ?? 0);
|
|
3130
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale ?? "unknown";
|
|
3131
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "unknown";
|
|
3132
|
+
const deviceModel = deviceInfo?.getModel() || "unknown";
|
|
3133
|
+
const osVersion = deviceInfo?.getSystemVersion() || "unknown";
|
|
3134
|
+
const appVersion = deviceInfo?.getVersion() || "unknown";
|
|
3135
|
+
const buildNumber = deviceInfo?.getBuildNumber() || "0";
|
|
3136
|
+
deviceData = {
|
|
3137
|
+
deviceModel,
|
|
3138
|
+
osVersion,
|
|
3139
|
+
appVersion,
|
|
3140
|
+
buildNumber,
|
|
3141
|
+
...screenWidth > 0 && { screenWidth },
|
|
3142
|
+
...screenHeight > 0 && { screenHeight },
|
|
3143
|
+
timezone,
|
|
3144
|
+
locale
|
|
3145
|
+
};
|
|
3146
|
+
} catch {
|
|
3147
|
+
if (this.debug) {
|
|
3148
|
+
console.warn("[Paywallo] Failed to collect device data for install event");
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
const [fbAnonId, referrer, adIds] = await Promise.all([
|
|
3152
|
+
facebookIdManager.getAnonId(),
|
|
3153
|
+
installReferrerManager.getReferrer(),
|
|
3154
|
+
this.advertisingIdManager.collect(requestATT)
|
|
3155
|
+
]);
|
|
3156
|
+
try {
|
|
3157
|
+
await apiClient.trackEvent("$app_installed", distinctId, {
|
|
3158
|
+
installedAt,
|
|
3159
|
+
platform: Platform6.OS,
|
|
3160
|
+
...sessionId && { sessionId },
|
|
3161
|
+
...deviceData,
|
|
3162
|
+
...fbAnonId && { fbAnonId },
|
|
3163
|
+
...referrer.rawReferrer && { installReferrer: referrer.rawReferrer },
|
|
3164
|
+
...referrer.trackingId && { referrerTrackingId: referrer.trackingId },
|
|
3165
|
+
...referrer.fbclid && { referrerFbclid: referrer.fbclid },
|
|
3166
|
+
...referrer.utmSource && { referrerUtmSource: referrer.utmSource },
|
|
3167
|
+
...referrer.utmMedium && { referrerUtmMedium: referrer.utmMedium },
|
|
3168
|
+
...referrer.utmCampaign && { referrerUtmCampaign: referrer.utmCampaign },
|
|
3169
|
+
...adIds.idfa && { idfa: adIds.idfa },
|
|
3170
|
+
...adIds.idfv && { idfv: adIds.idfv },
|
|
3171
|
+
...adIds.gaid && { gaid: adIds.gaid },
|
|
3172
|
+
attStatus: adIds.attStatus
|
|
3173
|
+
});
|
|
3174
|
+
const stored = await storage.set("install_tracked", String(installedAt));
|
|
3175
|
+
if (this.debug) {
|
|
3176
|
+
if (stored) {
|
|
3177
|
+
console.warn("[Paywallo] Install event tracked");
|
|
3178
|
+
} else {
|
|
3179
|
+
console.warn("[Paywallo] Install event tracked but flag storage failed \u2014 may re-track on next launch");
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
} catch (error) {
|
|
3183
|
+
if (this.debug) {
|
|
3184
|
+
console.warn("[Paywallo] Failed to track install:", error);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
};
|
|
3189
|
+
|
|
3190
|
+
// src/core/PaywalloClient.ts
|
|
3191
|
+
function isValidEventName(name) {
|
|
3192
|
+
if (name.startsWith("$")) return /^\$[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
|
|
3193
|
+
return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
|
|
3194
|
+
}
|
|
3195
|
+
var PaywalloClientClass = class {
|
|
3196
|
+
constructor() {
|
|
3197
|
+
this.config = null;
|
|
3198
|
+
this.apiClient = null;
|
|
3199
|
+
this.initPromise = null;
|
|
3200
|
+
this.paywallPresenter = null;
|
|
3201
|
+
this.campaignPresenter = null;
|
|
3202
|
+
this.subscriptionGetter = null;
|
|
3203
|
+
this.activeChecker = null;
|
|
3204
|
+
this.restoreHandler = null;
|
|
3205
|
+
this.lastOnboardingStepTimestamp = null;
|
|
3206
|
+
}
|
|
3207
|
+
async init(config) {
|
|
3208
|
+
if (this.config && this.apiClient) return;
|
|
3209
|
+
if (this.initPromise) {
|
|
3210
|
+
await this.initPromise;
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
if (!config.appKey) throw new ClientError(CLIENT_ERROR_CODES.MISSING_APP_KEY, "appKey is required");
|
|
3214
|
+
this.initPromise = this.doInit(config);
|
|
3215
|
+
try {
|
|
3216
|
+
await this.initPromise;
|
|
3217
|
+
} catch (error) {
|
|
3218
|
+
this.initPromise = null;
|
|
3219
|
+
this.config = null;
|
|
3220
|
+
this.apiClient = null;
|
|
3221
|
+
try {
|
|
3222
|
+
const tempClient = new ApiClient(config.appKey, config.baseUrl, config.debug);
|
|
3223
|
+
void tempClient.reportError("INIT_FAILED", error instanceof Error ? error.message : String(error));
|
|
3224
|
+
} catch {
|
|
3225
|
+
}
|
|
3226
|
+
throw error;
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
async doInit(config) {
|
|
3230
|
+
const environment = config.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "Sandbox" : "Production");
|
|
3231
|
+
const offlineQueueEnabled = config.offlineQueueEnabled !== false;
|
|
3232
|
+
await Promise.all([
|
|
3233
|
+
networkMonitor.initialize({ debug: config.debug }),
|
|
3234
|
+
offlineQueue.initialize({ debug: config.debug }).then(
|
|
3235
|
+
() => offlineQueue.clearItemsWithInvalidAppKey(config.appKey)
|
|
3236
|
+
)
|
|
3237
|
+
]);
|
|
3238
|
+
this.apiClient = new ApiClient(config.appKey, config.baseUrl, config.debug, environment, offlineQueueEnabled, config.timeout);
|
|
3239
|
+
queueProcessor.initialize(this.apiClient.getHttpClient(), { debug: config.debug });
|
|
3240
|
+
affiliateManager.initialize(this.apiClient, config.debug);
|
|
3241
|
+
await Promise.all([
|
|
3242
|
+
identityManager.initialize(this.apiClient, config.debug),
|
|
3243
|
+
sessionManager.initialize(this.apiClient, config.sessionConfig, config.debug)
|
|
3244
|
+
]);
|
|
3245
|
+
if (config.autoStartSession !== false) {
|
|
3246
|
+
sessionManager.startSession().catch(() => {
|
|
3247
|
+
if (config.debug) console.warn("[Paywallo]", "Session start failed during init \u2014 continuing");
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
new InstallTracker(config.debug).trackIfNeeded(this.apiClient, config.requestATT === true).catch(() => {
|
|
3251
|
+
});
|
|
3252
|
+
this.config = config;
|
|
3253
|
+
}
|
|
3254
|
+
async identify(options) {
|
|
3255
|
+
this.ensureInitialized();
|
|
3256
|
+
await identityManager.identify(options);
|
|
3257
|
+
}
|
|
3258
|
+
async track(eventName, options) {
|
|
3259
|
+
const apiClient = this.getApiClientOrThrow();
|
|
3260
|
+
const distinctId = identityManager.getDistinctId();
|
|
3261
|
+
if (!distinctId) {
|
|
3262
|
+
if (this.config?.debug) console.warn("[Paywallo] Skipping event \u2014 no distinctId available");
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
if (!isValidEventName(eventName)) {
|
|
3266
|
+
const msg = `Invalid event name: "${eventName}". Use snake_case or $reserved_name.`;
|
|
3267
|
+
if (this.config?.debug) console.warn("[Paywallo]", msg);
|
|
3268
|
+
throw new ClientError(CLIENT_ERROR_CODES.INVALID_EVENT_NAME, msg);
|
|
3269
|
+
}
|
|
3270
|
+
const sessionId = sessionManager.getSessionId();
|
|
3271
|
+
await apiClient.trackEvent(eventName, distinctId, {
|
|
3272
|
+
...identityManager.getProperties(),
|
|
3273
|
+
...options?.properties,
|
|
3274
|
+
...sessionId && { sessionId }
|
|
3275
|
+
}, options?.timestamp);
|
|
3276
|
+
}
|
|
3277
|
+
async onboardingStep(index, name) {
|
|
3278
|
+
const apiClient = this.getApiClientOrThrow();
|
|
3279
|
+
const distinctId = identityManager.getDistinctId();
|
|
3280
|
+
if (!distinctId) {
|
|
3281
|
+
if (this.config?.debug) console.warn("[Paywallo] Skipping event \u2014 no distinctId available");
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
3285
|
+
const msg = `Invalid stepIndex: "${index}". Must be a non-negative integer.`;
|
|
3286
|
+
if (this.config?.debug) console.warn("[Paywallo]", msg);
|
|
3287
|
+
throw new AnalyticsError(ANALYTICS_ERROR_CODES.INVALID_STEP_INDEX, msg);
|
|
3288
|
+
}
|
|
3289
|
+
const now = Date.now();
|
|
3290
|
+
const timeOnPreviousStep = this.lastOnboardingStepTimestamp !== null ? Math.round((now - this.lastOnboardingStepTimestamp) / 1e3) : 0;
|
|
3291
|
+
this.lastOnboardingStepTimestamp = now;
|
|
3292
|
+
const sessionId = sessionManager.getSessionId();
|
|
3293
|
+
await apiClient.trackEvent("$onboarding_step", distinctId, {
|
|
3294
|
+
...identityManager.getProperties(),
|
|
3295
|
+
stepIndex: index,
|
|
3296
|
+
stepName: name,
|
|
3297
|
+
timestamp: now,
|
|
3298
|
+
timeOnPreviousStep,
|
|
3299
|
+
...sessionId && { sessionId }
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
async coreAction(actionName) {
|
|
3303
|
+
const apiClient = this.getApiClientOrThrow();
|
|
3304
|
+
const distinctId = identityManager.getDistinctId();
|
|
3305
|
+
if (!distinctId) {
|
|
3306
|
+
if (this.config?.debug) console.warn("[Paywallo] Skipping event \u2014 no distinctId available");
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
3309
|
+
if (!actionName || typeof actionName !== "string") {
|
|
3310
|
+
const msg = `Invalid actionName: "${actionName}". Must be a non-empty string.`;
|
|
3311
|
+
if (this.config?.debug) console.warn("[Paywallo]", msg);
|
|
3312
|
+
throw new AnalyticsError(ANALYTICS_ERROR_CODES.INVALID_ACTION_NAME, msg);
|
|
3313
|
+
}
|
|
3314
|
+
const sessionId = sessionManager.getSessionId();
|
|
3315
|
+
await apiClient.trackEvent("$core_action", distinctId, {
|
|
3316
|
+
...identityManager.getProperties(),
|
|
3317
|
+
actionName,
|
|
3318
|
+
timestamp: Date.now(),
|
|
3319
|
+
...sessionId && { sessionId }
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
async getVariant(flagKey) {
|
|
3323
|
+
const distinctId = identityManager.getDistinctId();
|
|
3324
|
+
if (!distinctId) return { variant: null };
|
|
3325
|
+
return this.getApiClientOrThrow().getVariant(flagKey, distinctId);
|
|
3326
|
+
}
|
|
3327
|
+
async getConditionalFlag(flagKey, context) {
|
|
3328
|
+
const distinctId = identityManager.getDistinctId();
|
|
3329
|
+
if (!distinctId) return false;
|
|
3330
|
+
const result = await this.getApiClientOrThrow().getConditionalFlag(flagKey, { distinctId, ...context });
|
|
3331
|
+
return result.value;
|
|
3332
|
+
}
|
|
3333
|
+
async getPaywall(placement) {
|
|
3334
|
+
return this.getApiClientOrThrow().getPaywall(placement);
|
|
3335
|
+
}
|
|
3336
|
+
async presentPaywall(placement) {
|
|
3337
|
+
this.ensureInitialized();
|
|
3338
|
+
if (!this.paywallPresenter) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
|
|
3339
|
+
return this.paywallPresenter(placement);
|
|
3340
|
+
}
|
|
3341
|
+
async getCampaign(placement, context) {
|
|
3342
|
+
return campaignGateService.getCampaign(this.getApiClientOrThrow(), placement, context);
|
|
3343
|
+
}
|
|
3344
|
+
async presentCampaign(placement, context) {
|
|
3345
|
+
this.ensureInitialized();
|
|
3346
|
+
return campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, this.paywallPresenter, this.campaignPresenter, context);
|
|
3347
|
+
}
|
|
3348
|
+
async hasActiveSubscription() {
|
|
3349
|
+
this.ensureInitialized();
|
|
3350
|
+
try {
|
|
3351
|
+
if (this.activeChecker) return await this.activeChecker();
|
|
3352
|
+
const distinctId = identityManager.getDistinctId();
|
|
3353
|
+
if (!distinctId || !this.apiClient) return false;
|
|
3354
|
+
return (await this.apiClient.getSubscriptionStatus(distinctId)).hasActiveSubscription;
|
|
3355
|
+
} catch {
|
|
3356
|
+
if (this.config?.debug) console.warn("[Paywallo]", "hasActiveSubscription failed \u2014 returning false");
|
|
3357
|
+
if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", "hasActiveSubscription failed");
|
|
3358
|
+
return false;
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
async getSubscription() {
|
|
3362
|
+
this.ensureInitialized();
|
|
3363
|
+
try {
|
|
3364
|
+
if (this.subscriptionGetter) return await this.subscriptionGetter();
|
|
3365
|
+
const distinctId = identityManager.getDistinctId();
|
|
3366
|
+
if (!distinctId || !this.apiClient) return null;
|
|
3367
|
+
const status = await this.apiClient.getSubscriptionStatus(distinctId);
|
|
3368
|
+
if (!status.subscription) return null;
|
|
3369
|
+
return { ...status.subscription, expiresAt: status.subscription.expiresAt ? new Date(status.subscription.expiresAt) : null };
|
|
3370
|
+
} catch {
|
|
3371
|
+
if (this.config?.debug) console.warn("[Paywallo]", "getSubscription failed \u2014 returning null");
|
|
3372
|
+
return null;
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
async restorePurchases() {
|
|
3376
|
+
this.ensureInitialized();
|
|
3377
|
+
if (!this.restoreHandler) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
|
|
3378
|
+
return this.restoreHandler();
|
|
3379
|
+
}
|
|
3380
|
+
async reset() {
|
|
3381
|
+
await identityManager.reset();
|
|
3382
|
+
}
|
|
3383
|
+
async fullReset() {
|
|
3384
|
+
const wasDebug = this.config?.debug ?? false;
|
|
3385
|
+
await sessionManager.endSession();
|
|
3386
|
+
await identityManager.reset();
|
|
3387
|
+
queueProcessor.dispose();
|
|
3388
|
+
offlineQueue.dispose();
|
|
3389
|
+
networkMonitor.dispose();
|
|
3390
|
+
campaignGateService.clearPreloaded();
|
|
3391
|
+
this.apiClient = null;
|
|
3392
|
+
this.config = null;
|
|
3393
|
+
this.initPromise = null;
|
|
3394
|
+
this.paywallPresenter = null;
|
|
3395
|
+
this.campaignPresenter = null;
|
|
3396
|
+
this.subscriptionGetter = null;
|
|
3397
|
+
this.activeChecker = null;
|
|
3398
|
+
this.restoreHandler = null;
|
|
3399
|
+
this.lastOnboardingStepTimestamp = null;
|
|
3400
|
+
if (wasDebug) console.warn("[Paywallo]", "Full reset complete");
|
|
3401
|
+
}
|
|
3402
|
+
getConfig() {
|
|
3403
|
+
return this.config;
|
|
3404
|
+
}
|
|
3405
|
+
getDistinctId() {
|
|
3406
|
+
return identityManager.getDistinctId();
|
|
3407
|
+
}
|
|
3408
|
+
getDeviceId() {
|
|
3409
|
+
return identityManager.getDeviceId();
|
|
3410
|
+
}
|
|
3411
|
+
getEmail() {
|
|
3412
|
+
return identityManager.getEmail();
|
|
3413
|
+
}
|
|
3414
|
+
getEnvironment() {
|
|
3415
|
+
return this.apiClient?.getEnvironment() ?? "Production";
|
|
3416
|
+
}
|
|
3417
|
+
setEnvironment(environment) {
|
|
3418
|
+
this.apiClient?.setEnvironment(environment);
|
|
3419
|
+
}
|
|
3420
|
+
getApiClient() {
|
|
3421
|
+
return this.apiClient;
|
|
3422
|
+
}
|
|
3423
|
+
getWebUrl() {
|
|
3424
|
+
return this.apiClient?.getWebUrl() ?? null;
|
|
3425
|
+
}
|
|
3426
|
+
getIdentityState() {
|
|
3427
|
+
return identityManager.getState();
|
|
3428
|
+
}
|
|
3429
|
+
getSessionState() {
|
|
3430
|
+
return sessionManager.getState();
|
|
3431
|
+
}
|
|
3432
|
+
getSessionId() {
|
|
3433
|
+
return sessionManager.getSessionId();
|
|
3434
|
+
}
|
|
3435
|
+
async startSession() {
|
|
3436
|
+
return sessionManager.startSession();
|
|
3437
|
+
}
|
|
3438
|
+
async endSession() {
|
|
3439
|
+
return sessionManager.endSession();
|
|
3440
|
+
}
|
|
3441
|
+
registerPaywallPresenter(p) {
|
|
3442
|
+
this.paywallPresenter = p;
|
|
3443
|
+
}
|
|
3444
|
+
registerCampaignPresenter(p) {
|
|
3445
|
+
this.campaignPresenter = p;
|
|
3446
|
+
}
|
|
3447
|
+
registerSubscriptionGetter(g) {
|
|
3448
|
+
this.subscriptionGetter = g;
|
|
3449
|
+
}
|
|
3450
|
+
registerActiveChecker(c) {
|
|
3451
|
+
this.activeChecker = c;
|
|
3452
|
+
}
|
|
3453
|
+
registerRestoreHandler(h) {
|
|
3454
|
+
this.restoreHandler = h;
|
|
3455
|
+
}
|
|
3456
|
+
registerEmergencyPaywallHandler(h) {
|
|
3457
|
+
sessionManager.registerEmergencyPaywallHandler(h);
|
|
3458
|
+
}
|
|
3459
|
+
async requireSubscription(paywallPlacement) {
|
|
3460
|
+
this.ensureInitialized();
|
|
3461
|
+
if (await this.hasActiveSubscription()) return true;
|
|
3462
|
+
if (!paywallPlacement) return false;
|
|
3463
|
+
const r = await this.presentPaywall(paywallPlacement);
|
|
3464
|
+
return r.purchased || r.restored;
|
|
3465
|
+
}
|
|
3466
|
+
async requireSubscriptionWithCampaign(placement, context) {
|
|
3467
|
+
this.ensureInitialized();
|
|
3468
|
+
return campaignGateService.requireSubscriptionWithCampaign(
|
|
3469
|
+
this.getApiClientOrThrow(),
|
|
3470
|
+
placement,
|
|
3471
|
+
() => this.hasActiveSubscription(),
|
|
3472
|
+
this.paywallPresenter,
|
|
3473
|
+
this.campaignPresenter,
|
|
3474
|
+
context
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
async gateContent(content, paywallPlacement) {
|
|
3478
|
+
return await this.requireSubscription(paywallPlacement) ? content() : null;
|
|
3479
|
+
}
|
|
3480
|
+
async gateContentWithCampaign(content, placement, context) {
|
|
3481
|
+
return await this.requireSubscriptionWithCampaign(placement, context) ? content() : null;
|
|
3482
|
+
}
|
|
3483
|
+
async preloadCampaign(placement, context) {
|
|
3484
|
+
return campaignGateService.preloadCampaign(this.getApiClientOrThrow(), placement, context);
|
|
3485
|
+
}
|
|
3486
|
+
getPreloadedCampaign(placement) {
|
|
3487
|
+
return campaignGateService.getPreloadedCampaign(placement);
|
|
3488
|
+
}
|
|
3489
|
+
isOnline() {
|
|
3490
|
+
return networkMonitor.isOnline();
|
|
3491
|
+
}
|
|
3492
|
+
getOfflineQueueSize() {
|
|
3493
|
+
return offlineQueue.getSize();
|
|
3494
|
+
}
|
|
3495
|
+
async clearOfflineQueue() {
|
|
3496
|
+
await offlineQueue.clear();
|
|
3497
|
+
}
|
|
3498
|
+
async processOfflineQueue() {
|
|
3499
|
+
return queueProcessor.processQueue();
|
|
3500
|
+
}
|
|
3501
|
+
getApiClientOrThrow() {
|
|
3502
|
+
if (!this.config || !this.apiClient) throw new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, "SDK not initialized. Call Paywallo.init() first");
|
|
3503
|
+
return this.apiClient;
|
|
3504
|
+
}
|
|
3505
|
+
ensureInitialized() {
|
|
3506
|
+
if (!this.config || !this.apiClient) throw new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, "SDK not initialized. Call Paywallo.init() first");
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
var PaywalloClient = new PaywalloClientClass();
|
|
3510
|
+
|
|
3511
|
+
// src/context/PaywalloContext.ts
|
|
3512
|
+
import { createContext as createContext2 } from "react";
|
|
3513
|
+
var PaywalloContext = createContext2(null);
|
|
3514
|
+
|
|
3515
|
+
// src/utils/localization.ts
|
|
3516
|
+
import { NativeModules as NativeModules3, Platform as Platform7 } from "react-native";
|
|
3517
|
+
var currentLanguage = "pt-BR";
|
|
3518
|
+
var defaultLanguage = "pt-BR";
|
|
3519
|
+
function setCurrentLanguage(language) {
|
|
3520
|
+
currentLanguage = language;
|
|
3521
|
+
}
|
|
3522
|
+
function setDefaultLanguage(language) {
|
|
3523
|
+
defaultLanguage = language;
|
|
3524
|
+
}
|
|
3525
|
+
function getCurrentLanguage() {
|
|
3526
|
+
return currentLanguage;
|
|
3527
|
+
}
|
|
3528
|
+
function getDefaultLanguage() {
|
|
3529
|
+
return defaultLanguage;
|
|
3530
|
+
}
|
|
3531
|
+
function getLocalizedString(text) {
|
|
3532
|
+
if (!text) return "";
|
|
3533
|
+
if (typeof text === "string") return text;
|
|
3534
|
+
return text[currentLanguage] ?? text[defaultLanguage] ?? Object.values(text)[0] ?? "";
|
|
3535
|
+
}
|
|
3536
|
+
function detectDeviceLanguage() {
|
|
3537
|
+
try {
|
|
3538
|
+
if (Platform7.OS === "ios") {
|
|
3539
|
+
const settings = NativeModules3.SettingsManager?.settings;
|
|
3540
|
+
return settings?.AppleLocale ?? settings?.AppleLanguages?.[0] ?? "en-US";
|
|
3541
|
+
}
|
|
3542
|
+
return NativeModules3.I18nManager?.localeIdentifier ?? "en-US";
|
|
3543
|
+
} catch {
|
|
3544
|
+
return "en-US";
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
function initLocalization(options) {
|
|
3548
|
+
if (options?.defaultLanguage) {
|
|
3549
|
+
defaultLanguage = options.defaultLanguage;
|
|
3550
|
+
}
|
|
3551
|
+
if (options?.detectDevice !== false) {
|
|
3552
|
+
const deviceLanguage = detectDeviceLanguage();
|
|
3553
|
+
currentLanguage = deviceLanguage;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
// src/hooks/usePaywallo.ts
|
|
3558
|
+
import { useContext as useContext2 } from "react";
|
|
3559
|
+
function usePaywallo() {
|
|
3560
|
+
const context = useContext2(PaywalloContext);
|
|
3561
|
+
if (!context) {
|
|
3562
|
+
throw new ClientError(CLIENT_ERROR_CODES.PROVIDER_MISSING, "usePaywallo must be used within a PaywalloProvider");
|
|
3563
|
+
}
|
|
3564
|
+
return context;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
// src/hooks/useProducts.ts
|
|
3568
|
+
import { useCallback as useCallback5, useEffect as useEffect5, useState as useState5 } from "react";
|
|
3569
|
+
|
|
3570
|
+
// src/utils/productMappers.ts
|
|
3571
|
+
function mapToIAPProduct(product) {
|
|
3572
|
+
return {
|
|
3573
|
+
productId: product.productId,
|
|
3574
|
+
price: product.price,
|
|
3575
|
+
currency: product.currency,
|
|
3576
|
+
localizedPrice: product.localizedPrice,
|
|
3577
|
+
title: product.title,
|
|
3578
|
+
description: product.description,
|
|
3579
|
+
type: product.type === "subscription" ? "subs" : "inapp",
|
|
3580
|
+
subscriptionPeriodAndroid: product.subscriptionPeriod,
|
|
3581
|
+
introductoryPrice: product.introductoryPrice,
|
|
3582
|
+
freeTrialPeriodAndroid: product.freeTrialPeriod
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
function mapToFormattedProduct(product) {
|
|
3586
|
+
return {
|
|
3587
|
+
productId: product.productId,
|
|
3588
|
+
title: product.title,
|
|
3589
|
+
description: product.description,
|
|
3590
|
+
price: product.price,
|
|
3591
|
+
priceValue: product.priceValue,
|
|
3592
|
+
currency: product.currency,
|
|
3593
|
+
localizedPrice: product.localizedPrice,
|
|
3594
|
+
subscriptionPeriod: product.subscriptionPeriod,
|
|
3595
|
+
introductoryPrice: product.introductoryPrice,
|
|
3596
|
+
freeTrialPeriod: product.freeTrialPeriod,
|
|
3597
|
+
pricePerMonth: product.pricePerMonth,
|
|
3598
|
+
pricePerWeek: product.pricePerWeek,
|
|
3599
|
+
pricePerDay: product.pricePerDay
|
|
3600
|
+
};
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
// src/hooks/useProducts.ts
|
|
3604
|
+
function useProducts(initialProductIds) {
|
|
3605
|
+
const [products, setProducts] = useState5([]);
|
|
3606
|
+
const [formattedProducts, setFormattedProducts] = useState5([]);
|
|
3607
|
+
const [isLoading, setIsLoading] = useState5(false);
|
|
3608
|
+
const [error, setError] = useState5(null);
|
|
3609
|
+
const loadProducts = useCallback5(async (productIds) => {
|
|
3610
|
+
setIsLoading(true);
|
|
3611
|
+
setError(null);
|
|
3612
|
+
try {
|
|
3613
|
+
const iapService = getIAPService();
|
|
3614
|
+
const storeProducts = await iapService.loadProducts(productIds);
|
|
3615
|
+
setProducts(storeProducts.map(mapToIAPProduct));
|
|
3616
|
+
setFormattedProducts(storeProducts.map(mapToFormattedProduct));
|
|
3617
|
+
} catch (err) {
|
|
3618
|
+
setError(err instanceof PurchaseError ? err : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, String(err)));
|
|
3619
|
+
} finally {
|
|
3620
|
+
setIsLoading(false);
|
|
3621
|
+
}
|
|
3622
|
+
}, []);
|
|
3623
|
+
useEffect5(() => {
|
|
3624
|
+
if (initialProductIds && initialProductIds.length > 0) {
|
|
3625
|
+
void loadProducts(initialProductIds);
|
|
3626
|
+
}
|
|
3627
|
+
}, []);
|
|
3628
|
+
const getProduct = useCallback5(
|
|
3629
|
+
(productId) => {
|
|
3630
|
+
return products.find((p) => p.productId === productId);
|
|
3631
|
+
},
|
|
3632
|
+
[products]
|
|
3633
|
+
);
|
|
3634
|
+
const getFormattedProduct = useCallback5(
|
|
3635
|
+
(productId) => {
|
|
3636
|
+
return formattedProducts.find((p) => p.productId === productId);
|
|
3637
|
+
},
|
|
3638
|
+
[formattedProducts]
|
|
3639
|
+
);
|
|
3640
|
+
const getPriceInfo = useCallback5(
|
|
3641
|
+
(productId) => {
|
|
3642
|
+
const p = formattedProducts.find((fp) => fp.productId === productId);
|
|
3643
|
+
if (!p) return void 0;
|
|
3644
|
+
return {
|
|
3645
|
+
price: p.price,
|
|
3646
|
+
priceValue: p.priceValue,
|
|
3647
|
+
currency: p.currency,
|
|
3648
|
+
localizedPrice: p.localizedPrice,
|
|
3649
|
+
pricePerMonth: p.pricePerMonth,
|
|
3650
|
+
pricePerWeek: p.pricePerWeek,
|
|
3651
|
+
pricePerDay: p.pricePerDay
|
|
3652
|
+
};
|
|
3653
|
+
},
|
|
3654
|
+
[formattedProducts]
|
|
3655
|
+
);
|
|
3656
|
+
return {
|
|
3657
|
+
products,
|
|
3658
|
+
formattedProducts,
|
|
3659
|
+
isLoading,
|
|
3660
|
+
error,
|
|
3661
|
+
loadProducts,
|
|
3662
|
+
getProduct,
|
|
3663
|
+
getFormattedProduct,
|
|
3664
|
+
getPriceInfo
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
// src/hooks/usePurchase.ts
|
|
3669
|
+
import { useCallback as useCallback6, useState as useState6 } from "react";
|
|
3670
|
+
function usePurchase() {
|
|
3671
|
+
const [state, setState] = useState6("idle");
|
|
3672
|
+
const [error, setError] = useState6(null);
|
|
3673
|
+
const purchase = useCallback6(async (productId) => {
|
|
3674
|
+
setState("purchasing");
|
|
3675
|
+
setError(null);
|
|
3676
|
+
try {
|
|
3677
|
+
const iapService = getIAPService();
|
|
3678
|
+
const result = await iapService.purchase(productId);
|
|
3679
|
+
if (result.success && result.purchase) {
|
|
3680
|
+
setState("idle");
|
|
3681
|
+
return {
|
|
3682
|
+
productId: result.purchase.productId,
|
|
3683
|
+
transactionId: result.purchase.transactionId,
|
|
3684
|
+
transactionDate: result.purchase.transactionDate,
|
|
3685
|
+
transactionReceipt: result.purchase.receipt
|
|
3686
|
+
};
|
|
3687
|
+
}
|
|
3688
|
+
if (result.error) {
|
|
3689
|
+
const purchaseError = createPurchaseError("PURCHASE_FAILED", result.error.message);
|
|
3690
|
+
setError(purchaseError);
|
|
3691
|
+
setState("error");
|
|
3692
|
+
throw result.error;
|
|
3693
|
+
}
|
|
3694
|
+
setState("idle");
|
|
3695
|
+
throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, "Purchase cancelled by user", true);
|
|
3696
|
+
} catch (err) {
|
|
3697
|
+
setState("error");
|
|
3698
|
+
throw err;
|
|
3699
|
+
}
|
|
3700
|
+
}, []);
|
|
3701
|
+
const restore = useCallback6(async () => {
|
|
3702
|
+
setState("restoring");
|
|
3703
|
+
setError(null);
|
|
3704
|
+
try {
|
|
3705
|
+
const iapService = getIAPService();
|
|
3706
|
+
const transactions = await iapService.restore();
|
|
3707
|
+
setState("idle");
|
|
3708
|
+
return transactions.map((tx) => ({
|
|
3709
|
+
productId: tx.productId,
|
|
3710
|
+
transactionId: tx.transactionId,
|
|
3711
|
+
transactionDate: tx.transactionDate,
|
|
3712
|
+
transactionReceipt: tx.receipt
|
|
3713
|
+
}));
|
|
3714
|
+
} catch (err) {
|
|
3715
|
+
const e = err instanceof PurchaseError ? err : createPurchaseError("RESTORE_FAILED", String(err));
|
|
3716
|
+
setError(e);
|
|
3717
|
+
setState("error");
|
|
3718
|
+
throw e;
|
|
3719
|
+
}
|
|
3720
|
+
}, []);
|
|
3721
|
+
return {
|
|
3722
|
+
state,
|
|
3723
|
+
purchase,
|
|
3724
|
+
restore,
|
|
3725
|
+
error,
|
|
3726
|
+
isLoading: state === "loading",
|
|
3727
|
+
isPurchasing: state === "purchasing",
|
|
3728
|
+
isRestoring: state === "restoring"
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
// src/hooks/useSubscription.ts
|
|
3733
|
+
import { useCallback as useCallback7, useEffect as useEffect6, useState as useState7 } from "react";
|
|
3734
|
+
|
|
3735
|
+
// src/domains/subscription/SubscriptionManager.ts
|
|
3736
|
+
import { Platform as Platform8 } from "react-native";
|
|
3737
|
+
|
|
3738
|
+
// src/domains/subscription/SubscriptionCache.ts
|
|
3739
|
+
var CACHE_KEY = "subscription_cache";
|
|
3740
|
+
var DEFAULT_TTL = 5 * 60 * 1e3;
|
|
3741
|
+
var SubscriptionCache = class {
|
|
3742
|
+
constructor(ttl = DEFAULT_TTL) {
|
|
3743
|
+
this.memoryCache = null;
|
|
3744
|
+
this.ttl = ttl;
|
|
3745
|
+
this.storage = new SecureStorage();
|
|
3746
|
+
}
|
|
3747
|
+
async get() {
|
|
3748
|
+
if (this.memoryCache && !this.isExpired(this.memoryCache)) {
|
|
3749
|
+
return this.memoryCache;
|
|
3750
|
+
}
|
|
3751
|
+
try {
|
|
3752
|
+
const stored = await this.storage.get(CACHE_KEY);
|
|
3753
|
+
if (!stored) {
|
|
3754
|
+
return null;
|
|
3755
|
+
}
|
|
3756
|
+
const cached = JSON.parse(stored);
|
|
3757
|
+
if (cached.data.subscription?.expiresAt) {
|
|
3758
|
+
cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
|
|
3759
|
+
}
|
|
3760
|
+
if (cached.data.subscription?.originalPurchaseDate) {
|
|
3761
|
+
cached.data.subscription.originalPurchaseDate = new Date(
|
|
3762
|
+
cached.data.subscription.originalPurchaseDate
|
|
3763
|
+
);
|
|
3764
|
+
}
|
|
3765
|
+
if (cached.data.subscription?.latestPurchaseDate) {
|
|
3766
|
+
cached.data.subscription.latestPurchaseDate = new Date(
|
|
3767
|
+
cached.data.subscription.latestPurchaseDate
|
|
3768
|
+
);
|
|
3769
|
+
}
|
|
3770
|
+
if (cached.data.subscription?.cancellationDate) {
|
|
3771
|
+
cached.data.subscription.cancellationDate = new Date(
|
|
3772
|
+
cached.data.subscription.cancellationDate
|
|
3773
|
+
);
|
|
3774
|
+
}
|
|
3775
|
+
if (cached.data.subscription?.gracePeriodExpiresAt) {
|
|
3776
|
+
cached.data.subscription.gracePeriodExpiresAt = new Date(
|
|
3777
|
+
cached.data.subscription.gracePeriodExpiresAt
|
|
3778
|
+
);
|
|
3779
|
+
}
|
|
3780
|
+
cached.isStale = this.isExpired(cached);
|
|
3781
|
+
if (!cached.isStale) {
|
|
3782
|
+
this.memoryCache = cached;
|
|
3783
|
+
}
|
|
3784
|
+
return cached;
|
|
3785
|
+
} catch {
|
|
3786
|
+
return null;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
async set(data) {
|
|
3790
|
+
const cached = {
|
|
3791
|
+
data,
|
|
3792
|
+
cachedAt: Date.now(),
|
|
3793
|
+
isStale: false
|
|
3794
|
+
};
|
|
3795
|
+
this.memoryCache = cached;
|
|
3796
|
+
try {
|
|
3797
|
+
await this.storage.set(CACHE_KEY, JSON.stringify(cached));
|
|
3798
|
+
} catch {
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
async invalidate() {
|
|
3802
|
+
this.memoryCache = null;
|
|
3803
|
+
try {
|
|
3804
|
+
await this.storage.remove(CACHE_KEY);
|
|
3805
|
+
} catch {
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
isExpired(cached) {
|
|
3809
|
+
return Date.now() - cached.cachedAt > this.ttl;
|
|
3810
|
+
}
|
|
3811
|
+
setTTL(ttl) {
|
|
3812
|
+
this.ttl = ttl;
|
|
3813
|
+
}
|
|
3814
|
+
};
|
|
3815
|
+
|
|
3816
|
+
// src/domains/subscription/SubscriptionManager.ts
|
|
3817
|
+
var SubscriptionManagerClass = class {
|
|
3818
|
+
constructor() {
|
|
3819
|
+
this.config = null;
|
|
3820
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
3821
|
+
this.userId = null;
|
|
3822
|
+
this.cache = new SubscriptionCache();
|
|
3823
|
+
}
|
|
3824
|
+
init(config) {
|
|
3825
|
+
this.config = config;
|
|
3826
|
+
if (config.cacheTTL) {
|
|
3827
|
+
this.cache.setTTL(config.cacheTTL);
|
|
3828
|
+
}
|
|
3829
|
+
this.log("SubscriptionManager initialized");
|
|
3830
|
+
}
|
|
3831
|
+
setUserId(userId) {
|
|
3832
|
+
if (this.userId !== userId) {
|
|
3833
|
+
this.userId = userId;
|
|
3834
|
+
void this.cache.invalidate();
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
async hasActiveSubscription(forceRefresh = false) {
|
|
3838
|
+
const status = await this.getSubscriptionStatus(forceRefresh);
|
|
3839
|
+
return status.hasActiveSubscription;
|
|
3840
|
+
}
|
|
3841
|
+
async getSubscription(forceRefresh = false) {
|
|
3842
|
+
const status = await this.getSubscriptionStatus(forceRefresh);
|
|
3843
|
+
return status.subscription;
|
|
3844
|
+
}
|
|
3845
|
+
async getEntitlements(forceRefresh = false) {
|
|
3846
|
+
const status = await this.getSubscriptionStatus(forceRefresh);
|
|
3847
|
+
return status.entitlements;
|
|
3848
|
+
}
|
|
3849
|
+
async getSubscriptionStatus(forceRefresh = false) {
|
|
3850
|
+
if (!this.config) {
|
|
3851
|
+
return this.getEmptyStatus();
|
|
3852
|
+
}
|
|
3853
|
+
if (!forceRefresh) {
|
|
3854
|
+
const cached = await this.cache.get();
|
|
3855
|
+
if (cached && !cached.isStale) {
|
|
3856
|
+
return cached.data;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
try {
|
|
3860
|
+
const status = await this.fetchSubscriptionStatus();
|
|
3861
|
+
await this.cache.set(status);
|
|
3862
|
+
this.notifyListeners(status);
|
|
3863
|
+
return status;
|
|
3864
|
+
} catch (error) {
|
|
3865
|
+
this.log("Failed to fetch subscription status", error);
|
|
3866
|
+
const cached = await this.cache.get();
|
|
3867
|
+
if (cached) {
|
|
3868
|
+
return cached.data;
|
|
3869
|
+
}
|
|
3870
|
+
return this.getEmptyStatus();
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
async restorePurchases() {
|
|
3874
|
+
if (!this.config) {
|
|
3875
|
+
throw new SessionError(
|
|
3876
|
+
SESSION_ERROR_CODES.NOT_INITIALIZED,
|
|
3877
|
+
"SubscriptionManager not initialized"
|
|
3878
|
+
);
|
|
3879
|
+
}
|
|
3880
|
+
await this.cache.invalidate();
|
|
3881
|
+
return this.getSubscriptionStatus(true);
|
|
3882
|
+
}
|
|
3883
|
+
async fetchSubscriptionStatus() {
|
|
3884
|
+
if (!this.config) {
|
|
3885
|
+
return this.getEmptyStatus();
|
|
3886
|
+
}
|
|
3887
|
+
const url = new URL(`${this.config.serverUrl}/subscriptions/status`);
|
|
3888
|
+
if (this.userId) {
|
|
3889
|
+
url.searchParams.set("userId", this.userId);
|
|
3890
|
+
}
|
|
3891
|
+
url.searchParams.set("platform", Platform8.OS);
|
|
3892
|
+
const response = await fetch(url.toString(), {
|
|
3893
|
+
method: "GET",
|
|
3894
|
+
headers: {
|
|
3895
|
+
"Content-Type": "application/json",
|
|
3896
|
+
"X-App-Key": this.config.appKey
|
|
3897
|
+
}
|
|
3898
|
+
});
|
|
3899
|
+
if (!response.ok) {
|
|
3900
|
+
throw new SessionError(
|
|
3901
|
+
SESSION_ERROR_CODES.RESTORE_FAILED,
|
|
3902
|
+
`Failed to fetch subscription status: ${response.status}`
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3905
|
+
const data = await response.json();
|
|
3906
|
+
if (data.subscription) {
|
|
3907
|
+
data.subscription.expiresAt = new Date(data.subscription.expiresAt);
|
|
3908
|
+
data.subscription.originalPurchaseDate = new Date(data.subscription.originalPurchaseDate);
|
|
3909
|
+
data.subscription.latestPurchaseDate = new Date(data.subscription.latestPurchaseDate);
|
|
3910
|
+
if (data.subscription.cancellationDate) {
|
|
3911
|
+
data.subscription.cancellationDate = new Date(data.subscription.cancellationDate);
|
|
3912
|
+
}
|
|
3913
|
+
if (data.subscription.gracePeriodExpiresAt) {
|
|
3914
|
+
data.subscription.gracePeriodExpiresAt = new Date(data.subscription.gracePeriodExpiresAt);
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
return data;
|
|
3918
|
+
}
|
|
3919
|
+
getEmptyStatus() {
|
|
3920
|
+
return {
|
|
3921
|
+
hasActiveSubscription: false,
|
|
3922
|
+
subscription: null,
|
|
3923
|
+
entitlements: []
|
|
3924
|
+
};
|
|
3925
|
+
}
|
|
3926
|
+
addListener(listener) {
|
|
3927
|
+
this.listeners.add(listener);
|
|
3928
|
+
return () => this.listeners.delete(listener);
|
|
3929
|
+
}
|
|
3930
|
+
notifyListeners(status) {
|
|
3931
|
+
for (const listener of this.listeners) {
|
|
3932
|
+
listener(status);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
async onPurchaseComplete() {
|
|
3936
|
+
await this.cache.invalidate();
|
|
3937
|
+
await this.getSubscriptionStatus(true);
|
|
3938
|
+
}
|
|
3939
|
+
log(message, data) {
|
|
3940
|
+
if (this.config?.debug) {
|
|
3941
|
+
console.warn(`[SubscriptionManager] ${message}`, data ?? "");
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
};
|
|
3945
|
+
var subscriptionManager = new SubscriptionManagerClass();
|
|
3946
|
+
|
|
3947
|
+
// src/hooks/useSubscription.ts
|
|
3948
|
+
function useSubscription() {
|
|
3949
|
+
const [status, setStatus] = useState7(null);
|
|
3950
|
+
const [isLoading, setIsLoading] = useState7(true);
|
|
3951
|
+
const [error, setError] = useState7(null);
|
|
3952
|
+
const refresh = useCallback7(async () => {
|
|
3953
|
+
setIsLoading(true);
|
|
3954
|
+
setError(null);
|
|
3955
|
+
try {
|
|
3956
|
+
const newStatus = await subscriptionManager.getSubscriptionStatus(true);
|
|
3957
|
+
setStatus(newStatus);
|
|
3958
|
+
} catch (err) {
|
|
3959
|
+
const e = err instanceof PurchaseError ? err : new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, String(err));
|
|
3960
|
+
setError(e);
|
|
3961
|
+
} finally {
|
|
3962
|
+
setIsLoading(false);
|
|
3963
|
+
}
|
|
3964
|
+
}, []);
|
|
3965
|
+
const restore = useCallback7(async () => {
|
|
3966
|
+
setIsLoading(true);
|
|
3967
|
+
setError(null);
|
|
3968
|
+
try {
|
|
3969
|
+
const newStatus = await subscriptionManager.restorePurchases();
|
|
3970
|
+
setStatus(newStatus);
|
|
3971
|
+
} catch (err) {
|
|
3972
|
+
const e = err instanceof PurchaseError ? err : new PurchaseError(PURCHASE_ERROR_CODES.RESTORE_FAILED, String(err));
|
|
3973
|
+
setError(e);
|
|
3974
|
+
} finally {
|
|
3975
|
+
setIsLoading(false);
|
|
3976
|
+
}
|
|
3977
|
+
}, []);
|
|
3978
|
+
useEffect6(() => {
|
|
3979
|
+
void subscriptionManager.getSubscriptionStatus().then((s) => {
|
|
3980
|
+
setStatus(s);
|
|
3981
|
+
setIsLoading(false);
|
|
3982
|
+
});
|
|
3983
|
+
const removeListener = subscriptionManager.addListener((newStatus) => {
|
|
3984
|
+
setStatus(newStatus);
|
|
3985
|
+
});
|
|
3986
|
+
return () => {
|
|
3987
|
+
removeListener();
|
|
3988
|
+
};
|
|
3989
|
+
}, []);
|
|
3990
|
+
return {
|
|
3991
|
+
subscription: status?.subscription ?? null,
|
|
3992
|
+
isActive: status?.hasActiveSubscription ?? false,
|
|
3993
|
+
isLoading,
|
|
3994
|
+
error,
|
|
3995
|
+
entitlements: status?.entitlements ?? [],
|
|
3996
|
+
refresh,
|
|
3997
|
+
restore
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
// src/PaywalloProvider.tsx
|
|
4002
|
+
import { useCallback as useCallback12, useEffect as useEffect7, useMemo as useMemo2, useRef as useRef8, useState as useState11 } from "react";
|
|
4003
|
+
import { Animated as Animated3, Dimensions as Dimensions4, Easing as Easing2, StyleSheet as StyleSheet3 } from "react-native";
|
|
4004
|
+
|
|
4005
|
+
// src/hooks/useCampaignPreload.ts
|
|
4006
|
+
import { useCallback as useCallback8, useRef as useRef6, useState as useState8 } from "react";
|
|
4007
|
+
|
|
4008
|
+
// src/utils/serverProduct.ts
|
|
4009
|
+
var PERIOD_MAP = {
|
|
4010
|
+
P1Y: "ano",
|
|
4011
|
+
P1M: "m\xEAs",
|
|
4012
|
+
P1W: "semana",
|
|
4013
|
+
P3M: "trimestre",
|
|
4014
|
+
P6M: "semestre"
|
|
4015
|
+
};
|
|
4016
|
+
function buildProductFromServerInfo(info) {
|
|
4017
|
+
const period = PERIOD_MAP[info.billingPeriod ?? ""] ?? "";
|
|
4018
|
+
const priceStr = info.price != null ? `R$ ${info.price.toFixed(2).replace(".", ",")}` : "";
|
|
4019
|
+
return {
|
|
4020
|
+
productId: info.storeProductId,
|
|
4021
|
+
title: info.name,
|
|
4022
|
+
description: "",
|
|
4023
|
+
price: priceStr,
|
|
4024
|
+
priceValue: info.price ?? 0,
|
|
4025
|
+
currency: "BRL",
|
|
4026
|
+
localizedPrice: priceStr,
|
|
4027
|
+
type: "subscription",
|
|
4028
|
+
subscriptionPeriod: period,
|
|
4029
|
+
freeTrialPeriod: info.trialDays ? `${info.trialDays} dias` : void 0
|
|
4030
|
+
};
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
// src/utils/loadCampaignProducts.ts
|
|
4034
|
+
async function loadCampaignProducts(serverProducts, storeProductIds) {
|
|
4035
|
+
const productsMap = /* @__PURE__ */ new Map();
|
|
4036
|
+
if (storeProductIds.length > 0) {
|
|
4037
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
4038
|
+
try {
|
|
4039
|
+
const storeProducts = await getIAPService().loadProducts(storeProductIds);
|
|
4040
|
+
if (storeProducts.length > 0) {
|
|
4041
|
+
for (const p of storeProducts) productsMap.set(p.productId, p);
|
|
4042
|
+
}
|
|
4043
|
+
if (productsMap.size >= storeProductIds.length) break;
|
|
4044
|
+
if (attempt < 3) await new Promise((r) => setTimeout(r, 250 * Math.pow(2, attempt)));
|
|
4045
|
+
} catch {
|
|
4046
|
+
if (attempt < 3) await new Promise((r) => setTimeout(r, 250 * Math.pow(2, attempt)));
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
for (const sp of serverProducts) {
|
|
4051
|
+
if (!productsMap.has(sp.storeProductId)) {
|
|
4052
|
+
const prod = buildProductFromServerInfo(sp);
|
|
4053
|
+
productsMap.set(prod.productId, prod);
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
return productsMap;
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
// src/utils/preloadCache.ts
|
|
4060
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
4061
|
+
function isCacheEntryValid(entry) {
|
|
4062
|
+
return !!entry && Date.now() - entry.preloadedAt < CACHE_TTL_MS;
|
|
4063
|
+
}
|
|
4064
|
+
function consumeCacheEntry(cache, placement) {
|
|
4065
|
+
const entry = cache.get(placement);
|
|
4066
|
+
if (!isCacheEntryValid(entry)) return null;
|
|
4067
|
+
cache.delete(placement);
|
|
4068
|
+
return entry ?? null;
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
// src/hooks/useCampaignPreload.ts
|
|
4072
|
+
function useCampaignPreload() {
|
|
4073
|
+
const [preloadedWebView, setPreloadedWebView] = useState8(null);
|
|
4074
|
+
const preloadedCampaignsRef = useRef6(/* @__PURE__ */ new Map());
|
|
4075
|
+
const isPreloadValid = useCallback8(
|
|
4076
|
+
(placement) => isCacheEntryValid(preloadedCampaignsRef.current.get(placement)),
|
|
4077
|
+
[]
|
|
4078
|
+
);
|
|
4079
|
+
const consumePreloadedCampaign = useCallback8(
|
|
4080
|
+
(placement) => consumeCacheEntry(preloadedCampaignsRef.current, placement),
|
|
4081
|
+
[]
|
|
4082
|
+
);
|
|
4083
|
+
const preloadCampaign = useCallback8(
|
|
4084
|
+
async (placement, context) => {
|
|
4085
|
+
try {
|
|
4086
|
+
const campaign = await PaywalloClient.getCampaign(placement, context);
|
|
4087
|
+
if (!campaign?.paywall) {
|
|
4088
|
+
return {
|
|
4089
|
+
success: false,
|
|
4090
|
+
error: new CampaignError(
|
|
4091
|
+
CAMPAIGN_ERROR_CODES.NOT_FOUND,
|
|
4092
|
+
`Campaign or paywall not found: ${placement}`
|
|
4093
|
+
)
|
|
4094
|
+
};
|
|
4095
|
+
}
|
|
4096
|
+
const { paywall } = campaign;
|
|
4097
|
+
const serverProducts = [paywall.primaryProduct, paywall.secondaryProduct].filter(Boolean);
|
|
4098
|
+
const storeProductIds = serverProducts.map((p) => p.storeProductId).filter(Boolean);
|
|
4099
|
+
const productsMap = await loadCampaignProducts(
|
|
4100
|
+
serverProducts.map((p) => p),
|
|
4101
|
+
storeProductIds
|
|
4102
|
+
);
|
|
4103
|
+
preloadedCampaignsRef.current.set(placement, {
|
|
4104
|
+
campaignId: campaign.campaignId,
|
|
4105
|
+
variantKey: campaign.variantKey,
|
|
4106
|
+
paywall: {
|
|
4107
|
+
id: paywall.id,
|
|
4108
|
+
placement: paywall.placement,
|
|
4109
|
+
config: paywall.config,
|
|
4110
|
+
primaryProductId: paywall.primaryProductId,
|
|
4111
|
+
secondaryProductId: paywall.secondaryProductId
|
|
4112
|
+
},
|
|
4113
|
+
products: productsMap,
|
|
4114
|
+
preloadedAt: Date.now()
|
|
4115
|
+
});
|
|
4116
|
+
const craftData = paywall.config?.craftData ?? "";
|
|
4117
|
+
setPreloadedWebView({
|
|
4118
|
+
placement,
|
|
4119
|
+
paywallId: paywall.id,
|
|
4120
|
+
craftData,
|
|
4121
|
+
products: productsMap,
|
|
4122
|
+
primaryProductId: paywall.primaryProductId,
|
|
4123
|
+
secondaryProductId: paywall.secondaryProductId,
|
|
4124
|
+
loaded: false,
|
|
4125
|
+
campaignId: campaign.campaignId,
|
|
4126
|
+
variantKey: campaign.variantKey
|
|
4127
|
+
});
|
|
4128
|
+
return { success: true };
|
|
4129
|
+
} catch (error) {
|
|
4130
|
+
return {
|
|
4131
|
+
success: false,
|
|
4132
|
+
error: error instanceof Error ? error : new CampaignError(CAMPAIGN_ERROR_CODES.FETCH_FAILED, String(error))
|
|
4133
|
+
};
|
|
4134
|
+
}
|
|
4135
|
+
},
|
|
4136
|
+
[]
|
|
4137
|
+
);
|
|
4138
|
+
return {
|
|
4139
|
+
preloadedCampaignsRef,
|
|
4140
|
+
preloadedWebView,
|
|
4141
|
+
setPreloadedWebView,
|
|
4142
|
+
preloadCampaign,
|
|
4143
|
+
consumePreloadedCampaign,
|
|
4144
|
+
isPreloadValid
|
|
4145
|
+
};
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
// src/hooks/useProductLoader.ts
|
|
4149
|
+
import { useCallback as useCallback9, useState as useState9 } from "react";
|
|
4150
|
+
function useProductLoader() {
|
|
4151
|
+
const [products, setProducts] = useState9(/* @__PURE__ */ new Map());
|
|
4152
|
+
const [isLoadingProducts, setIsLoadingProducts] = useState9(false);
|
|
4153
|
+
const refreshProducts = useCallback9(async (productIds) => {
|
|
4154
|
+
setIsLoadingProducts(true);
|
|
4155
|
+
try {
|
|
4156
|
+
const iapService = getIAPService();
|
|
4157
|
+
const loaded = await iapService.loadProducts(productIds);
|
|
4158
|
+
const map = /* @__PURE__ */ new Map();
|
|
4159
|
+
for (const p of loaded) map.set(p.productId, p);
|
|
4160
|
+
setProducts(map);
|
|
4161
|
+
} catch (error) {
|
|
4162
|
+
if (PaywalloClient.getConfig()?.debug) console.warn("[Panel] Product load failed:", error);
|
|
4163
|
+
} finally {
|
|
4164
|
+
setIsLoadingProducts(false);
|
|
4165
|
+
}
|
|
4166
|
+
}, []);
|
|
4167
|
+
return { products, isLoadingProducts, refreshProducts };
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
// src/hooks/usePurchaseOrchestrator.ts
|
|
4171
|
+
import { useCallback as useCallback10 } from "react";
|
|
4172
|
+
function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef) {
|
|
4173
|
+
const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
|
|
4174
|
+
const handlePreloadedPurchase = useCallback10(
|
|
4175
|
+
async (productId) => {
|
|
4176
|
+
try {
|
|
4177
|
+
if (preloadedWebView) {
|
|
4178
|
+
trackProductSelected({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, productId, variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
|
|
4179
|
+
}
|
|
4180
|
+
const result = await getIAPService().purchase(productId, {
|
|
4181
|
+
paywallPlacement: preloadedWebView?.placement,
|
|
4182
|
+
variantKey: preloadedWebView?.variantKey
|
|
4183
|
+
});
|
|
4184
|
+
if (!result.success) {
|
|
4185
|
+
if (result.error) {
|
|
4186
|
+
setShowingPreloadedWebView(false);
|
|
4187
|
+
clearPreloadedWebView();
|
|
4188
|
+
resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
|
|
4189
|
+
}
|
|
4190
|
+
return;
|
|
4191
|
+
}
|
|
4192
|
+
if (preloadedWebView) {
|
|
4193
|
+
const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
|
|
4194
|
+
await trackDismissed({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, durationSeconds, action: "purchase", variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
|
|
4195
|
+
await trackPurchased({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, productId, variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
|
|
4196
|
+
}
|
|
4197
|
+
setShowingPreloadedWebView(false);
|
|
4198
|
+
clearPreloadedWebView();
|
|
4199
|
+
resolvePreloadedPaywall({ presented: true, purchased: true, cancelled: false, restored: false });
|
|
4200
|
+
} catch (error) {
|
|
4201
|
+
if (PaywalloClient.getConfig()?.debug) console.warn("[Panel] Preloaded purchase error:", error);
|
|
4202
|
+
setShowingPreloadedWebView(false);
|
|
4203
|
+
clearPreloadedWebView();
|
|
4204
|
+
resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
|
|
4205
|
+
}
|
|
4206
|
+
},
|
|
4207
|
+
[preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased]
|
|
4208
|
+
);
|
|
4209
|
+
const handlePreloadedRestore = useCallback10(async () => {
|
|
4210
|
+
try {
|
|
4211
|
+
const transactions = await getIAPService().restore();
|
|
4212
|
+
if (transactions.length === 0) return;
|
|
4213
|
+
if (preloadedWebView) {
|
|
4214
|
+
const durationSeconds = Math.round((Date.now() - presentedAtRef.current) / 1e3);
|
|
4215
|
+
await trackDismissed({ placement: preloadedWebView.placement, paywallId: preloadedWebView.paywallId, durationSeconds, action: "restore", variantKey: preloadedWebView.variantKey, campaignId: preloadedWebView.campaignId });
|
|
4216
|
+
}
|
|
4217
|
+
setShowingPreloadedWebView(false);
|
|
4218
|
+
clearPreloadedWebView();
|
|
4219
|
+
resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: true });
|
|
4220
|
+
} catch (error) {
|
|
4221
|
+
if (PaywalloClient.getConfig()?.debug) console.warn("[Panel] Preloaded restore error:", error);
|
|
4222
|
+
setShowingPreloadedWebView(false);
|
|
4223
|
+
clearPreloadedWebView();
|
|
4224
|
+
resolvePreloadedPaywall({ presented: true, purchased: false, cancelled: false, restored: false });
|
|
4225
|
+
}
|
|
4226
|
+
}, [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed]);
|
|
4227
|
+
return { handlePreloadedPurchase, handlePreloadedRestore };
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
// src/hooks/useSubscriptionSync.ts
|
|
4231
|
+
import { useCallback as useCallback11, useRef as useRef7, useState as useState10 } from "react";
|
|
4232
|
+
var CACHE_TTL_MS2 = 5 * 60 * 1e3;
|
|
4233
|
+
var ACTIVE_STATUSES = ["active", "in_grace_period"];
|
|
4234
|
+
function useSubscriptionSync() {
|
|
4235
|
+
const [subscription, setSubscription] = useState10(null);
|
|
4236
|
+
const cacheRef = useRef7(null);
|
|
4237
|
+
const invalidateCache = useCallback11(() => {
|
|
4238
|
+
cacheRef.current = null;
|
|
4239
|
+
}, []);
|
|
4240
|
+
const fetchAndCache = useCallback11(async () => {
|
|
4241
|
+
const apiClient = PaywalloClient.getApiClient();
|
|
4242
|
+
const distinctId = PaywalloClient.getDistinctId();
|
|
4243
|
+
if (!apiClient || !distinctId) return null;
|
|
4244
|
+
const status = await apiClient.getSubscriptionStatus(distinctId);
|
|
4245
|
+
const sub = status.subscription ? {
|
|
4246
|
+
...status.subscription,
|
|
4247
|
+
expiresAt: status.subscription.expiresAt ? new Date(status.subscription.expiresAt) : null
|
|
4248
|
+
} : null;
|
|
4249
|
+
cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
|
|
4250
|
+
setSubscription(sub);
|
|
4251
|
+
return sub;
|
|
4252
|
+
}, []);
|
|
4253
|
+
const hasActiveSubscription = useCallback11(async () => {
|
|
4254
|
+
const apiClient = PaywalloClient.getApiClient();
|
|
4255
|
+
const distinctId = PaywalloClient.getDistinctId();
|
|
4256
|
+
if (!apiClient || !distinctId) return false;
|
|
4257
|
+
const cached = cacheRef.current;
|
|
4258
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
4259
|
+
if (!cached.data) return false;
|
|
4260
|
+
if (!ACTIVE_STATUSES.includes(cached.data.status)) return false;
|
|
4261
|
+
if (cached.data.expiresAt && cached.data.expiresAt < /* @__PURE__ */ new Date()) return false;
|
|
4262
|
+
return true;
|
|
4263
|
+
}
|
|
4264
|
+
try {
|
|
4265
|
+
const status = await apiClient.getSubscriptionStatus(distinctId);
|
|
4266
|
+
const sub = status.subscription ? {
|
|
4267
|
+
...status.subscription,
|
|
4268
|
+
expiresAt: status.subscription.expiresAt ? new Date(status.subscription.expiresAt) : null
|
|
4269
|
+
} : null;
|
|
4270
|
+
cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
|
|
4271
|
+
setSubscription(sub);
|
|
4272
|
+
return status.hasActiveSubscription;
|
|
4273
|
+
} catch {
|
|
4274
|
+
return false;
|
|
4275
|
+
}
|
|
4276
|
+
}, []);
|
|
4277
|
+
const getSubscription = useCallback11(async () => {
|
|
4278
|
+
const apiClient = PaywalloClient.getApiClient();
|
|
4279
|
+
const distinctId = PaywalloClient.getDistinctId();
|
|
4280
|
+
if (!apiClient || !distinctId) return null;
|
|
4281
|
+
const cached = cacheRef.current;
|
|
4282
|
+
if (cached && cached.expiresAt > Date.now()) return cached.data;
|
|
4283
|
+
try {
|
|
4284
|
+
return await fetchAndCache();
|
|
4285
|
+
} catch {
|
|
4286
|
+
return null;
|
|
4287
|
+
}
|
|
4288
|
+
}, [fetchAndCache]);
|
|
4289
|
+
return { subscription, hasActiveSubscription, getSubscription, invalidateCache };
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
// src/PaywalloProvider.tsx
|
|
4293
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
4294
|
+
function PaywalloProvider({ children, config }) {
|
|
4295
|
+
const [isInitialized, setIsInitialized] = useState11(false);
|
|
4296
|
+
const [initError, setInitError] = useState11(null);
|
|
4297
|
+
const [distinctId, setDistinctId] = useState11(null);
|
|
4298
|
+
const [activePaywall, setActivePaywall] = useState11(null);
|
|
4299
|
+
const { products, isLoadingProducts, refreshProducts } = useProductLoader();
|
|
4300
|
+
const { subscription, hasActiveSubscription, getSubscription, invalidateCache } = useSubscriptionSync();
|
|
4301
|
+
const { preloadedWebView, setPreloadedWebView, preloadCampaign, consumePreloadedCampaign, isPreloadValid } = useCampaignPreload();
|
|
4302
|
+
const clearPreloadedWebView = useCallback12(() => setPreloadedWebView(null), [setPreloadedWebView]);
|
|
4303
|
+
const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView);
|
|
4304
|
+
const { handlePreloadedPurchase: rawPreloadedPurchase, handlePreloadedRestore } = usePurchaseOrchestrator(
|
|
4305
|
+
preloadedWebView,
|
|
4306
|
+
resolvePreloadedPaywall,
|
|
4307
|
+
clearPreloadedWebView,
|
|
4308
|
+
setShowingPreloadedWebView,
|
|
4309
|
+
presentedAtRef
|
|
4310
|
+
);
|
|
4311
|
+
const [isPreloadPurchasing, setIsPreloadPurchasing] = useState11(false);
|
|
4312
|
+
const handlePreloadedPurchase = useCallback12(async (productId) => {
|
|
4313
|
+
setIsPreloadPurchasing(true);
|
|
4314
|
+
try {
|
|
4315
|
+
await rawPreloadedPurchase(productId);
|
|
4316
|
+
} finally {
|
|
4317
|
+
setIsPreloadPurchasing(false);
|
|
4318
|
+
}
|
|
4319
|
+
}, [rawPreloadedPurchase]);
|
|
4320
|
+
useEffect7(() => {
|
|
4321
|
+
initLocalization({ detectDevice: true });
|
|
4322
|
+
PaywalloClient.init(config).then(() => {
|
|
4323
|
+
setDistinctId(PaywalloClient.getDistinctId());
|
|
4324
|
+
setIsInitialized(true);
|
|
4325
|
+
}).catch((err) => {
|
|
4326
|
+
setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
|
|
4327
|
+
});
|
|
4328
|
+
}, [config]);
|
|
4329
|
+
const getPaywallConfig = useCallback12(async (p) => {
|
|
4330
|
+
const api = PaywalloClient.getApiClient();
|
|
4331
|
+
return api ? api.getPaywall(p) : null;
|
|
4332
|
+
}, []);
|
|
4333
|
+
const presentPaywall = useCallback12((placement) => {
|
|
4334
|
+
return new Promise((resolve) => setActivePaywall({ placement, resolver: resolve }));
|
|
4335
|
+
}, []);
|
|
4336
|
+
const trackCampaignImpression = useCallback12((placement, variantKey, campaignId) => {
|
|
4337
|
+
const api = PaywalloClient.getApiClient();
|
|
4338
|
+
if (!api) return;
|
|
4339
|
+
const sessionId = PaywalloClient.getSessionId();
|
|
4340
|
+
void api.trackEvent("$campaign_impression", PaywalloClient.getDistinctId(), { placement, variantKey, campaignId, ...sessionId && { sessionId } }).catch(() => {
|
|
4341
|
+
});
|
|
4342
|
+
}, []);
|
|
4343
|
+
const presentCampaign = useCallback12(
|
|
4344
|
+
async (placement, context) => {
|
|
4345
|
+
try {
|
|
4346
|
+
if (preloadedWebView?.placement === placement && preloadedWebView.loaded && isPreloadValid(placement)) {
|
|
4347
|
+
consumePreloadedCampaign(placement);
|
|
4348
|
+
const apiClient = PaywalloClient.getApiClient();
|
|
4349
|
+
if (apiClient) {
|
|
4350
|
+
const sessionId = PaywalloClient.getSessionId();
|
|
4351
|
+
void apiClient.trackEvent("$paywall_viewed", PaywalloClient.getDistinctId(), {
|
|
4352
|
+
placement,
|
|
4353
|
+
paywallId: preloadedWebView.paywallId,
|
|
4354
|
+
preloaded: true,
|
|
4355
|
+
variantKey: preloadedWebView.variantKey ?? "",
|
|
4356
|
+
campaignId: preloadedWebView.campaignId ?? "",
|
|
4357
|
+
...sessionId && { sessionId }
|
|
4358
|
+
});
|
|
4359
|
+
}
|
|
4360
|
+
trackCampaignImpression(placement, preloadedWebView.variantKey ?? "", preloadedWebView.campaignId ?? "");
|
|
4361
|
+
const r2 = await presentPreloadedWebView();
|
|
4362
|
+
return { ...r2, campaignId: preloadedWebView.campaignId ?? "", variantKey: preloadedWebView.variantKey ?? "" };
|
|
4363
|
+
}
|
|
4364
|
+
const preloaded = consumePreloadedCampaign(placement);
|
|
4365
|
+
if (preloaded) {
|
|
4366
|
+
trackCampaignImpression(placement, preloaded.variantKey, preloaded.campaignId);
|
|
4367
|
+
const r2 = await new Promise((resolve) => setActivePaywall({
|
|
4368
|
+
placement: preloaded.paywall.placement,
|
|
4369
|
+
resolver: resolve,
|
|
4370
|
+
paywallConfig: preloaded.paywall,
|
|
4371
|
+
preloadedProducts: preloaded.products,
|
|
4372
|
+
variantKey: preloaded.variantKey,
|
|
4373
|
+
campaignId: preloaded.campaignId
|
|
4374
|
+
}));
|
|
4375
|
+
return { ...r2, campaignId: preloaded.campaignId, variantKey: preloaded.variantKey };
|
|
4376
|
+
}
|
|
4377
|
+
const campaign = PaywalloClient.getPreloadedCampaign(placement) ?? await PaywalloClient.getCampaign(placement, context);
|
|
4378
|
+
if (!campaign?.paywall) return {
|
|
4379
|
+
presented: false,
|
|
4380
|
+
purchased: false,
|
|
4381
|
+
cancelled: false,
|
|
4382
|
+
restored: false,
|
|
4383
|
+
error: new CampaignError(campaign ? CAMPAIGN_ERROR_CODES.PRELOAD_FAILED : CAMPAIGN_ERROR_CODES.NOT_FOUND, campaign ? `No paywall: ${placement}` : `Campaign not found: ${placement}`)
|
|
4384
|
+
};
|
|
4385
|
+
trackCampaignImpression(placement, campaign.variantKey, campaign.campaignId);
|
|
4386
|
+
const r = await new Promise((resolve) => setActivePaywall({
|
|
4387
|
+
placement: campaign.paywall.placement,
|
|
4388
|
+
resolver: resolve,
|
|
4389
|
+
paywallConfig: {
|
|
4390
|
+
id: campaign.paywall.id,
|
|
4391
|
+
placement: campaign.paywall.placement,
|
|
4392
|
+
config: campaign.paywall.config,
|
|
4393
|
+
primaryProductId: campaign.paywall.primaryProductId,
|
|
4394
|
+
secondaryProductId: campaign.paywall.secondaryProductId
|
|
4395
|
+
},
|
|
4396
|
+
variantKey: campaign.variantKey,
|
|
4397
|
+
campaignId: campaign.campaignId
|
|
4398
|
+
}));
|
|
4399
|
+
return { ...r, campaignId: campaign.campaignId, variantKey: campaign.variantKey };
|
|
4400
|
+
} catch (error) {
|
|
4401
|
+
return {
|
|
4402
|
+
presented: false,
|
|
4403
|
+
purchased: false,
|
|
4404
|
+
cancelled: false,
|
|
4405
|
+
restored: false,
|
|
4406
|
+
error: error instanceof Error ? error : new CampaignError(CAMPAIGN_ERROR_CODES.PRESENT_FAILED, String(error))
|
|
4407
|
+
};
|
|
4408
|
+
}
|
|
4409
|
+
},
|
|
4410
|
+
[preloadedWebView, isPreloadValid, consumePreloadedCampaign, presentPreloadedWebView, trackCampaignImpression]
|
|
4411
|
+
);
|
|
4412
|
+
const restorePurchases = useCallback12(async () => {
|
|
4413
|
+
invalidateCache();
|
|
4414
|
+
try {
|
|
4415
|
+
const txs = await getIAPService().restore();
|
|
4416
|
+
return txs.length > 0 ? { success: true, restoredProducts: txs.map((tx) => tx.productId) } : { success: false, restoredProducts: [] };
|
|
4417
|
+
} catch (error) {
|
|
4418
|
+
return { success: false, restoredProducts: [], error: error instanceof Error ? error : new PurchaseError(PURCHASE_ERROR_CODES.RESTORE_FAILED, String(error)) };
|
|
4419
|
+
}
|
|
4420
|
+
}, [invalidateCache]);
|
|
4421
|
+
const handlePaywallResult = useCallback12((result) => {
|
|
4422
|
+
if (activePaywall) {
|
|
4423
|
+
activePaywall.resolver(result);
|
|
4424
|
+
setActivePaywall(null);
|
|
4425
|
+
}
|
|
4426
|
+
}, [activePaywall]);
|
|
4427
|
+
useEffect7(() => {
|
|
4428
|
+
if (!isInitialized) return;
|
|
4429
|
+
const onEmergency = async (id) => {
|
|
4430
|
+
try {
|
|
4431
|
+
await presentPaywall(id);
|
|
4432
|
+
} catch {
|
|
4433
|
+
}
|
|
4434
|
+
};
|
|
4435
|
+
PaywalloClient.registerPaywallPresenter(presentPaywall);
|
|
4436
|
+
PaywalloClient.registerSubscriptionGetter(getSubscription);
|
|
4437
|
+
PaywalloClient.registerActiveChecker(hasActiveSubscription);
|
|
4438
|
+
PaywalloClient.registerRestoreHandler(restorePurchases);
|
|
4439
|
+
PaywalloClient.registerEmergencyPaywallHandler(onEmergency);
|
|
4440
|
+
}, [isInitialized, presentPaywall, getSubscription, hasActiveSubscription, restorePurchases]);
|
|
4441
|
+
useEffect7(() => {
|
|
4442
|
+
if (activePaywall) setPreloadedWebView(null);
|
|
4443
|
+
}, [activePaywall, setPreloadedWebView]);
|
|
4444
|
+
const noOp = useCallback12(() => {
|
|
4445
|
+
}, []);
|
|
4446
|
+
const contextValue = useMemo2(() => ({
|
|
4447
|
+
config: PaywalloClient.getConfig(),
|
|
4448
|
+
isInitialized,
|
|
4449
|
+
initError,
|
|
4450
|
+
distinctId,
|
|
4451
|
+
subscription,
|
|
4452
|
+
products,
|
|
4453
|
+
isLoadingProducts,
|
|
4454
|
+
presentPaywall,
|
|
4455
|
+
presentCampaign,
|
|
4456
|
+
preloadCampaign,
|
|
4457
|
+
hasActiveSubscription,
|
|
4458
|
+
getSubscription,
|
|
4459
|
+
restorePurchases,
|
|
4460
|
+
getPaywallConfig,
|
|
4461
|
+
refreshProducts
|
|
4462
|
+
}), [
|
|
4463
|
+
isInitialized,
|
|
4464
|
+
initError,
|
|
4465
|
+
distinctId,
|
|
4466
|
+
subscription,
|
|
4467
|
+
products,
|
|
4468
|
+
isLoadingProducts,
|
|
4469
|
+
presentPaywall,
|
|
4470
|
+
presentCampaign,
|
|
4471
|
+
preloadCampaign,
|
|
4472
|
+
hasActiveSubscription,
|
|
4473
|
+
getSubscription,
|
|
4474
|
+
restorePurchases,
|
|
4475
|
+
getPaywallConfig,
|
|
4476
|
+
refreshProducts
|
|
4477
|
+
]);
|
|
4478
|
+
const SCREEN_HEIGHT3 = useMemo2(() => Dimensions4.get("window").height, []);
|
|
4479
|
+
const slideAnim = useRef8(new Animated3.Value(SCREEN_HEIGHT3)).current;
|
|
4480
|
+
useEffect7(() => {
|
|
4481
|
+
if (showingPreloadedWebView) {
|
|
4482
|
+
slideAnim.setValue(SCREEN_HEIGHT3);
|
|
4483
|
+
Animated3.timing(slideAnim, { toValue: 0, duration: 320, easing: Easing2.out(Easing2.cubic), useNativeDriver: true }).start();
|
|
4484
|
+
} else {
|
|
4485
|
+
slideAnim.setValue(SCREEN_HEIGHT3);
|
|
4486
|
+
}
|
|
4487
|
+
}, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT3]);
|
|
4488
|
+
const preloadStyle = [
|
|
4489
|
+
styles3.preloadOverlay,
|
|
4490
|
+
showingPreloadedWebView ? styles3.visible : styles3.hidden,
|
|
4491
|
+
{ transform: [{ translateY: showingPreloadedWebView ? slideAnim : SCREEN_HEIGHT3 }] }
|
|
4492
|
+
];
|
|
4493
|
+
return /* @__PURE__ */ jsxs2(PaywalloContext.Provider, { value: contextValue, children: [
|
|
4494
|
+
children,
|
|
4495
|
+
/* @__PURE__ */ jsx3(PaywallErrorBoundary, { children: !!preloadedWebView?.craftData && /* @__PURE__ */ jsx3(Animated3.View, { style: preloadStyle, pointerEvents: showingPreloadedWebView ? "auto" : "none", children: /* @__PURE__ */ jsx3(
|
|
4496
|
+
PaywallWebView,
|
|
4497
|
+
{
|
|
4498
|
+
paywallId: preloadedWebView.paywallId,
|
|
4499
|
+
craftData: preloadedWebView.craftData,
|
|
4500
|
+
products: preloadedWebView.products,
|
|
4501
|
+
primaryProductId: preloadedWebView.primaryProductId,
|
|
4502
|
+
secondaryProductId: preloadedWebView.secondaryProductId,
|
|
4503
|
+
isPurchasing: isPreloadPurchasing,
|
|
4504
|
+
onReady: handlePreloadedWebViewReady,
|
|
4505
|
+
onClose: showingPreloadedWebView ? handlePreloadedClose : noOp,
|
|
4506
|
+
onPurchase: showingPreloadedWebView ? handlePreloadedPurchase : noOp,
|
|
4507
|
+
onRestore: showingPreloadedWebView ? handlePreloadedRestore : noOp
|
|
4508
|
+
}
|
|
4509
|
+
) }) }),
|
|
4510
|
+
/* @__PURE__ */ jsx3(PaywallErrorBoundary, { children: activePaywall && /* @__PURE__ */ jsx3(
|
|
4511
|
+
PaywallModal,
|
|
4512
|
+
{
|
|
4513
|
+
placement: activePaywall.placement,
|
|
4514
|
+
visible: true,
|
|
4515
|
+
onResult: handlePaywallResult,
|
|
4516
|
+
paywallConfig: activePaywall.paywallConfig,
|
|
4517
|
+
preloadedProducts: activePaywall.preloadedProducts,
|
|
4518
|
+
variantKey: activePaywall.variantKey,
|
|
4519
|
+
campaignId: activePaywall.campaignId
|
|
4520
|
+
}
|
|
4521
|
+
) })
|
|
4522
|
+
] });
|
|
4523
|
+
}
|
|
4524
|
+
var styles3 = StyleSheet3.create({
|
|
4525
|
+
preloadOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#ffffff" },
|
|
4526
|
+
hidden: { zIndex: -1 },
|
|
4527
|
+
visible: { zIndex: 9999 }
|
|
4528
|
+
});
|
|
4529
|
+
|
|
4530
|
+
// src/utils/productVariableResolver.ts
|
|
4531
|
+
function getProductVariable(product, property) {
|
|
4532
|
+
if (!product) return "";
|
|
4533
|
+
switch (property) {
|
|
4534
|
+
case "id":
|
|
4535
|
+
return product.productId;
|
|
4536
|
+
case "name":
|
|
4537
|
+
return product.title;
|
|
4538
|
+
case "price":
|
|
4539
|
+
return product.localizedPrice;
|
|
4540
|
+
case "pricePerMonth":
|
|
4541
|
+
return product.pricePerMonth ?? product.localizedPrice;
|
|
4542
|
+
case "pricePerWeek":
|
|
4543
|
+
return product.pricePerWeek ?? "";
|
|
4544
|
+
case "pricePerDay":
|
|
4545
|
+
return product.pricePerDay ?? "";
|
|
4546
|
+
case "period":
|
|
4547
|
+
return product.subscriptionPeriod ?? "";
|
|
4548
|
+
case "trialPeriod":
|
|
4549
|
+
return product.freeTrialPeriod ?? "";
|
|
4550
|
+
case "introPrice":
|
|
4551
|
+
return product.introductoryPrice ?? "";
|
|
4552
|
+
case "savings":
|
|
4553
|
+
return product.savings ?? "";
|
|
4554
|
+
case "savingsPercent":
|
|
4555
|
+
return product.savingsPercent ?? "";
|
|
4556
|
+
case "hasFreeTrial":
|
|
4557
|
+
return product.freeTrialPeriod ? "true" : "false";
|
|
4558
|
+
case "hasIntroOffer":
|
|
4559
|
+
return product.introductoryPrice ? "true" : "false";
|
|
4560
|
+
case "renewalPeriod":
|
|
4561
|
+
return product.subscriptionPeriod ?? "";
|
|
4562
|
+
default:
|
|
4563
|
+
return "";
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
function resolveProductsVariable(parts, context) {
|
|
4567
|
+
if (parts[0] !== "products") return null;
|
|
4568
|
+
if (parts[1] === "selected" && context.selectedProductId) {
|
|
4569
|
+
return getProductVariable(context.products.get(context.selectedProductId), parts[2]);
|
|
4570
|
+
}
|
|
4571
|
+
if (parts[1] === "primary" && context.primaryProductId) {
|
|
4572
|
+
return getProductVariable(context.products.get(context.primaryProductId), parts[2]);
|
|
4573
|
+
}
|
|
4574
|
+
if (parts[1] === "secondary" && context.secondaryProductId) {
|
|
4575
|
+
return getProductVariable(context.products.get(context.secondaryProductId), parts[2]);
|
|
4576
|
+
}
|
|
4577
|
+
if (parts[1] === "hasIntroductoryOffer") {
|
|
4578
|
+
return Array.from(context.products.values()).some((p) => p.introductoryPrice) ? "true" : "false";
|
|
4579
|
+
}
|
|
4580
|
+
return null;
|
|
4581
|
+
}
|
|
4582
|
+
function resolveLegacyVariable(variableName, context) {
|
|
4583
|
+
if (variableName === "device_name") return context.deviceInfo?.name ?? "";
|
|
4584
|
+
if (variableName === "product_price" && context.selectedProductId) {
|
|
4585
|
+
return context.products.get(context.selectedProductId)?.localizedPrice ?? "";
|
|
4586
|
+
}
|
|
4587
|
+
if (variableName === "trial_period" && context.selectedProductId) {
|
|
4588
|
+
return context.products.get(context.selectedProductId)?.freeTrialPeriod ?? "";
|
|
4589
|
+
}
|
|
4590
|
+
return null;
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4593
|
+
// src/utils/variableResolver.ts
|
|
4594
|
+
function resolveVariable(variableName, context) {
|
|
4595
|
+
const parts = variableName.split(".");
|
|
4596
|
+
if (parts[0] === "device" && context.deviceInfo) {
|
|
4597
|
+
switch (parts[1]) {
|
|
4598
|
+
case "name":
|
|
4599
|
+
return context.deviceInfo.name ?? "";
|
|
4600
|
+
case "model":
|
|
4601
|
+
return context.deviceInfo.model ?? "";
|
|
4602
|
+
case "os":
|
|
4603
|
+
return context.deviceInfo.os ?? "";
|
|
4604
|
+
case "osVersion":
|
|
4605
|
+
return context.deviceInfo.osVersion ?? "";
|
|
4606
|
+
case "locale":
|
|
4607
|
+
return context.deviceInfo.locale ?? "";
|
|
4608
|
+
default:
|
|
4609
|
+
return "";
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
if (parts[0] === "user" && context.userInfo) {
|
|
4613
|
+
switch (parts[1]) {
|
|
4614
|
+
case "id":
|
|
4615
|
+
return context.userInfo.id ?? "";
|
|
4616
|
+
case "name":
|
|
4617
|
+
return context.userInfo.name ?? "";
|
|
4618
|
+
case "email":
|
|
4619
|
+
return context.userInfo.email ?? "";
|
|
4620
|
+
default:
|
|
4621
|
+
return "";
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
if (parts[0] === "products") {
|
|
4625
|
+
return resolveProductsVariable(parts, context) ?? "";
|
|
4626
|
+
}
|
|
4627
|
+
return resolveLegacyVariable(variableName, context) ?? context.customVariables?.[variableName] ?? "";
|
|
4628
|
+
}
|
|
4629
|
+
function resolveText(text, context) {
|
|
4630
|
+
const regex = /\{\{([^}]+)\}\}/g;
|
|
4631
|
+
return text.replace(regex, (_, variableName) => {
|
|
4632
|
+
return resolveVariable(variableName.trim(), context);
|
|
4633
|
+
});
|
|
4634
|
+
}
|
|
4635
|
+
function parseVariables(text) {
|
|
4636
|
+
const regex = /\{\{([^}]+)\}\}/g;
|
|
4637
|
+
const variables = [];
|
|
4638
|
+
let match;
|
|
4639
|
+
while ((match = regex.exec(text)) !== null) {
|
|
4640
|
+
variables.push(match[1].trim());
|
|
4641
|
+
}
|
|
4642
|
+
return variables;
|
|
4643
|
+
}
|
|
4644
|
+
function hasVariables(text) {
|
|
4645
|
+
return /\{\{[^}]+\}\}/.test(text);
|
|
4646
|
+
}
|
|
4647
|
+
|
|
4648
|
+
// src/index.ts
|
|
4649
|
+
var Paywallo = PaywalloClient;
|
|
4650
|
+
var index_default = Paywallo;
|
|
4651
|
+
export {
|
|
4652
|
+
AFFILIATE_ERROR_CODES,
|
|
4653
|
+
ANALYTICS_ERROR_CODES,
|
|
4654
|
+
AffiliateError,
|
|
4655
|
+
AffiliateManager,
|
|
4656
|
+
AnalyticsError,
|
|
4657
|
+
ApiClient,
|
|
4658
|
+
CAMPAIGN_ERROR_CODES,
|
|
4659
|
+
CampaignError,
|
|
4660
|
+
PURCHASE_ERROR_CODES as ERROR_CODES,
|
|
4661
|
+
HttpClient,
|
|
4662
|
+
IAPService,
|
|
4663
|
+
IDENTITY_ERROR_CODES,
|
|
4664
|
+
IdentityError,
|
|
4665
|
+
IdentityManager,
|
|
4666
|
+
PAYWALL_ERROR_CODES,
|
|
4667
|
+
PURCHASE_ERROR_CODES,
|
|
4668
|
+
PaywallContext,
|
|
4669
|
+
PaywallError,
|
|
4670
|
+
PaywallModal,
|
|
4671
|
+
Paywallo,
|
|
4672
|
+
PaywalloAffiliate,
|
|
4673
|
+
PaywalloClient,
|
|
4674
|
+
PaywalloContext,
|
|
4675
|
+
PaywalloError,
|
|
4676
|
+
PaywalloProvider,
|
|
4677
|
+
PurchaseError,
|
|
4678
|
+
SESSION_ERROR_CODES,
|
|
4679
|
+
SessionError,
|
|
4680
|
+
SessionManager,
|
|
4681
|
+
SubscriptionCache,
|
|
4682
|
+
affiliateManager,
|
|
4683
|
+
createPurchaseError,
|
|
4684
|
+
index_default as default,
|
|
4685
|
+
detectDeviceLanguage,
|
|
4686
|
+
getCurrentLanguage,
|
|
4687
|
+
getDefaultLanguage,
|
|
4688
|
+
getIAPService,
|
|
4689
|
+
getLocalizedString,
|
|
4690
|
+
hasVariables,
|
|
4691
|
+
identityManager,
|
|
4692
|
+
initLocalization,
|
|
4693
|
+
nativeStoreKit,
|
|
4694
|
+
networkMonitor,
|
|
4695
|
+
offlineQueue,
|
|
4696
|
+
parseVariables,
|
|
4697
|
+
queueProcessor,
|
|
4698
|
+
resolveText,
|
|
4699
|
+
sessionManager,
|
|
4700
|
+
setCurrentLanguage,
|
|
4701
|
+
setDefaultLanguage,
|
|
4702
|
+
subscriptionManager,
|
|
4703
|
+
usePaywallContext,
|
|
4704
|
+
usePaywallo,
|
|
4705
|
+
useProducts,
|
|
4706
|
+
usePurchase,
|
|
4707
|
+
useSubscription
|
|
4708
|
+
};
|
|
4709
|
+
//# sourceMappingURL=index.mjs.map
|