@namiml/sdk-core 3.4.12-dev.202609251647 → 3.4.12-dev.202609281806
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +752 -79
- package/dist/index.d.ts +257 -15
- package/dist/index.mjs +753 -80
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -104,7 +104,7 @@ const {
|
|
|
104
104
|
// version — stamped by scripts/version.sh
|
|
105
105
|
NAMI_SDK_VERSION = "3.4.12",
|
|
106
106
|
// full package version including dev suffix — stamped by scripts/version.sh
|
|
107
|
-
NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.
|
|
107
|
+
NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.202609281806",
|
|
108
108
|
// environments
|
|
109
109
|
PRODUCTION = "production", DEVELOPMENT = "development",
|
|
110
110
|
// error messages
|
|
@@ -114,7 +114,25 @@ AUTH_DEVICE = "nami_auth_device", NAMI_CONFIGURATION = "nami_configuration", NAM
|
|
|
114
114
|
// API settings
|
|
115
115
|
API_VERSION = "v3", BASE_URL_PATH = `sdk/${API_VERSION}/platform`, BASE_URL = "https://app.namiml.com", BASE_STAGING_URL = "https://app-staging.namiml.com", CUSTOM_HOST_PREFIX = "namiAPIHost=", USE_STAGING_API = "useStagingAPI",
|
|
116
116
|
// // extended client info
|
|
117
|
-
EXTENDED_CLIENT_INFO_PREFIX = "extendedClientInfo", EXTENDED_CLIENT_INFO_DELIMITER = ":", VALIDATE_PRODUCT_GROUPS = "validateProductGroups", LOG_HTTP_REQUESTS = "logHTTPRequests", LOG_HTTP_TRAFFIC = "logHTTPTraffic", API_TRAFFIC_OBSERVER = "apiTrafficObserver", STARTUP_TELEMETRY = "startupTelemetry",
|
|
117
|
+
EXTENDED_CLIENT_INFO_PREFIX = "extendedClientInfo", EXTENDED_CLIENT_INFO_DELIMITER = ":", VALIDATE_PRODUCT_GROUPS = "validateProductGroups", LOG_HTTP_REQUESTS = "logHTTPRequests", LOG_HTTP_TRAFFIC = "logHTTPTraffic", API_TRAFFIC_OBSERVER = "apiTrafficObserver", STARTUP_TELEMETRY = "startupTelemetry",
|
|
118
|
+
// Test hook: every platform-config fetch attempt fails before the request leaves.
|
|
119
|
+
// Same namiCommand as Apple's and Android's, so the shared first-run flows drive it.
|
|
120
|
+
SIMULATE_CONFIG_FETCH_FAILURE = "simulateConfigFetchFailure",
|
|
121
|
+
// Test hook: every per-paywall body fetch fails before the request leaves, so the body cache's
|
|
122
|
+
// failure rows can be exercised without a proxy. Same namiCommand as Apple's and Android's (NAM-4357).
|
|
123
|
+
SIMULATE_PAYWALL_FETCH_FAILURE = "simulatePaywallFetchFailure",
|
|
124
|
+
// `campaignCacheTTL=<seconds>` overrides the config's campaign_cache_ttl — Apple's command name.
|
|
125
|
+
// It lets the paywall-cache e2e suite pin TTL 3600 / 1 / 0 per load (NAM-4357).
|
|
126
|
+
CAMPAIGN_CACHE_TTL_PREFIX = "campaignCacheTTL=", EXTENDED_PLATFORM = "extended-platform", EXTENDED_PLATFORM_VERSION = "extended-platform-version", API_MAX_CALLS_LIMIT = 2, API_RETRY_DELAY_SEC = 2, API_TIMEOUT_LIMIT = 20000, DEVICE_API_TIMEOUT_LIMIT = 2000,
|
|
127
|
+
// Platform-config attempts when no initial config was supplied (Android's
|
|
128
|
+
// CONFIG_MAX_ATTEMPTS; Apple's retry(maxAttempts: 3)).
|
|
129
|
+
CONFIG_MAX_ATTEMPTS = 3,
|
|
130
|
+
// Per-attempt timeout for GET config/. Android's config client allows 5 s to connect plus 5 s
|
|
131
|
+
// to read (Apple allows 30 s); three attempts keep a total outage near 30 s.
|
|
132
|
+
CONFIG_API_TIMEOUT_LIMIT = 10000,
|
|
133
|
+
// A foreground within this long of the last launch/resume run is skipped — Apple's
|
|
134
|
+
// NamiStartupCoordinator.shouldRunResume window.
|
|
135
|
+
RESUME_THROTTLE_MS = 30000,
|
|
118
136
|
// status codes
|
|
119
137
|
STATUS_SUCCESS = 200, STATUS_BAD_REQUEST = 400, STATUS_NOT_FOUND = 404, STATUS_CONFLICT = 409, STATUS_INTERNAL_SERVER_ERROR = 500,
|
|
120
138
|
// configuration states
|
|
@@ -3045,6 +3063,206 @@ function uniqBy(array, iteratee) {
|
|
|
3045
3063
|
return (array && array.length) ? baseUniq(array, baseIteratee(iteratee)) : [];
|
|
3046
3064
|
}
|
|
3047
3065
|
|
|
3066
|
+
/** Storage key of an entry's raw body; the exact URL follows the prefix. */
|
|
3067
|
+
const PAYWALL_BODY_KEY_PREFIX = "api_paywall_body:";
|
|
3068
|
+
/**
|
|
3069
|
+
* Storage key of an entry's metadata. Written AFTER the body, so it is the commit point: an entry
|
|
3070
|
+
* is visible only once both are durable, and a body with no metadata is an interrupted write.
|
|
3071
|
+
*/
|
|
3072
|
+
const PAYWALL_BODY_META_KEY_PREFIX = "api_paywall_body_meta:";
|
|
3073
|
+
const DEFAULT_MAX_BODY_ENTRIES = 500;
|
|
3074
|
+
class PaywallBodyCache {
|
|
3075
|
+
constructor(storage = () => getPlatformAdapters().storage, maxEntries = DEFAULT_MAX_BODY_ENTRIES) {
|
|
3076
|
+
this.storage = storage;
|
|
3077
|
+
this.maxEntries = maxEntries;
|
|
3078
|
+
/** Set under `campaign_cache_ttl == 0`: entries live here, in this process only. */
|
|
3079
|
+
this.memory = null;
|
|
3080
|
+
/** Metadata of every persisted entry, loaded once from the storage adapter. */
|
|
3081
|
+
this.index = null;
|
|
3082
|
+
}
|
|
3083
|
+
get count() {
|
|
3084
|
+
return this.memory ? this.memory.size : this.loadIndex().size;
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Applies `campaign_cache_ttl` at the start of a resolution. `null` keeps everything, `n > 0`
|
|
3088
|
+
* purges entries whose own age is `>= n` seconds, and `0` purges the persistent store and keeps
|
|
3089
|
+
* this process's entries in memory only.
|
|
3090
|
+
*/
|
|
3091
|
+
evaluateTtl(ttl, now = Date.now()) {
|
|
3092
|
+
if (ttl === 0) {
|
|
3093
|
+
if (this.memory)
|
|
3094
|
+
return;
|
|
3095
|
+
this.dropPersisted([...this.loadIndex().keys()]);
|
|
3096
|
+
this.memory = new Map();
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
// Leaving TTL 0 goes back to the persistent store; the process-local entries are dropped.
|
|
3100
|
+
this.memory = null;
|
|
3101
|
+
if (ttl === null)
|
|
3102
|
+
return;
|
|
3103
|
+
const expired = [...this.loadIndex().values()].filter((meta) => isExpired(meta, ttl, now));
|
|
3104
|
+
this.dropPersisted(expired.map((meta) => meta.url));
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* The entry stored under this exact URL, or null when there is none, it is past `ttl`, or its
|
|
3108
|
+
* body is missing (a half-written entry, which is deleted here).
|
|
3109
|
+
*/
|
|
3110
|
+
validEntry(url, ttl, now = Date.now()) {
|
|
3111
|
+
if (this.memory)
|
|
3112
|
+
return this.memory.get(url) ?? null;
|
|
3113
|
+
const meta = this.loadIndex().get(url);
|
|
3114
|
+
if (!meta)
|
|
3115
|
+
return null;
|
|
3116
|
+
if (ttl !== null && ttl > 0 && isExpired(meta, ttl, now)) {
|
|
3117
|
+
this.dropPersisted([url]);
|
|
3118
|
+
return null;
|
|
3119
|
+
}
|
|
3120
|
+
const body = this.read(PAYWALL_BODY_KEY_PREFIX + url);
|
|
3121
|
+
if (body === null) {
|
|
3122
|
+
this.dropPersisted([url]);
|
|
3123
|
+
return null;
|
|
3124
|
+
}
|
|
3125
|
+
return { meta, body };
|
|
3126
|
+
}
|
|
3127
|
+
/**
|
|
3128
|
+
* The stored body for this exact URL, read straight from storage rather than through the index,
|
|
3129
|
+
* with no TTL check. This is what rebuilds the aggregate `api_paywalls`, so it also sees an entry
|
|
3130
|
+
* a host wrote after the index was loaded (the web test apps inject local test paywalls that way).
|
|
3131
|
+
*/
|
|
3132
|
+
bodyFor(url) {
|
|
3133
|
+
if (this.memory)
|
|
3134
|
+
return this.memory.get(url)?.body ?? null;
|
|
3135
|
+
return this.read(PAYWALL_BODY_KEY_PREFIX + url);
|
|
3136
|
+
}
|
|
3137
|
+
/** Stores a body under its exact URL. Returns false when the write did not land (quota, I/O). */
|
|
3138
|
+
store(url, contentHash, body, now = Date.now()) {
|
|
3139
|
+
const meta = { url, savedAt: now, ...(contentHash ? { contentHash } : {}) };
|
|
3140
|
+
if (this.memory) {
|
|
3141
|
+
this.memory.set(url, { meta, body });
|
|
3142
|
+
return true;
|
|
3143
|
+
}
|
|
3144
|
+
const index = this.loadIndex();
|
|
3145
|
+
const metaJson = JSON.stringify(meta);
|
|
3146
|
+
// Body first, metadata last. Each write is verified by reading it back: the web adapter
|
|
3147
|
+
// swallows a quota error rather than throwing, so a silent no-op is the common failure.
|
|
3148
|
+
if (this.write(PAYWALL_BODY_KEY_PREFIX + url, body) &&
|
|
3149
|
+
this.write(PAYWALL_BODY_META_KEY_PREFIX + url, metaJson)) {
|
|
3150
|
+
index.set(url, meta);
|
|
3151
|
+
return true;
|
|
3152
|
+
}
|
|
3153
|
+
logger.warn(`Paywall body cache: could not store ${url}; it will be fetched next time`);
|
|
3154
|
+
this.dropPersisted([url]);
|
|
3155
|
+
return false;
|
|
3156
|
+
}
|
|
3157
|
+
/** Stores a bundled body unless an entry already exists — a body the SDK fetched always wins. */
|
|
3158
|
+
seedIfAbsent(url, contentHash, body, now = Date.now()) {
|
|
3159
|
+
const exists = this.memory ? this.memory.has(url) : this.loadIndex().has(url);
|
|
3160
|
+
return exists ? false : this.store(url, contentHash, body, now);
|
|
3161
|
+
}
|
|
3162
|
+
/** Deletes one entry — used for an undecodable body. Not eviction. */
|
|
3163
|
+
remove(url) {
|
|
3164
|
+
if (this.memory)
|
|
3165
|
+
this.memory.delete(url);
|
|
3166
|
+
else
|
|
3167
|
+
this.dropPersisted([url]);
|
|
3168
|
+
}
|
|
3169
|
+
/**
|
|
3170
|
+
* Eviction, run once per resolution after a complete `campaign_rules` fetch: delete every entry
|
|
3171
|
+
* whose URL this resolution did not name, then cap the store at `maxEntries`, oldest first.
|
|
3172
|
+
*/
|
|
3173
|
+
reconcile(currentUrls) {
|
|
3174
|
+
const metas = this.memory
|
|
3175
|
+
? [...this.memory.values()].map((entry) => entry.meta)
|
|
3176
|
+
: [...this.loadIndex().values()];
|
|
3177
|
+
const unnamed = metas.filter((meta) => !currentUrls.has(meta.url)).map((meta) => meta.url);
|
|
3178
|
+
const kept = metas.filter((meta) => currentUrls.has(meta.url)).sort((a, b) => a.savedAt - b.savedAt);
|
|
3179
|
+
const overCap = kept.slice(0, Math.max(0, kept.length - this.maxEntries)).map((meta) => meta.url);
|
|
3180
|
+
[...unnamed, ...overCap].forEach((url) => this.remove(url));
|
|
3181
|
+
}
|
|
3182
|
+
/** Empties the store — persisted and in-memory entries alike. Called by `Nami.reset()`. */
|
|
3183
|
+
purgeAll() {
|
|
3184
|
+
this.dropPersisted([...this.loadIndex().keys()]);
|
|
3185
|
+
this.memory?.clear();
|
|
3186
|
+
}
|
|
3187
|
+
/** Builds the index from the metadata keys, clearing away half-written and unreadable entries. */
|
|
3188
|
+
loadIndex() {
|
|
3189
|
+
if (this.index)
|
|
3190
|
+
return this.index;
|
|
3191
|
+
this.index = new Map();
|
|
3192
|
+
let keys;
|
|
3193
|
+
try {
|
|
3194
|
+
keys = this.storage().getAllKeys();
|
|
3195
|
+
}
|
|
3196
|
+
catch (error) {
|
|
3197
|
+
logger.warn(`Paywall body cache: storage unavailable, caching nothing (${error})`);
|
|
3198
|
+
return this.index;
|
|
3199
|
+
}
|
|
3200
|
+
const bodies = new Set(keys.filter((key) => key.startsWith(PAYWALL_BODY_KEY_PREFIX)).map((key) => key.slice(PAYWALL_BODY_KEY_PREFIX.length)));
|
|
3201
|
+
const stale = [];
|
|
3202
|
+
for (const key of keys) {
|
|
3203
|
+
if (!key.startsWith(PAYWALL_BODY_META_KEY_PREFIX))
|
|
3204
|
+
continue;
|
|
3205
|
+
const url = key.slice(PAYWALL_BODY_META_KEY_PREFIX.length);
|
|
3206
|
+
const meta = parseMeta(this.read(key));
|
|
3207
|
+
if (meta?.url === url && bodies.has(url))
|
|
3208
|
+
this.index.set(url, meta);
|
|
3209
|
+
else
|
|
3210
|
+
stale.push(url);
|
|
3211
|
+
bodies.delete(url);
|
|
3212
|
+
}
|
|
3213
|
+
// Bodies whose metadata never landed: interrupted writes.
|
|
3214
|
+
this.dropPersisted([...stale, ...bodies]);
|
|
3215
|
+
return this.index;
|
|
3216
|
+
}
|
|
3217
|
+
dropPersisted(urls) {
|
|
3218
|
+
for (const url of urls) {
|
|
3219
|
+
this.index?.delete(url);
|
|
3220
|
+
this.removeKey(PAYWALL_BODY_META_KEY_PREFIX + url);
|
|
3221
|
+
this.removeKey(PAYWALL_BODY_KEY_PREFIX + url);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
read(key) {
|
|
3225
|
+
try {
|
|
3226
|
+
return this.storage().getItem(key);
|
|
3227
|
+
}
|
|
3228
|
+
catch {
|
|
3229
|
+
return null;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
write(key, value) {
|
|
3233
|
+
try {
|
|
3234
|
+
this.storage().setItem(key, value);
|
|
3235
|
+
}
|
|
3236
|
+
catch {
|
|
3237
|
+
return false;
|
|
3238
|
+
}
|
|
3239
|
+
return this.read(key) === value;
|
|
3240
|
+
}
|
|
3241
|
+
removeKey(key) {
|
|
3242
|
+
try {
|
|
3243
|
+
this.storage().removeItem(key);
|
|
3244
|
+
}
|
|
3245
|
+
catch {
|
|
3246
|
+
// Nothing more to do: an entry that cannot be deleted is re-validated on the next read.
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
function isExpired(meta, ttlSeconds, now) {
|
|
3251
|
+
return (now - meta.savedAt) / 1000 >= ttlSeconds;
|
|
3252
|
+
}
|
|
3253
|
+
function parseMeta(raw) {
|
|
3254
|
+
if (raw === null)
|
|
3255
|
+
return null;
|
|
3256
|
+
try {
|
|
3257
|
+
const meta = JSON.parse(raw);
|
|
3258
|
+
return typeof meta?.url === "string" && typeof meta?.savedAt === "number" ? meta : null;
|
|
3259
|
+
}
|
|
3260
|
+
catch {
|
|
3261
|
+
return null;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
const paywallBodyCache = new PaywallBodyCache();
|
|
3265
|
+
|
|
3048
3266
|
class StorageService {
|
|
3049
3267
|
constructor() {
|
|
3050
3268
|
this.memoryStore = {};
|
|
@@ -3127,6 +3345,20 @@ class StorageService {
|
|
|
3127
3345
|
}
|
|
3128
3346
|
this.setItem(key, paywalls);
|
|
3129
3347
|
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Writes the aggregate `api_paywalls` as the list of URLs a per-URL resolution produced, rather
|
|
3350
|
+
* than a second copy of their bodies (NAM-4357): the bodies already live in the per-URL body
|
|
3351
|
+
* cache, and `getPaywalls(API_PAYWALLS)` rebuilds the paywall objects from it. The legacy bulk
|
|
3352
|
+
* path still writes whole objects through `setPaywalls`.
|
|
3353
|
+
*/
|
|
3354
|
+
setApiPaywallsFromBodyCache(urls) {
|
|
3355
|
+
const index = { bodyCacheUrls: urls };
|
|
3356
|
+
if (this.getCampaignCacheTtl() === 0) {
|
|
3357
|
+
this.memoryStore[API_PAYWALLS] = JSON.stringify(index);
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
this.setItem(API_PAYWALLS, index);
|
|
3361
|
+
}
|
|
3130
3362
|
getPaywalls(key) {
|
|
3131
3363
|
if (key === INITIAL_PAYWALLS) {
|
|
3132
3364
|
return this.memoryStore[INITIAL_PAYWALLS]
|
|
@@ -3135,8 +3367,10 @@ class StorageService {
|
|
|
3135
3367
|
}
|
|
3136
3368
|
if (key === API_PAYWALLS) {
|
|
3137
3369
|
const memoryValue = this.memoryStore[API_PAYWALLS];
|
|
3138
|
-
if (memoryValue != null)
|
|
3139
|
-
|
|
3370
|
+
if (memoryValue != null) {
|
|
3371
|
+
const parsed = JSON.parse(memoryValue);
|
|
3372
|
+
return isBodyCacheAggregate(parsed) ? paywallsFromBodyCache(parsed.bodyCacheUrls) : parsed;
|
|
3373
|
+
}
|
|
3140
3374
|
let stored = this.getItem(API_PAYWALLS);
|
|
3141
3375
|
if (stored === null)
|
|
3142
3376
|
return null;
|
|
@@ -3148,6 +3382,8 @@ class StorageService {
|
|
|
3148
3382
|
return stored;
|
|
3149
3383
|
}
|
|
3150
3384
|
}
|
|
3385
|
+
if (isBodyCacheAggregate(stored))
|
|
3386
|
+
return paywallsFromBodyCache(stored.bodyCacheUrls);
|
|
3151
3387
|
return this.isLegacyTimestampedCache(stored) ? stored.data : stored;
|
|
3152
3388
|
}
|
|
3153
3389
|
return this.getItem(key);
|
|
@@ -3316,7 +3552,18 @@ class StorageService {
|
|
|
3316
3552
|
this.resetItem(API_CAMPAIGN_RULES);
|
|
3317
3553
|
this.resetItem(API_CAMPAIGN_SESSION_TIMESTAMP);
|
|
3318
3554
|
}
|
|
3555
|
+
/**
|
|
3556
|
+
* The effective `campaign_cache_ttl`: a `campaignCacheTTL=<n>` namiCommand first, then the API
|
|
3557
|
+
* config, then the initial config. Lives here rather than in `utils/config` because this service
|
|
3558
|
+
* needs it to decide where the aggregate caches go, and `utils/config` already imports this one.
|
|
3559
|
+
*/
|
|
3319
3560
|
getCampaignCacheTtl() {
|
|
3561
|
+
const override = this.getNamiConfig()?.namiCommands
|
|
3562
|
+
?.filter((cmd) => cmd.startsWith(CAMPAIGN_CACHE_TTL_PREFIX))
|
|
3563
|
+
.map((cmd) => cmd.slice(CAMPAIGN_CACHE_TTL_PREFIX.length))
|
|
3564
|
+
.find((value) => /^\d+$/.test(value));
|
|
3565
|
+
if (override !== undefined)
|
|
3566
|
+
return parseInt(override, 10);
|
|
3320
3567
|
const config = this.getAppConfig(API_CONFIG)
|
|
3321
3568
|
|| this.getAppConfig(INITIAL_APP_CONFIG);
|
|
3322
3569
|
return config?.campaign_cache_ttl ?? null;
|
|
@@ -3408,6 +3655,28 @@ class StorageService {
|
|
|
3408
3655
|
this.memoryStore = {};
|
|
3409
3656
|
}
|
|
3410
3657
|
}
|
|
3658
|
+
function isBodyCacheAggregate(value) {
|
|
3659
|
+
return typeof value === "object" && value !== null && Array.isArray(value.bodyCacheUrls);
|
|
3660
|
+
}
|
|
3661
|
+
/**
|
|
3662
|
+
* The paywalls a per-URL resolution produced, decoded from the body cache. A URL whose entry has
|
|
3663
|
+
* since gone (evicted, purged, or a write that never landed) is simply absent.
|
|
3664
|
+
*/
|
|
3665
|
+
function paywallsFromBodyCache(urls) {
|
|
3666
|
+
const paywalls = [];
|
|
3667
|
+
for (const url of urls) {
|
|
3668
|
+
const body = paywallBodyCache.bodyFor(url);
|
|
3669
|
+
if (body === null)
|
|
3670
|
+
continue;
|
|
3671
|
+
try {
|
|
3672
|
+
paywalls.push(JSON.parse(body));
|
|
3673
|
+
}
|
|
3674
|
+
catch {
|
|
3675
|
+
paywallBodyCache.remove(url);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
return paywalls;
|
|
3679
|
+
}
|
|
3411
3680
|
const storageService = new StorageService();
|
|
3412
3681
|
|
|
3413
3682
|
class RetryLimitExceededError extends Error {
|
|
@@ -6785,11 +7054,8 @@ const hasCapability = (capability) => {
|
|
|
6785
7054
|
|| storageService.getAppConfig(INITIAL_APP_CONFIG);
|
|
6786
7055
|
return appConfig?.capabilities?.includes(capability) ?? false;
|
|
6787
7056
|
};
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|| storageService.getAppConfig(INITIAL_APP_CONFIG);
|
|
6791
|
-
return config?.campaign_cache_ttl ?? null;
|
|
6792
|
-
};
|
|
7057
|
+
/** The effective `campaign_cache_ttl`, `campaignCacheTTL=<n>` namiCommand first. */
|
|
7058
|
+
const getCampaignCacheTtl = () => storageService.getCampaignCacheTtl();
|
|
6793
7059
|
/**
|
|
6794
7060
|
* The effective global journey settings from the current config, coalescing a missing
|
|
6795
7061
|
* `journey_settings` object — or any missing field — to {@link DEFAULT_JOURNEY_SETTINGS}.
|
|
@@ -57496,22 +57762,23 @@ class CampaignRuleRepository {
|
|
|
57496
57762
|
static hasPaywallUrls(campaigns) {
|
|
57497
57763
|
return campaigns.some((c) => c.paywall_url || c.page_urls || c.flow?.page_urls);
|
|
57498
57764
|
}
|
|
57765
|
+
/**
|
|
57766
|
+
* The campaign rules for this form factor, unvalidated, plus whether they came from the network.
|
|
57767
|
+
* `fromNetwork` is false when the stored rules stood in for a failed fetch (or updates are
|
|
57768
|
+
* disabled): the per-URL body cache must then neither resolve nor evict (NAM-4357).
|
|
57769
|
+
*/
|
|
57499
57770
|
async fetchCampaignRulesRaw() {
|
|
57500
|
-
const authDevice = storageService.getDevice();
|
|
57501
57771
|
if (this.disableCampaignUpdates) {
|
|
57502
57772
|
logger.info("Campaign updates disabled by configuration. Skipping fetch.");
|
|
57503
|
-
return [];
|
|
57773
|
+
return { campaigns: [], fromNetwork: false };
|
|
57504
57774
|
}
|
|
57505
|
-
|
|
57506
|
-
|
|
57507
|
-
|
|
57508
|
-
|
|
57509
|
-
|
|
57510
|
-
campaignRules = await this.getCampaigns(authDevice.id);
|
|
57511
|
-
}
|
|
57512
|
-
campaignRules = campaignRules?.filter((cRule) => (cRule.paywall && cRule.form_factors.some((f) => f.form_factor === this.currentFormFactor)) ||
|
|
57775
|
+
const authDevice = storageService.getDevice();
|
|
57776
|
+
const fetched = authDevice?.id
|
|
57777
|
+
? await this.fetchCampaigns(authDevice.id)
|
|
57778
|
+
: await this.fetchAnonymousCampaigns();
|
|
57779
|
+
const campaigns = fetched.campaigns?.filter((cRule) => (cRule.paywall && cRule.form_factors.some((f) => f.form_factor === this.currentFormFactor)) ||
|
|
57513
57780
|
(cRule.flow && cRule.form_factors?.some((f) => f.form_factor === this.currentFormFactor)));
|
|
57514
|
-
return
|
|
57781
|
+
return { campaigns, fromNetwork: fetched.fromNetwork };
|
|
57515
57782
|
}
|
|
57516
57783
|
finalizeCampaignRules(campaignRules, paywalls) {
|
|
57517
57784
|
// NAM-2457: a server flow rule is kept only when BOTH assets are satisfied —
|
|
@@ -57547,10 +57814,19 @@ class CampaignRuleRepository {
|
|
|
57547
57814
|
listeners.forEach((handler) => handler(campaigns));
|
|
57548
57815
|
}
|
|
57549
57816
|
async getAnonymousCampaigns(sdkVersion = NAMI_SDK_VERSION) {
|
|
57817
|
+
return (await this.fetchAnonymousCampaigns(sdkVersion)).campaigns;
|
|
57818
|
+
}
|
|
57819
|
+
async getCampaigns(deviceId) {
|
|
57820
|
+
return (await this.fetchCampaigns(deviceId)).campaigns;
|
|
57821
|
+
}
|
|
57822
|
+
async fetchAnonymousCampaigns(sdkVersion = NAMI_SDK_VERSION) {
|
|
57550
57823
|
const path = `campaign_rules/?sdk_version=${sdkVersion}`;
|
|
57551
57824
|
try {
|
|
57552
57825
|
const resp = await NamiAPI.instance.fetchAPI(path);
|
|
57553
|
-
return
|
|
57826
|
+
return {
|
|
57827
|
+
campaigns: mapAnonymousCampaigns(resp.results ?? [], this.splitPosition, this.currentFormFactor),
|
|
57828
|
+
fromNetwork: true,
|
|
57829
|
+
};
|
|
57554
57830
|
}
|
|
57555
57831
|
catch (error) {
|
|
57556
57832
|
const data = this.fallbackData();
|
|
@@ -57558,23 +57834,23 @@ class CampaignRuleRepository {
|
|
|
57558
57834
|
logger.error(handleErrors(error.status, path));
|
|
57559
57835
|
throw error;
|
|
57560
57836
|
}
|
|
57561
|
-
return data.results;
|
|
57837
|
+
return { campaigns: data.results, fromNetwork: false };
|
|
57562
57838
|
}
|
|
57563
57839
|
}
|
|
57564
|
-
async
|
|
57840
|
+
async fetchCampaigns(deviceId) {
|
|
57565
57841
|
const path = `device/${deviceId}/campaign_rules/`;
|
|
57566
|
-
let data;
|
|
57567
57842
|
try {
|
|
57568
|
-
data = await NamiAPI.instance.fetchAPI(path);
|
|
57843
|
+
const data = await NamiAPI.instance.fetchAPI(path);
|
|
57844
|
+
return { campaigns: data?.results || [], fromNetwork: true };
|
|
57569
57845
|
}
|
|
57570
57846
|
catch (error) {
|
|
57571
|
-
data = this.fallbackData();
|
|
57847
|
+
const data = this.fallbackData();
|
|
57572
57848
|
if (!data) {
|
|
57573
57849
|
logger.error(handleErrors(error.status, path));
|
|
57574
57850
|
throw error;
|
|
57575
57851
|
}
|
|
57852
|
+
return { campaigns: data.results || [], fromNetwork: false };
|
|
57576
57853
|
}
|
|
57577
|
-
return data?.results || [];
|
|
57578
57854
|
}
|
|
57579
57855
|
fallbackData() {
|
|
57580
57856
|
const storedData = storageService.getCampaignRules(API_CAMPAIGN_RULES)
|
|
@@ -57857,6 +58133,15 @@ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
57857
58133
|
* RetryExhaustedError when all attempts and/or the budget are spent.
|
|
57858
58134
|
*/
|
|
57859
58135
|
async function fetchWithCdnRetry(url, opts = {}) {
|
|
58136
|
+
const { data, retryCount } = await fetchTextWithCdnRetry(url, opts);
|
|
58137
|
+
return { data: JSON.parse(data), retryCount };
|
|
58138
|
+
}
|
|
58139
|
+
/**
|
|
58140
|
+
* {@link fetchWithCdnRetry}'s retry loop, returning the raw 2xx body text instead of parsed JSON.
|
|
58141
|
+
* The per-URL paywall body cache stores those exact bytes, so a cached body is decoded by the
|
|
58142
|
+
* same `JSON.parse` a fetched one is (NAM-4357).
|
|
58143
|
+
*/
|
|
58144
|
+
async function fetchTextWithCdnRetry(url, opts = {}) {
|
|
57860
58145
|
const maxAttempts = opts.maxAttempts ?? DEFAULTS.maxAttempts;
|
|
57861
58146
|
const baseDelayMs = opts.baseDelayMs ?? DEFAULTS.baseDelayMs;
|
|
57862
58147
|
const maxDelayMs = opts.maxDelayMs ?? DEFAULTS.maxDelayMs;
|
|
@@ -57906,8 +58191,7 @@ async function fetchWithCdnRetry(url, opts = {}) {
|
|
|
57906
58191
|
const responseBody = await response.clone().text();
|
|
57907
58192
|
logger.debug(`[HTTP] Response ${response.status}: ${responseBody}`);
|
|
57908
58193
|
}
|
|
57909
|
-
|
|
57910
|
-
return { data, retryCount: attempt };
|
|
58194
|
+
return { data: await response.text(), retryCount: attempt };
|
|
57911
58195
|
}
|
|
57912
58196
|
catch (err) {
|
|
57913
58197
|
if (err instanceof NonRetryableHttpError)
|
|
@@ -57928,7 +58212,43 @@ async function fetchWithCdnRetry(url, opts = {}) {
|
|
|
57928
58212
|
throw new RetryExhaustedError(url, attempt, lastStatus);
|
|
57929
58213
|
}
|
|
57930
58214
|
|
|
58215
|
+
/**
|
|
58216
|
+
* Reads the parts of a `campaign_rules` paywall URL the per-URL body cache needs (NAM-4357).
|
|
58217
|
+
*
|
|
58218
|
+
* Both helpers scan the string rather than build a `URL`: the cache is keyed on the exact bytes
|
|
58219
|
+
* the server sent, so nothing here may normalise, re-encode or reject a URL — a malformed one is
|
|
58220
|
+
* simply un-hashed.
|
|
58221
|
+
*/
|
|
58222
|
+
/**
|
|
58223
|
+
* The `content_hash` query value, or undefined when it is missing or empty. When the item appears
|
|
58224
|
+
* more than once the first value wins. The value is never parsed or validated: it is a
|
|
58225
|
+
* cache-validity token, not a content selector.
|
|
58226
|
+
*/
|
|
58227
|
+
function contentHashOf(url) {
|
|
58228
|
+
const query = url.split("#")[0].split("?")[1];
|
|
58229
|
+
if (!query)
|
|
58230
|
+
return undefined;
|
|
58231
|
+
for (const item of query.split("&")) {
|
|
58232
|
+
const eq = item.indexOf("=");
|
|
58233
|
+
const name = eq === -1 ? item : item.slice(0, eq);
|
|
58234
|
+
if (name !== "content_hash")
|
|
58235
|
+
continue;
|
|
58236
|
+
const value = eq === -1 ? "" : item.slice(eq + 1);
|
|
58237
|
+
return value || undefined;
|
|
58238
|
+
}
|
|
58239
|
+
return undefined;
|
|
58240
|
+
}
|
|
58241
|
+
/** The path segment after `/paywall/` — the paywall id the URL names — or undefined. */
|
|
58242
|
+
function paywallIdOf(url) {
|
|
58243
|
+
const path = url.split(/[?#]/)[0];
|
|
58244
|
+
const match = /\/paywall\/([^/]+)/.exec(path);
|
|
58245
|
+
return match ? match[1] : undefined;
|
|
58246
|
+
}
|
|
58247
|
+
|
|
57931
58248
|
class PaywallRepository {
|
|
58249
|
+
constructor(bodyCache = paywallBodyCache) {
|
|
58250
|
+
this.bodyCache = bodyCache;
|
|
58251
|
+
}
|
|
57932
58252
|
async fetchPaywalls() {
|
|
57933
58253
|
const authDevice = storageService.getDevice();
|
|
57934
58254
|
let paywalls;
|
|
@@ -57977,34 +58297,131 @@ class PaywallRepository {
|
|
|
57977
58297
|
}
|
|
57978
58298
|
return data?.results || [];
|
|
57979
58299
|
}
|
|
57980
|
-
|
|
57981
|
-
|
|
57982
|
-
|
|
57983
|
-
|
|
58300
|
+
/**
|
|
58301
|
+
* Resolves the URL set named by `campaign_rules` into paywall objects — the decision table of
|
|
58302
|
+
* `api-resilience-and-caching.md` § 2.2, run identically at startup and on `refresh()`:
|
|
58303
|
+
*
|
|
58304
|
+
* | URL | entry for the exact URL within TTL | action | provenance |
|
|
58305
|
+
* |-----------|------------------------------------|-------------------------------|--------------------------|
|
|
58306
|
+
* | hashed | yes | serve it, no request | `cacheHit` |
|
|
58307
|
+
* | hashed | no | fetch; store the 2xx body | `fetched` |
|
|
58308
|
+
* | un-hashed | not consulted | fetch; store the 2xx body | `fetched` |
|
|
58309
|
+
* | either | fetch failed, entry within TTL | serve it, keep it | `cacheAfterFetchFailure` |
|
|
58310
|
+
* | either | fetch failed, no entry | no body | counts `failed` |
|
|
58311
|
+
*
|
|
58312
|
+
* Writes the aggregate `api_paywalls` — as the URLs of every body this resolution produced, since
|
|
58313
|
+
* the bodies themselves are already in the body cache — unless it produced none, so an outage
|
|
58314
|
+
* cannot overwrite a good aggregate with an empty one. Eviction is the caller's: it runs only
|
|
58315
|
+
* after a complete `campaign_rules` fetch.
|
|
58316
|
+
*/
|
|
58317
|
+
async resolvePaywallsByUrls(urls) {
|
|
57984
58318
|
const uniqueUrls = [...new Set(urls)];
|
|
57985
|
-
const
|
|
57986
|
-
|
|
57987
|
-
|
|
57988
|
-
|
|
57989
|
-
|
|
57990
|
-
|
|
57991
|
-
|
|
57992
|
-
|
|
57993
|
-
|
|
57994
|
-
|
|
57995
|
-
|
|
57996
|
-
|
|
57997
|
-
|
|
58319
|
+
const ttl = getCampaignCacheTtl();
|
|
58320
|
+
this.bodyCache.evaluateTtl(ttl);
|
|
58321
|
+
const resolutions = await Promise.all(uniqueUrls.map((url) => this.resolvePaywall(url, ttl)));
|
|
58322
|
+
const outcome = {
|
|
58323
|
+
paywalls: [],
|
|
58324
|
+
urls: uniqueUrls.length,
|
|
58325
|
+
hashed: uniqueUrls.filter((url) => contentHashOf(url) !== undefined).length,
|
|
58326
|
+
cacheHits: 0,
|
|
58327
|
+
fetched: 0,
|
|
58328
|
+
cacheAfterFetchFailure: 0,
|
|
58329
|
+
failed: 0,
|
|
58330
|
+
retryCount: 0,
|
|
58331
|
+
};
|
|
58332
|
+
const resolvedUrls = new Map();
|
|
58333
|
+
resolutions.forEach((resolution, i) => {
|
|
58334
|
+
outcome.retryCount += resolution.retryCount;
|
|
58335
|
+
if (!resolution.paywall) {
|
|
58336
|
+
outcome.failed += 1;
|
|
58337
|
+
return;
|
|
57998
58338
|
}
|
|
58339
|
+
resolvedUrls.set(resolution.paywall, uniqueUrls[i]);
|
|
58340
|
+
if (resolution.paywall.fetchSource === "cacheHit")
|
|
58341
|
+
outcome.cacheHits += 1;
|
|
58342
|
+
else if (resolution.paywall.fetchSource === "cacheAfterFetchFailure")
|
|
58343
|
+
outcome.cacheAfterFetchFailure += 1;
|
|
58344
|
+
else
|
|
58345
|
+
outcome.fetched += 1;
|
|
58346
|
+
});
|
|
58347
|
+
outcome.paywalls = this.validatePaywalls([...resolvedUrls.keys()]);
|
|
58348
|
+
if (outcome.paywalls.length > 0) {
|
|
58349
|
+
storageService.setApiPaywallsFromBodyCache(outcome.paywalls.map((paywall) => resolvedUrls.get(paywall)));
|
|
57999
58350
|
}
|
|
58000
|
-
|
|
58001
|
-
|
|
58002
|
-
|
|
58003
|
-
|
|
58351
|
+
return outcome;
|
|
58352
|
+
}
|
|
58353
|
+
/**
|
|
58354
|
+
* Seeds the body cache from the initial-config bundle (§ 2.2 "Seeding"): every **hashed** bundled
|
|
58355
|
+
* URL whose paywall id matches a bundled body is stored under that exact URL unless an entry
|
|
58356
|
+
* already exists. A hash in a bundled URL was minted against the bundled body, so the pair is
|
|
58357
|
+
* exact. Runs at configure, before the first resolution.
|
|
58358
|
+
*/
|
|
58359
|
+
seedFromInitialConfig(campaignRules, paywalls) {
|
|
58360
|
+
const bundled = new Map(paywalls.map((paywall) => [paywall.id, paywall]));
|
|
58361
|
+
this.bodyCache.evaluateTtl(getCampaignCacheTtl());
|
|
58362
|
+
let candidates = 0;
|
|
58363
|
+
let seeded = 0;
|
|
58364
|
+
for (const url of new Set(bundledPaywallUrls(campaignRules))) {
|
|
58365
|
+
const contentHash = contentHashOf(url);
|
|
58366
|
+
const id = paywallIdOf(url);
|
|
58367
|
+
const paywall = id ? bundled.get(id) : undefined;
|
|
58368
|
+
if (!contentHash || !paywall)
|
|
58369
|
+
continue;
|
|
58370
|
+
candidates += 1;
|
|
58371
|
+
if (this.bodyCache.seedIfAbsent(url, contentHash, JSON.stringify(paywall)))
|
|
58372
|
+
seeded += 1;
|
|
58373
|
+
}
|
|
58374
|
+
logger.info(`Paywall body cache seeded from initial config: seeded=${seeded}, candidates=${candidates}`);
|
|
58375
|
+
}
|
|
58376
|
+
/** One row of the decision table. Never throws. */
|
|
58377
|
+
async resolvePaywall(url, ttl) {
|
|
58378
|
+
const contentHash = contentHashOf(url);
|
|
58379
|
+
if (contentHash) {
|
|
58380
|
+
const hit = this.decodeCached(url, ttl);
|
|
58381
|
+
if (hit)
|
|
58382
|
+
return { paywall: withProvenance(hit, "cacheHit", contentHash), retryCount: 0 };
|
|
58383
|
+
}
|
|
58384
|
+
let retryCount = 0;
|
|
58385
|
+
try {
|
|
58386
|
+
const response = await this.fetchPaywallText(url);
|
|
58387
|
+
retryCount = response.retryCount;
|
|
58388
|
+
const paywall = decodePaywall(response.data);
|
|
58389
|
+
if (paywall) {
|
|
58390
|
+
this.bodyCache.store(url, contentHash, response.data);
|
|
58391
|
+
return { paywall: withProvenance(paywall, "fetched", contentHash), retryCount };
|
|
58004
58392
|
}
|
|
58005
|
-
|
|
58393
|
+
logger.error(`Paywall body from ${url} is not a decodable paywall`);
|
|
58394
|
+
}
|
|
58395
|
+
catch (error) {
|
|
58396
|
+
// Note: an exhausted-failure path contributed retries we can't see from the thrown error,
|
|
58397
|
+
// so retry_count under-reports failures. Acceptable for the telemetry signal.
|
|
58398
|
+
logger.error(`Failed to fetch individual paywall: ${error}`);
|
|
58399
|
+
}
|
|
58400
|
+
const fallback = this.decodeCached(url, ttl);
|
|
58401
|
+
if (fallback) {
|
|
58402
|
+
logger.warn(`Serving the cached body for ${url} after fetch failure (within campaign_cache_ttl)`);
|
|
58403
|
+
return { paywall: withProvenance(fallback, "cacheAfterFetchFailure", contentHash), retryCount };
|
|
58006
58404
|
}
|
|
58007
|
-
return {
|
|
58405
|
+
return { retryCount };
|
|
58406
|
+
}
|
|
58407
|
+
/** The stored body for this exact URL, decoded; an undecodable one is deleted (not eviction). */
|
|
58408
|
+
decodeCached(url, ttl) {
|
|
58409
|
+
const entry = this.bodyCache.validEntry(url, ttl);
|
|
58410
|
+
if (!entry)
|
|
58411
|
+
return undefined;
|
|
58412
|
+
const paywall = decodePaywall(entry.body);
|
|
58413
|
+
if (!paywall) {
|
|
58414
|
+
logger.warn(`Deleting an undecodable cached paywall body for ${url}`);
|
|
58415
|
+
this.bodyCache.remove(url);
|
|
58416
|
+
}
|
|
58417
|
+
return paywall;
|
|
58418
|
+
}
|
|
58419
|
+
fetchPaywallText(url) {
|
|
58420
|
+
if (storageService.getNamiConfig()?.namiCommands?.includes(SIMULATE_PAYWALL_FETCH_FAILURE)) {
|
|
58421
|
+
logger.warn(`${SIMULATE_PAYWALL_FETCH_FAILURE}: failing the fetch for ${url} before the network`);
|
|
58422
|
+
return Promise.reject(new RetryExhaustedError(url, 0));
|
|
58423
|
+
}
|
|
58424
|
+
return fetchTextWithCdnRetry(url);
|
|
58008
58425
|
}
|
|
58009
58426
|
validatePaywalls(paywalls) {
|
|
58010
58427
|
return paywalls.filter((paywall) => {
|
|
@@ -58029,29 +58446,154 @@ class PaywallRepository {
|
|
|
58029
58446
|
}
|
|
58030
58447
|
}
|
|
58031
58448
|
PaywallRepository.instance = new PaywallRepository();
|
|
58449
|
+
function decodePaywall(text) {
|
|
58450
|
+
try {
|
|
58451
|
+
const value = JSON.parse(text);
|
|
58452
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
58453
|
+
}
|
|
58454
|
+
catch {
|
|
58455
|
+
return undefined;
|
|
58456
|
+
}
|
|
58457
|
+
}
|
|
58458
|
+
function withProvenance(paywall, fetchSource, contentHash) {
|
|
58459
|
+
return { ...paywall, fetchSource, ...(contentHash ? { contentHash } : {}) };
|
|
58460
|
+
}
|
|
58461
|
+
/** Every paywall URL a bundled (anonymous-format) rule names, across all of its segments. */
|
|
58462
|
+
function bundledPaywallUrls(campaignRules) {
|
|
58463
|
+
const urls = [];
|
|
58464
|
+
for (const rule of campaignRules) {
|
|
58465
|
+
for (const segment of rule.segments ?? []) {
|
|
58466
|
+
if (segment.paywall_url)
|
|
58467
|
+
urls.push(segment.paywall_url);
|
|
58468
|
+
if (segment.page_urls)
|
|
58469
|
+
urls.push(...Object.values(segment.page_urls));
|
|
58470
|
+
if (segment.flow?.page_urls)
|
|
58471
|
+
urls.push(...Object.values(segment.flow.page_urls));
|
|
58472
|
+
}
|
|
58473
|
+
}
|
|
58474
|
+
return urls;
|
|
58475
|
+
}
|
|
58032
58476
|
|
|
58033
58477
|
class ConfigRepository {
|
|
58478
|
+
/**
|
|
58479
|
+
* One `GET config/` attempt. Persists the config on success and throws on any failure —
|
|
58480
|
+
* a non-2xx or a transport error alike. `withRetry`'s own retries are turned off, so the
|
|
58481
|
+
* attempt count belongs to {@link fetchConfigForStartup} alone.
|
|
58482
|
+
*/
|
|
58034
58483
|
async fetchConfig() {
|
|
58035
|
-
|
|
58036
|
-
|
|
58037
|
-
|
|
58038
|
-
;
|
|
58039
|
-
}
|
|
58040
|
-
catch (error) {
|
|
58041
|
-
data = storageService.getAppConfig(API_CONFIG)
|
|
58042
|
-
|| storageService.getAppConfig(INITIAL_APP_CONFIG);
|
|
58043
|
-
if (!data) {
|
|
58044
|
-
logger.error(handleErrors(error.status, "config/"));
|
|
58045
|
-
throw error;
|
|
58046
|
-
}
|
|
58484
|
+
// Thrown from *inside* the attempt on purpose, like Apple's and Android's: the ladder then
|
|
58485
|
+
// treats it exactly as a real 500, which is the whole point of the mock.
|
|
58486
|
+
if (storageService.getNamiConfig()?.namiCommands?.includes(SIMULATE_CONFIG_FETCH_FAILURE)) {
|
|
58487
|
+
throw new RetryLimitExceededError(STATUS_INTERNAL_SERVER_ERROR, SIMULATE_CONFIG_FETCH_FAILURE);
|
|
58047
58488
|
}
|
|
58048
|
-
|
|
58489
|
+
const platformId = storageService.getNamiConfig()?.appPlatformID;
|
|
58490
|
+
const data = await NamiAPI.instance.fetchAPI("config/", CONFIG_API_TIMEOUT_LIMIT, 0);
|
|
58491
|
+
// A late answer (the startup await gave up on it) must not overwrite another platform's
|
|
58492
|
+
// config after a reset or a platform-switching reconfigure.
|
|
58493
|
+
if (storageService.getNamiConfig()?.appPlatformID === platformId) {
|
|
58049
58494
|
storageService.setAppConfig(API_CONFIG, data);
|
|
58050
58495
|
}
|
|
58496
|
+
else {
|
|
58497
|
+
logger.debug("Discarding an app config response for a platform that is no longer configured");
|
|
58498
|
+
}
|
|
58051
58499
|
return data;
|
|
58052
58500
|
}
|
|
58501
|
+
/**
|
|
58502
|
+
* The startup platform-config step, converged on Apple's `NamiPlatformConfigService.fetch` and
|
|
58503
|
+
* Android's `fetchAppConfigOnce` (NAM-4470, NAM-4589):
|
|
58504
|
+
*
|
|
58505
|
+
* | case | outcome |
|
|
58506
|
+
* |---|---|
|
|
58507
|
+
* | an initial config was supplied | NOT awaited: one attempt runs in the background and stores the config when it lands; usable |
|
|
58508
|
+
* | no initial config, success, capabilities include `sdk` | usable |
|
|
58509
|
+
* | no initial config, success, capabilities lack `sdk` | not usable |
|
|
58510
|
+
* | no initial config, failure | {@link CONFIG_MAX_ATTEMPTS} attempts of up to {@link CONFIG_API_TIMEOUT_LIMIT} each, then usable only if a config is cached (and grants `sdk`) |
|
|
58511
|
+
*
|
|
58512
|
+
* The two fallbacks are consulted at two different points, deliberately. An initial config
|
|
58513
|
+
* decides the case *before* any request: startup does not wait on the network at all, since the
|
|
58514
|
+
* initial config is what makes the SDK ready without it (Apple: "Initial config is available.
|
|
58515
|
+
* Skipping retry."). The config cached by a previous launch is consulted only *after* the
|
|
58516
|
+
* retries run out. A stored `INITIAL_APP_CONFIG` is not that cache.
|
|
58517
|
+
*
|
|
58518
|
+
* One check does apply with an initial config, because it costs no wait: a cached config that
|
|
58519
|
+
* withholds `sdk` still stops startup, as Apple's cache load re-applies `updateSdkFlags`.
|
|
58520
|
+
* Without it a kill switch could never reach an app that ships an initial config — the
|
|
58521
|
+
* background fetch lands after startup has already run, so it takes effect from the next launch.
|
|
58522
|
+
*
|
|
58523
|
+
* A `404` is treated as any other 4xx, matching Apple's effective behaviour (its dedicated 404
|
|
58524
|
+
* branch is unreachable, NAM-4471) and Android's.
|
|
58525
|
+
*
|
|
58526
|
+
* @param hasInitialConfig whether the host passed an `initialConfig` that decoded to content.
|
|
58527
|
+
*/
|
|
58528
|
+
async fetchConfigForStartup(hasInitialConfig) {
|
|
58529
|
+
if (hasInitialConfig)
|
|
58530
|
+
return this.startupOnInitialConfig();
|
|
58531
|
+
const maxAttempts = CONFIG_MAX_ATTEMPTS;
|
|
58532
|
+
let lastError;
|
|
58533
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
58534
|
+
if (attempt > 1) {
|
|
58535
|
+
logger.debug(`Retrying app config fetch (attempt ${attempt} of ${maxAttempts})`);
|
|
58536
|
+
}
|
|
58537
|
+
try {
|
|
58538
|
+
return outcomeOf(await this.fetchConfig());
|
|
58539
|
+
}
|
|
58540
|
+
catch (error) {
|
|
58541
|
+
lastError = error;
|
|
58542
|
+
}
|
|
58543
|
+
}
|
|
58544
|
+
const cached = storageService.getAppConfig(API_CONFIG);
|
|
58545
|
+
if (cached) {
|
|
58546
|
+
// A cached kill switch still holds: Apple re-applies `updateSdkFlags` when it loads the
|
|
58547
|
+
// cached config, so one that withheld `sdk` keeps the SDK disabled through an outage.
|
|
58548
|
+
if (!grantsSdk(cached)) {
|
|
58549
|
+
return { usable: false, reason: "the cached app config does not grant the 'sdk' capability" };
|
|
58550
|
+
}
|
|
58551
|
+
logger.info(`App config fetch failed after ${maxAttempts} attempts - continuing on the cached config`);
|
|
58552
|
+
return { usable: true };
|
|
58553
|
+
}
|
|
58554
|
+
return {
|
|
58555
|
+
usable: false,
|
|
58556
|
+
reason: `app config fetch failed after ${maxAttempts} attempts (${describeFailure(lastError)}) ` +
|
|
58557
|
+
"and no initial or cached config is available",
|
|
58558
|
+
};
|
|
58559
|
+
}
|
|
58560
|
+
/**
|
|
58561
|
+
* The initial-config case: nothing is awaited. The cached kill switch is a storage read; the
|
|
58562
|
+
* config request is left running and persists its answer (`fetchConfig`) when it lands.
|
|
58563
|
+
*/
|
|
58564
|
+
startupOnInitialConfig() {
|
|
58565
|
+
// Read before the request can land, so this launch is judged on what the last one stored.
|
|
58566
|
+
const cached = storageService.getAppConfig(API_CONFIG);
|
|
58567
|
+
// Issued even when the cache holds a kill switch: that is how a LIFTED kill switch reaches
|
|
58568
|
+
// the next launch.
|
|
58569
|
+
this.fetchConfig().then((config) => {
|
|
58570
|
+
if (!grantsSdk(config)) {
|
|
58571
|
+
logger.warn("App config does not grant the 'sdk' capability - it takes effect from the next launch");
|
|
58572
|
+
}
|
|
58573
|
+
}, (error) => {
|
|
58574
|
+
logger.info(`App config fetch failed (${describeFailure(error)}). Initial config is available. Skipping retry.`);
|
|
58575
|
+
});
|
|
58576
|
+
if (cached && !grantsSdk(cached)) {
|
|
58577
|
+
return { usable: false, reason: "the cached app config does not grant the 'sdk' capability" };
|
|
58578
|
+
}
|
|
58579
|
+
return { usable: true };
|
|
58580
|
+
}
|
|
58053
58581
|
}
|
|
58054
58582
|
ConfigRepository.instance = new ConfigRepository();
|
|
58583
|
+
function outcomeOf(config) {
|
|
58584
|
+
return grantsSdk(config)
|
|
58585
|
+
? { usable: true }
|
|
58586
|
+
: { usable: false, reason: "app config does not grant the 'sdk' capability" };
|
|
58587
|
+
}
|
|
58588
|
+
function grantsSdk(config) {
|
|
58589
|
+
return config.capabilities?.includes(Capabilities.SDK) ?? false;
|
|
58590
|
+
}
|
|
58591
|
+
function describeFailure(error) {
|
|
58592
|
+
const status = error?.status;
|
|
58593
|
+
if (typeof status === "number")
|
|
58594
|
+
return `HTTP ${status}`;
|
|
58595
|
+
return error instanceof Error ? error.message : String(error);
|
|
58596
|
+
}
|
|
58055
58597
|
|
|
58056
58598
|
// Global reference to avoid circular imports
|
|
58057
58599
|
let namiRefsInstance = null;
|
|
@@ -61595,6 +62137,15 @@ class NamiRefs {
|
|
|
61595
62137
|
this.inMemoryAnonymousMode = false;
|
|
61596
62138
|
// Captured before device registration; true only on genuine fresh installs
|
|
61597
62139
|
this.isFirstSession = false;
|
|
62140
|
+
// Set when the platform-config step leaves no usable config (see isStartupDisabled)
|
|
62141
|
+
this.startupDisabled = false;
|
|
62142
|
+
// Whether this configure() supplied an initial config (the config ladder's early exit)
|
|
62143
|
+
this.hasInitialConfig = false;
|
|
62144
|
+
// Resume bookkeeping: the configuration startup last ran with, when it (or a resume) last
|
|
62145
|
+
// ran, and whether one is running now.
|
|
62146
|
+
this.lastConfig = null;
|
|
62147
|
+
this.lastStartupRunAt = 0;
|
|
62148
|
+
this.startupInFlight = false;
|
|
61598
62149
|
// Register this instance to avoid circular dependency issues
|
|
61599
62150
|
setNamiRefsInstance(this);
|
|
61600
62151
|
}
|
|
@@ -61612,13 +62163,101 @@ class NamiRefs {
|
|
|
61612
62163
|
getIsFirstSession() {
|
|
61613
62164
|
return this.isFirstSession;
|
|
61614
62165
|
}
|
|
62166
|
+
/**
|
|
62167
|
+
* True when the platform-config step found no usable config and startup stopped after it —
|
|
62168
|
+
* the equivalent of Apple's `sdkEnabled = false` and Android's `NamiRefs.startupDisabled`.
|
|
62169
|
+
* Like both, it gates only the startup sequence, nothing else.
|
|
62170
|
+
*/
|
|
62171
|
+
isStartupDisabled() {
|
|
62172
|
+
return this.startupDisabled;
|
|
62173
|
+
}
|
|
61615
62174
|
async init(config) {
|
|
61616
62175
|
this.isFirstSession = storageService.getDevice() === null && !storageService.getAnonymousMode();
|
|
62176
|
+
// Cleared per configure so a transient failure cannot disable a later run.
|
|
62177
|
+
this.startupDisabled = false;
|
|
62178
|
+
// `Nami.configure` has already normalised a missing or undecodable initialConfig to `{}`,
|
|
62179
|
+
// so "has content" is Apple's `initialConfig != nil`.
|
|
62180
|
+
this.hasInitialConfig = Object.keys(config.initialConfig ?? {}).length > 0;
|
|
61617
62181
|
this.setInitialValues(config);
|
|
61618
|
-
|
|
62182
|
+
this.lastConfig = config;
|
|
62183
|
+
this.lastStartupRunAt = Date.now();
|
|
62184
|
+
this.startupInFlight = true;
|
|
62185
|
+
try {
|
|
62186
|
+
if (!(await this.fetchPlatformConfig()))
|
|
62187
|
+
return;
|
|
62188
|
+
await this.runStartupAfterConfig(config);
|
|
62189
|
+
}
|
|
62190
|
+
finally {
|
|
62191
|
+
this.startupInFlight = false;
|
|
62192
|
+
}
|
|
62193
|
+
}
|
|
62194
|
+
/**
|
|
62195
|
+
* The app returned to the foreground. Re-reads the platform config, as Apple's `.appResume`
|
|
62196
|
+
* and Android's `onAppComesFromBackground` do, so a config change — the `sdk` kill switch
|
|
62197
|
+
* included — reaches a long-lived tab or app without a cold start.
|
|
62198
|
+
*
|
|
62199
|
+
* - Skipped within {@link RESUME_THROTTLE_MS} of the last launch/resume, and while one is
|
|
62200
|
+
* still running.
|
|
62201
|
+
* - When the launch had **stopped** for want of a config and one is now usable, the rest of
|
|
62202
|
+
* startup runs — the steps an Apple resume re-runs because the launch never executed them.
|
|
62203
|
+
* - Otherwise this is a config refresh only. Apple and Android also re-run device, campaigns,
|
|
62204
|
+
* profile, products and the session start on resume; core does not yet, and has no
|
|
62205
|
+
* session end on background for a second session start to pair with (NAM-4623).
|
|
62206
|
+
*/
|
|
62207
|
+
async resume() {
|
|
62208
|
+
if (!this.lastConfig || this.startupInFlight)
|
|
62209
|
+
return;
|
|
62210
|
+
if (Date.now() - this.lastStartupRunAt < RESUME_THROTTLE_MS) {
|
|
62211
|
+
logger.debug("Resuming too quickly to call the service.");
|
|
62212
|
+
return;
|
|
62213
|
+
}
|
|
62214
|
+
const config = this.lastConfig;
|
|
62215
|
+
const wasStopped = this.startupDisabled;
|
|
62216
|
+
this.lastStartupRunAt = Date.now();
|
|
62217
|
+
this.startupInFlight = true;
|
|
62218
|
+
this.startupDisabled = false;
|
|
62219
|
+
try {
|
|
62220
|
+
if (!(await this.fetchPlatformConfig()) || !wasStopped)
|
|
62221
|
+
return;
|
|
62222
|
+
logger.info("Usable app config on resume - completing the startup that stopped at launch");
|
|
62223
|
+
await this.runStartupAfterConfig(config);
|
|
62224
|
+
}
|
|
62225
|
+
catch (error) {
|
|
62226
|
+
logger.warn(`Resume refresh failed: ${error}`);
|
|
62227
|
+
}
|
|
62228
|
+
finally {
|
|
62229
|
+
this.startupInFlight = false;
|
|
62230
|
+
}
|
|
62231
|
+
}
|
|
62232
|
+
/** Everything startup runs once the platform-config step has found a usable config. */
|
|
62233
|
+
async runStartupAfterConfig(config) {
|
|
61619
62234
|
await this.initAndFetchRequiredData(config);
|
|
61620
62235
|
SessionService.instance.startSession();
|
|
61621
62236
|
}
|
|
62237
|
+
/** Forgets the configuration a resume would run against; called by `Nami.reset()`. */
|
|
62238
|
+
clearResumeState() {
|
|
62239
|
+
this.lastConfig = null;
|
|
62240
|
+
this.lastStartupRunAt = 0;
|
|
62241
|
+
this.startupDisabled = false;
|
|
62242
|
+
}
|
|
62243
|
+
/**
|
|
62244
|
+
* The platform-config startup step, ahead of device registration — the position it holds on
|
|
62245
|
+
* Apple (`.fetchPlatformConfig`) and Android (`fetchAppConfigOnce`) (NAM-4464, NAM-4589).
|
|
62246
|
+
* Without an initial config it is awaited, so everything downstream — `campaign_cache_ttl`
|
|
62247
|
+
* included — is evaluated against the config it leaves in storage. With one it resolves at
|
|
62248
|
+
* once and the request finishes in the background (see `fetchConfigForStartup`).
|
|
62249
|
+
*
|
|
62250
|
+
* @returns false when there is no usable config, in which case startup must stop.
|
|
62251
|
+
*/
|
|
62252
|
+
async fetchPlatformConfig() {
|
|
62253
|
+
const outcome = await ConfigRepository.instance.fetchConfigForStartup(this.hasInitialConfig);
|
|
62254
|
+
if (!outcome.usable) {
|
|
62255
|
+
this.startupDisabled = true;
|
|
62256
|
+
logger.error(`Nami startup stopped: ${outcome.reason}`);
|
|
62257
|
+
return false;
|
|
62258
|
+
}
|
|
62259
|
+
return true;
|
|
62260
|
+
}
|
|
61622
62261
|
setInitialValues(config) {
|
|
61623
62262
|
const baseConfig = { ...config };
|
|
61624
62263
|
delete baseConfig.initialConfig;
|
|
@@ -61634,7 +62273,10 @@ class NamiRefs {
|
|
|
61634
62273
|
}
|
|
61635
62274
|
}
|
|
61636
62275
|
if (initconfig?.paywalls) {
|
|
61637
|
-
storageService.setPaywalls(INITIAL_PAYWALLS, initconfig.paywalls);
|
|
62276
|
+
storageService.setPaywalls(INITIAL_PAYWALLS, initconfig.paywalls.map((paywall) => ({ ...paywall, fetchSource: "initialConfig" })));
|
|
62277
|
+
if (initconfig.campaign_rules?.length) {
|
|
62278
|
+
PaywallRepository.instance.seedFromInitialConfig(initconfig.campaign_rules, initconfig.paywalls);
|
|
62279
|
+
}
|
|
61638
62280
|
}
|
|
61639
62281
|
if (initconfig?.products) {
|
|
61640
62282
|
storageService.setProducts(INITIAL_PRODUCTS, initconfig.products);
|
|
@@ -61701,7 +62343,7 @@ class NamiRefs {
|
|
|
61701
62343
|
const paywallRepo = PaywallRepository.instance;
|
|
61702
62344
|
const startTime = Date.now();
|
|
61703
62345
|
const campaignRulesStartTime = Date.now();
|
|
61704
|
-
const rawCampaigns = await campaignRepo.fetchCampaignRulesRaw();
|
|
62346
|
+
const { campaigns: rawCampaigns, fromNetwork } = await campaignRepo.fetchCampaignRulesRaw();
|
|
61705
62347
|
const campaignRulesDuration = Date.now() - campaignRulesStartTime;
|
|
61706
62348
|
// Hydrate flow objects: fetch flow JSON from flow.url for campaigns that
|
|
61707
62349
|
// use the URL-based format (4.0.0+ since NAM-3493) instead of inline flow.object
|
|
@@ -61722,24 +62364,35 @@ class NamiRefs {
|
|
|
61722
62364
|
const useIndividual = !campaignRepo.useLegacyPaywallFetch &&
|
|
61723
62365
|
CampaignRuleRepository.hasPaywallUrls(rawCampaigns);
|
|
61724
62366
|
let paywalls;
|
|
61725
|
-
let
|
|
62367
|
+
let outcome = null;
|
|
61726
62368
|
const paywallsStartTime = Date.now();
|
|
61727
|
-
if (useIndividual) {
|
|
62369
|
+
if (useIndividual && !fromNetwork) {
|
|
62370
|
+
// campaign_rules came from the stored copy, so this is not a complete page set: no
|
|
62371
|
+
// resolution and no eviction (api-resilience-and-caching.md § 2.2). The aggregate serves.
|
|
62372
|
+
logger.info("Paywall resolution skipped: campaign_rules were served from the stored copy");
|
|
62373
|
+
paywalls = getApiPaywalls();
|
|
62374
|
+
}
|
|
62375
|
+
else if (useIndividual) {
|
|
61728
62376
|
const urls = CampaignRuleRepository.extractPaywallUrls(rawCampaigns);
|
|
61729
|
-
|
|
61730
|
-
paywalls =
|
|
61731
|
-
|
|
62377
|
+
outcome = await paywallRepo.resolvePaywallsByUrls(urls);
|
|
62378
|
+
paywalls = outcome.paywalls;
|
|
62379
|
+
paywallBodyCache.reconcile(new Set(urls));
|
|
61732
62380
|
}
|
|
61733
62381
|
else {
|
|
61734
62382
|
paywalls = await paywallRepo.fetchPaywalls();
|
|
61735
62383
|
}
|
|
61736
62384
|
const paywallsDuration = Date.now() - paywallsStartTime;
|
|
61737
62385
|
const totalDuration = Date.now() - startTime;
|
|
61738
|
-
if (
|
|
61739
|
-
|
|
61740
|
-
|
|
62386
|
+
if (!useIndividual || outcome) {
|
|
62387
|
+
// The line every SDK emits once per resolution; the paywall-cache e2e suites parse it.
|
|
62388
|
+
const cacheFields = outcome
|
|
62389
|
+
? `, urls=${outcome.urls}, hashed=${outcome.hashed}, cache_hits=${outcome.cacheHits}, ` +
|
|
62390
|
+
`fetched=${outcome.fetched}, cache_after_fetch_failure=${outcome.cacheAfterFetchFailure}, failed=${outcome.failed}`
|
|
62391
|
+
: "";
|
|
62392
|
+
logger.debug(`Paywall fetch telemetry: strategy=${useIndividual ? "individual" : "bulk"}, ` +
|
|
62393
|
+
`count=${paywalls.length}${cacheFields}, total=${totalDuration}ms, ` +
|
|
61741
62394
|
`campaigns=${campaignRulesDuration}ms, paywalls=${paywallsDuration}ms` +
|
|
61742
|
-
`${
|
|
62395
|
+
`${outcome ? `, retry_count=${outcome.retryCount}` : ""}`);
|
|
61743
62396
|
}
|
|
61744
62397
|
return campaignRepo.finalizeCampaignRules(rawCampaigns, paywalls);
|
|
61745
62398
|
}
|
|
@@ -61879,7 +62532,7 @@ class LastLaunchStore {
|
|
|
61879
62532
|
}
|
|
61880
62533
|
}
|
|
61881
62534
|
|
|
61882
|
-
var _Nami_isInitialized;
|
|
62535
|
+
var _Nami_isInitialized, _Nami_removeForegroundListener;
|
|
61883
62536
|
// NamiFlowManager is intentionally NOT imported at top level — it
|
|
61884
62537
|
// transitively imports back to this module (`Nami` for logging), and
|
|
61885
62538
|
// pulling it in here causes a load-order cycle that breaks tests
|
|
@@ -61887,6 +62540,7 @@ var _Nami_isInitialized;
|
|
|
61887
62540
|
class Nami {
|
|
61888
62541
|
constructor() {
|
|
61889
62542
|
_Nami_isInitialized.set(this, false);
|
|
62543
|
+
_Nami_removeForegroundListener.set(this, null);
|
|
61890
62544
|
this.maxLogging = false;
|
|
61891
62545
|
}
|
|
61892
62546
|
get isInitialized() {
|
|
@@ -61910,9 +62564,15 @@ class Nami {
|
|
|
61910
62564
|
* integrating app, consistent with the Apple and Android SDKs.
|
|
61911
62565
|
*/
|
|
61912
62566
|
static async reset() {
|
|
62567
|
+
var _a;
|
|
61913
62568
|
storageService.clearAll();
|
|
62569
|
+
// The per-URL paywall body cache keeps its own prefixed keys and an in-memory index (NAM-4357).
|
|
62570
|
+
paywallBodyCache.purgeAll();
|
|
61914
62571
|
clearInMemoryAnonymousMode();
|
|
61915
62572
|
__classPrivateFieldSet(Nami.instance, _Nami_isInitialized, false, "f");
|
|
62573
|
+
__classPrivateFieldGet((_a = Nami.instance), _Nami_removeForegroundListener, "f")?.call(_a);
|
|
62574
|
+
__classPrivateFieldSet(Nami.instance, _Nami_removeForegroundListener, null, "f");
|
|
62575
|
+
NamiRefs.instance.clearResumeState();
|
|
61916
62576
|
// In-memory singleton state that previously survived reset and
|
|
61917
62577
|
// bled into the next configure() cycle.
|
|
61918
62578
|
NamiProfileManager$1.instance.setExternalId(undefined);
|
|
@@ -61975,6 +62635,18 @@ class Nami {
|
|
|
61975
62635
|
}
|
|
61976
62636
|
config.initialConfig = {};
|
|
61977
62637
|
}
|
|
62638
|
+
/**
|
|
62639
|
+
* Subscribes the resume config refresh to the host's foreground signal, once per process —
|
|
62640
|
+
* a reconfigure must not stack a second listener (the Android defect NAM-4541 fixed).
|
|
62641
|
+
* Registered before startup runs; `resume()` itself skips while startup is in flight.
|
|
62642
|
+
*/
|
|
62643
|
+
listenForAppForeground() {
|
|
62644
|
+
if (__classPrivateFieldGet(this, _Nami_removeForegroundListener, "f"))
|
|
62645
|
+
return;
|
|
62646
|
+
__classPrivateFieldSet(this, _Nami_removeForegroundListener, getPlatformAdapters().device.onAppForeground?.(() => {
|
|
62647
|
+
void NamiRefs.instance.resume();
|
|
62648
|
+
}) ?? null, "f");
|
|
62649
|
+
}
|
|
61978
62650
|
async initializeSDK(config) {
|
|
61979
62651
|
let fullReconfig = false;
|
|
61980
62652
|
let partialReconfig = false;
|
|
@@ -62015,6 +62687,7 @@ class Nami {
|
|
|
62015
62687
|
// configure SDK with new config
|
|
62016
62688
|
__classPrivateFieldSet(this, _Nami_isInitialized, true, "f");
|
|
62017
62689
|
NamiAPI.configure(config);
|
|
62690
|
+
this.listenForAppForeground();
|
|
62018
62691
|
await NamiRefs.instance.init(config);
|
|
62019
62692
|
logger.info("SDK successfully initialized!");
|
|
62020
62693
|
const state = partialReconfig || fullReconfig ? RECONFIG_SUCCESS : INITIAL_SUCCESS;
|
|
@@ -62026,7 +62699,7 @@ class Nami {
|
|
|
62026
62699
|
};
|
|
62027
62700
|
}
|
|
62028
62701
|
}
|
|
62029
|
-
_Nami_isInitialized = new WeakMap();
|
|
62702
|
+
_Nami_isInitialized = new WeakMap(), _Nami_removeForegroundListener = new WeakMap();
|
|
62030
62703
|
Nami.instance = new Nami();
|
|
62031
62704
|
|
|
62032
62705
|
const VALID_STYLES = new Set(['fullscreen', 'sheet', 'compact_sheet', 'modal']);
|
|
@@ -65996,4 +66669,4 @@ var internal = /*#__PURE__*/Object.freeze({
|
|
|
65996
66669
|
NamiPurchaseManager: NamiPurchaseManager
|
|
65997
66670
|
});
|
|
65998
66671
|
|
|
65999
|
-
export { ALREADY_CONFIGURED, ANONYMOUS_MODE, ANONYMOUS_MODE_ALREADY_OFF, ANONYMOUS_MODE_ALREADY_ON, ANONYMOUS_MODE_LOGIN_NOT_ALLOWED, ANONYMOUS_UUID, APIError, API_ACTIVE_ENTITLEMENTS, API_CAMPAIGN_RULES, API_CAMPAIGN_SESSION_TIMESTAMP, API_CONFIG, API_MAX_CALLS_LIMIT, API_PAYWALLS, API_PRODUCTS, API_RETRY_DELAY_SEC, API_TIMEOUT_LIMIT, API_TRAFFIC_OBSERVER, API_VERSION, AUTH_DEVICE, AVAILABLE_ACTIVE_ENTITLEMENTS_CHANGED, AVAILABLE_CAMPAIGNS_CHANGED, AccountStateAction, AnonymousCDPError, AnonymousLoginError, AnonymousModeAlreadyOffError, AnonymousModeAlreadyOnError, BASE_STAGING_URL, BASE_URL, BASE_URL_PATH, BadRequestError, BasicNamiFlow, BorderMap, BorderSideMap, CAMPAIGN_NOT_AVAILABLE, CUSTOMER_ATTRIBUTES_KEY_PREFIX, CUSTOMER_JOURNEY_STATE_CHANGED, CUSTOM_HOST_PREFIX, CampaignNotAvailableError, CampaignRuleConversionEventType, CampaignRuleRepository, Capabilities, ClientError, ConfigRepository, ConflictError, CustomerJourneyRepository, DEFAULT_JOURNEY_SETTINGS, DEVELOPMENT, DEVICE_API_TIMEOUT_LIMIT, DEVICE_ID_NOT_SET, DEVICE_ID_REQUIRED, DISABLE_ASYNC_LOGIN_LOGOUT, DeviceIDRequiredError, DeviceRepository, EXTENDED_CLIENT_INFO_DELIMITER, EXTENDED_CLIENT_INFO_PREFIX, EXTENDED_PLATFORM, EXTENDED_PLATFORM_VERSION, EXTERNAL_ID_REQUIRED, EntitlementRepository, EntitlementUtils, ExternalIDRequiredError, FLOW_SCREENS_NOT_AVAILABLE, FlowScreensNotAvailableError, HTML_REGEX, INITIAL_APP_CONFIG, INITIAL_CAMPAIGN_RULES, INITIAL_PAYWALLS, INITIAL_PRODUCTS, INITIAL_SESSION_COUNTER_VALUE, INITIAL_SUCCESS, InternalServerError, KEY_SESSION_COUNTER, LIQUID_VARIABLE_REGEX, LOCAL_NAMI_ENTITLEMENTS, LOG_HTTP_REQUESTS, LOG_HTTP_TRAFFIC, LaunchCampaignError, LaunchContextResolver, LogLevel, NAMI_CONFIGURATION, NAMI_CUSTOMER_JOURNEY_STATE, NAMI_LANGUAGE_CODE, NAMI_LAST_IMPRESSION_ID, NAMI_LAUNCH_ID, NAMI_PROFILE, NAMI_PURCHASE_CHANNEL, NAMI_PURCHASE_IMPRESSION_ID, NAMI_SDK_PACKAGE_VERSION, NAMI_SDK_VERSION, NAMI_SESSION_ID, NAMI_STORAGE_KEYS, Nami, NamiAPI, NamiAnimationType, NamiCampaignManager$1 as NamiCampaignManager, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager$1 as NamiCustomerManager, NamiEntitlementManager$1 as NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager$1 as NamiFlowManager, NamiFlowStepType, NamiHandoffTag, NamiPaywallAction, NamiPaywallManager$1 as NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiPermissionOutcome, NamiPurchaseManager$1 as NamiPurchaseManager, NamiRefs, NamiReservedActions, NotFoundError, PAYWALL_ACTION_EVENT, PLATFORM_ID_REQUIRED, PRODUCTION, PaywallManagerEvents, PaywallRepository, PaywallState, PlacementLabelResolver, PlatformIDRequiredError, ProductRepository, RECONFIG_SUCCESS, RetryLimitExceededError, SDKNotInitializedError, SDK_NOT_INITIALIZED, SERVER_NAMI_ENTITLEMENTS, SESSION_REQUIRED, SHOULD_SHOW_LOADING_INDICATOR, SKU_TEXT_REGEX, SMART_TEXT_PATTERN, STARTUP_TELEMETRY, STATUS_BAD_REQUEST, STATUS_CONFLICT, STATUS_INTERNAL_SERVER_ERROR, STATUS_NOT_FOUND, STATUS_SUCCESS, SessionService, SimpleEventTarget, StorageService, TRANSPORT_FAILURE_STATUS, UNABLE_TO_UPDATE_CDP_ID, USE_STAGING_API, VALIDATE_PRODUCT_GROUPS, VAR_REGEX, NamiProfileManager$1 as _NamiProfileManager, internal as _internal, activateEntitlementByPurchase, activeEntitlements, aggregateScreenreaderText, allCampaigns, allPaywalls, applyEntitlementActivation, audienceSplitPosition, bestUrlCampaignMatch, bigintToUuid, checkAnySkuHasPromoOffer, checkAnySkuHasTrialOffer, convertISO8601PeriodToText, convertLocale, convertOfferToPricingPhase, createNamiEntitlements, currentSku, didReceiveResponse, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, getJourneySettings, getPaywall, getPaywallDataFromLabel, getPercentagePriceDifference, getPeriodNumberInDays, getPeriodNumberInWeeks, getPlatformAdapters, getPriceDifference, getPricePerMonth, getProductDetail, getPurchaseAdapter, getReferenceSku, getSkuProductDetailKeys, getSkuSmartTextValue, getSlideSmartTextValue, getStandardBillingPeriod, getTranslate, getUrlParams, handleErrors, hasAllPaywalls, hasCapability, hasPurchaseManagement, initialState, invokeHandler, isAnonymousMode, isInitialConfigCompressed, isNamiFlowCampaign, isSubscription, isValidISODate, isValidUrl, logger, mapAnonymousCampaigns, namiBuySKU, normalizeLaunchContext, parseToSemver, postConversion, productDetail, registerAPITrafficObserver, registerPlatformAdapters, registerPurchaseAdapter, resolveLoopSource, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateForm, validateMinSDKVersion, validateTextInput };
|
|
66672
|
+
export { ALREADY_CONFIGURED, ANONYMOUS_MODE, ANONYMOUS_MODE_ALREADY_OFF, ANONYMOUS_MODE_ALREADY_ON, ANONYMOUS_MODE_LOGIN_NOT_ALLOWED, ANONYMOUS_UUID, APIError, API_ACTIVE_ENTITLEMENTS, API_CAMPAIGN_RULES, API_CAMPAIGN_SESSION_TIMESTAMP, API_CONFIG, API_MAX_CALLS_LIMIT, API_PAYWALLS, API_PRODUCTS, API_RETRY_DELAY_SEC, API_TIMEOUT_LIMIT, API_TRAFFIC_OBSERVER, API_VERSION, AUTH_DEVICE, AVAILABLE_ACTIVE_ENTITLEMENTS_CHANGED, AVAILABLE_CAMPAIGNS_CHANGED, AccountStateAction, AnonymousCDPError, AnonymousLoginError, AnonymousModeAlreadyOffError, AnonymousModeAlreadyOnError, BASE_STAGING_URL, BASE_URL, BASE_URL_PATH, BadRequestError, BasicNamiFlow, BorderMap, BorderSideMap, CAMPAIGN_CACHE_TTL_PREFIX, CAMPAIGN_NOT_AVAILABLE, CONFIG_API_TIMEOUT_LIMIT, CONFIG_MAX_ATTEMPTS, CUSTOMER_ATTRIBUTES_KEY_PREFIX, CUSTOMER_JOURNEY_STATE_CHANGED, CUSTOM_HOST_PREFIX, CampaignNotAvailableError, CampaignRuleConversionEventType, CampaignRuleRepository, Capabilities, ClientError, ConfigRepository, ConflictError, CustomerJourneyRepository, DEFAULT_JOURNEY_SETTINGS, DEVELOPMENT, DEVICE_API_TIMEOUT_LIMIT, DEVICE_ID_NOT_SET, DEVICE_ID_REQUIRED, DISABLE_ASYNC_LOGIN_LOGOUT, DeviceIDRequiredError, DeviceRepository, EXTENDED_CLIENT_INFO_DELIMITER, EXTENDED_CLIENT_INFO_PREFIX, EXTENDED_PLATFORM, EXTENDED_PLATFORM_VERSION, EXTERNAL_ID_REQUIRED, EntitlementRepository, EntitlementUtils, ExternalIDRequiredError, FLOW_SCREENS_NOT_AVAILABLE, FlowScreensNotAvailableError, HTML_REGEX, INITIAL_APP_CONFIG, INITIAL_CAMPAIGN_RULES, INITIAL_PAYWALLS, INITIAL_PRODUCTS, INITIAL_SESSION_COUNTER_VALUE, INITIAL_SUCCESS, InternalServerError, KEY_SESSION_COUNTER, LIQUID_VARIABLE_REGEX, LOCAL_NAMI_ENTITLEMENTS, LOG_HTTP_REQUESTS, LOG_HTTP_TRAFFIC, LaunchCampaignError, LaunchContextResolver, LogLevel, NAMI_CONFIGURATION, NAMI_CUSTOMER_JOURNEY_STATE, NAMI_LANGUAGE_CODE, NAMI_LAST_IMPRESSION_ID, NAMI_LAUNCH_ID, NAMI_PROFILE, NAMI_PURCHASE_CHANNEL, NAMI_PURCHASE_IMPRESSION_ID, NAMI_SDK_PACKAGE_VERSION, NAMI_SDK_VERSION, NAMI_SESSION_ID, NAMI_STORAGE_KEYS, Nami, NamiAPI, NamiAnimationType, NamiCampaignManager$1 as NamiCampaignManager, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager$1 as NamiCustomerManager, NamiEntitlementManager$1 as NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager$1 as NamiFlowManager, NamiFlowStepType, NamiHandoffTag, NamiPaywallAction, NamiPaywallManager$1 as NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiPermissionOutcome, NamiPurchaseManager$1 as NamiPurchaseManager, NamiRefs, NamiReservedActions, NotFoundError, PAYWALL_ACTION_EVENT, PLATFORM_ID_REQUIRED, PRODUCTION, PaywallManagerEvents, PaywallRepository, PaywallState, PlacementLabelResolver, PlatformIDRequiredError, ProductRepository, RECONFIG_SUCCESS, RESUME_THROTTLE_MS, RetryLimitExceededError, SDKNotInitializedError, SDK_NOT_INITIALIZED, SERVER_NAMI_ENTITLEMENTS, SESSION_REQUIRED, SHOULD_SHOW_LOADING_INDICATOR, SIMULATE_CONFIG_FETCH_FAILURE, SIMULATE_PAYWALL_FETCH_FAILURE, SKU_TEXT_REGEX, SMART_TEXT_PATTERN, STARTUP_TELEMETRY, STATUS_BAD_REQUEST, STATUS_CONFLICT, STATUS_INTERNAL_SERVER_ERROR, STATUS_NOT_FOUND, STATUS_SUCCESS, SessionService, SimpleEventTarget, StorageService, TRANSPORT_FAILURE_STATUS, UNABLE_TO_UPDATE_CDP_ID, USE_STAGING_API, VALIDATE_PRODUCT_GROUPS, VAR_REGEX, NamiProfileManager$1 as _NamiProfileManager, internal as _internal, activateEntitlementByPurchase, activeEntitlements, aggregateScreenreaderText, allCampaigns, allPaywalls, applyEntitlementActivation, audienceSplitPosition, bestUrlCampaignMatch, bigintToUuid, checkAnySkuHasPromoOffer, checkAnySkuHasTrialOffer, convertISO8601PeriodToText, convertLocale, convertOfferToPricingPhase, createNamiEntitlements, currentSku, didReceiveResponse, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, getJourneySettings, getPaywall, getPaywallDataFromLabel, getPercentagePriceDifference, getPeriodNumberInDays, getPeriodNumberInWeeks, getPlatformAdapters, getPriceDifference, getPricePerMonth, getProductDetail, getPurchaseAdapter, getReferenceSku, getSkuProductDetailKeys, getSkuSmartTextValue, getSlideSmartTextValue, getStandardBillingPeriod, getTranslate, getUrlParams, handleErrors, hasAllPaywalls, hasCapability, hasPurchaseManagement, initialState, invokeHandler, isAnonymousMode, isInitialConfigCompressed, isNamiFlowCampaign, isSubscription, isValidISODate, isValidUrl, logger, mapAnonymousCampaigns, namiBuySKU, normalizeLaunchContext, parseToSemver, postConversion, productDetail, registerAPITrafficObserver, registerPlatformAdapters, registerPurchaseAdapter, resolveLoopSource, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateForm, validateMinSDKVersion, validateTextInput };
|