@namiml/sdk-core 3.4.0-dev.202605190129 → 3.4.0-dev.202605191719

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 CHANGED
@@ -98,7 +98,7 @@ const {
98
98
  // version — stamped by scripts/version.sh
99
99
  NAMI_SDK_VERSION: exports.NAMI_SDK_VERSION = "3.4.0",
100
100
  // full package version including dev suffix — stamped by scripts/version.sh
101
- NAMI_SDK_PACKAGE_VERSION: exports.NAMI_SDK_PACKAGE_VERSION = "3.4.0-dev.202605190129",
101
+ NAMI_SDK_PACKAGE_VERSION: exports.NAMI_SDK_PACKAGE_VERSION = "3.4.0-dev.202605191719",
102
102
  // environments
103
103
  PRODUCTION: exports.PRODUCTION = "production", DEVELOPMENT: exports.DEVELOPMENT = "development",
104
104
  // error messages
@@ -6849,7 +6849,7 @@ async function withRetry(url, options, timeout = exports.API_TIMEOUT_LIMIT, retr
6849
6849
  if (logTraffic && options?.body) {
6850
6850
  logger.debug(`[HTTP] Request body: ${options.body}`);
6851
6851
  }
6852
- const response = await timeoutRequest(url, options, timeout);
6852
+ const response = await timeoutRequest$1(url, options, timeout);
6853
6853
  if (!response.ok) {
6854
6854
  if (logTraffic) {
6855
6855
  const errorBody = await response.clone().text();
@@ -6879,7 +6879,7 @@ async function withRetry(url, options, timeout = exports.API_TIMEOUT_LIMIT, retr
6879
6879
  const response = await fetchWithRetry();
6880
6880
  return response.json();
6881
6881
  }
6882
- async function timeoutRequest(url, options = {}, timeout) {
6882
+ async function timeoutRequest$1(url, options = {}, timeout) {
6883
6883
  const controller = new AbortController();
6884
6884
  const timeoutId = setTimeout(() => controller.abort(), timeout);
6885
6885
  options["signal"] = controller.signal;
@@ -8309,6 +8309,161 @@ class ProductRepository {
8309
8309
  }
8310
8310
  ProductRepository.instance = new ProductRepository();
8311
8311
 
8312
+ const DEFAULTS = {
8313
+ maxAttempts: 3,
8314
+ baseDelayMs: 200,
8315
+ maxDelayMs: 2000,
8316
+ budgetMs: 5000,
8317
+ };
8318
+ const TRANSIENT_STATUSES = new Set([408, 425, 429]);
8319
+ const TRANSIENT_ERROR_CODES = new Set(["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT"]);
8320
+ class NonRetryableHttpError extends Error {
8321
+ constructor(status, url) {
8322
+ super(`CDN fetch failed with non-retryable status ${status} for ${url}`);
8323
+ this.name = "NonRetryableHttpError";
8324
+ this.status = status;
8325
+ this.url = url;
8326
+ }
8327
+ }
8328
+ class RetryExhaustedError extends Error {
8329
+ constructor(url, attempts, lastStatus) {
8330
+ super(`CDN fetch exhausted ${attempts} attempts for ${url}${lastStatus !== undefined ? ` (last status ${lastStatus})` : ""}`);
8331
+ this.name = "RetryExhaustedError";
8332
+ this.url = url;
8333
+ this.attempts = attempts;
8334
+ this.lastStatus = lastStatus;
8335
+ }
8336
+ }
8337
+ /**
8338
+ * Classify a failure as transient (worth retrying) or not.
8339
+ *
8340
+ * Transient:
8341
+ * - HTTP 408 / 425 / 429 / any 5xx (504 Gateway Timeout from the CDN edge
8342
+ * is the motivating production case)
8343
+ * - AbortError from the request timeout
8344
+ * - Any TypeError thrown by fetch — covers Safari's "Load failed",
8345
+ * Chrome's "Failed to fetch", Firefox's "NetworkError ...", and
8346
+ * CORS-blocked responses from the CDN edge (a 504 without
8347
+ * Access-Control-Allow-Origin surfaces as a TypeError, not a Response)
8348
+ * - ECONNRESET/ECONNREFUSED/ENOTFOUND/ETIMEDOUT in Node test envs
8349
+ *
8350
+ * Non-transient: every other status (including 404 — a missing paywall is
8351
+ * genuinely gone; retrying just delays the inevitable failure).
8352
+ */
8353
+ function isTransientFailure(err, status) {
8354
+ if (typeof status === "number") {
8355
+ return TRANSIENT_STATUSES.has(status) || status >= 500;
8356
+ }
8357
+ if (err && typeof err === "object") {
8358
+ const e = err;
8359
+ if (e.name === "AbortError")
8360
+ return true;
8361
+ if (e.code && TRANSIENT_ERROR_CODES.has(e.code))
8362
+ return true;
8363
+ // fetch() throws TypeError for network-layer failures (DNS/TCP/TLS,
8364
+ // CORS-blocked, edge dropped). HTTP status failures arrive as fulfilled
8365
+ // responses with !response.ok — never as a thrown TypeError. So any
8366
+ // TypeError reaching this catch is by definition transient. Don't filter
8367
+ // on message text: Safari ("Load failed"), Chrome ("Failed to fetch"),
8368
+ // and Firefox ("NetworkError ...") all differ, and any future variation
8369
+ // would silently be classified non-transient.
8370
+ if (err instanceof TypeError)
8371
+ return true;
8372
+ }
8373
+ return false;
8374
+ }
8375
+ async function timeoutRequest(url, timeout) {
8376
+ const controller = new AbortController();
8377
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
8378
+ try {
8379
+ return await fetch(url, { signal: controller.signal });
8380
+ }
8381
+ finally {
8382
+ clearTimeout(timeoutId);
8383
+ }
8384
+ }
8385
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8386
+ /**
8387
+ * Fetch a single CDN-hosted paywall with bounded retries on transient
8388
+ * failures. Exponential backoff (baseDelayMs * 2^n, capped at maxDelayMs)
8389
+ * with ±50% jitter; the whole loop is clamped to budgetMs wall-clock so
8390
+ * the parallel-fetch window can't balloon when one edge is degraded.
8391
+ *
8392
+ * Throws NonRetryableHttpError for hard 4xx (including 404), or
8393
+ * RetryExhaustedError when all attempts and/or the budget are spent.
8394
+ */
8395
+ async function fetchWithCdnRetry(url, opts = {}) {
8396
+ const maxAttempts = opts.maxAttempts ?? DEFAULTS.maxAttempts;
8397
+ const baseDelayMs = opts.baseDelayMs ?? DEFAULTS.baseDelayMs;
8398
+ const maxDelayMs = opts.maxDelayMs ?? DEFAULTS.maxDelayMs;
8399
+ const budgetMs = opts.budgetMs ?? DEFAULTS.budgetMs;
8400
+ const timeoutMs = opts.timeoutMs ?? exports.API_TIMEOUT_LIMIT;
8401
+ const randomFn = opts.randomFn ?? Math.random;
8402
+ const sleepFn = opts.sleepFn ?? defaultSleep;
8403
+ const logRequests = shouldLogHTTPRequests();
8404
+ const logTraffic = shouldLogHTTPTraffic();
8405
+ const start = Date.now();
8406
+ let attempt = 0;
8407
+ let lastStatus;
8408
+ let lastError;
8409
+ while (attempt < maxAttempts) {
8410
+ if (attempt > 0) {
8411
+ const elapsed = Date.now() - start;
8412
+ if (elapsed >= budgetMs)
8413
+ break;
8414
+ const baseDelay = Math.min(baseDelayMs * Math.pow(2, attempt - 1), maxDelayMs);
8415
+ // ±50% jitter: random in [0,1) → factor in [-0.5, 0.5)
8416
+ const jitter = baseDelay * (randomFn() - 0.5);
8417
+ const remaining = Math.max(0, budgetMs - elapsed);
8418
+ const sleepFor = Math.min(Math.max(0, baseDelay + jitter), remaining);
8419
+ await sleepFn(sleepFor);
8420
+ // If the sleep consumed the rest of the budget, don't fire another fetch.
8421
+ if (Date.now() - start >= budgetMs)
8422
+ break;
8423
+ }
8424
+ try {
8425
+ if (logRequests || logTraffic) {
8426
+ logger.debug(`[HTTP] GET ${url} (cdn attempt ${attempt + 1}/${maxAttempts})`);
8427
+ }
8428
+ const response = await timeoutRequest(url, timeoutMs);
8429
+ if (!response.ok) {
8430
+ if (logTraffic) {
8431
+ const errorBody = await response.clone().text();
8432
+ logger.debug(`[HTTP] Response ${response.status}: ${errorBody}`);
8433
+ }
8434
+ lastStatus = response.status;
8435
+ if (isTransientFailure(undefined, response.status)) {
8436
+ attempt++;
8437
+ continue;
8438
+ }
8439
+ throw new NonRetryableHttpError(response.status, url);
8440
+ }
8441
+ if (logTraffic) {
8442
+ const responseBody = await response.clone().text();
8443
+ logger.debug(`[HTTP] Response ${response.status}: ${responseBody}`);
8444
+ }
8445
+ const data = (await response.json());
8446
+ return { data, retryCount: attempt };
8447
+ }
8448
+ catch (err) {
8449
+ if (err instanceof NonRetryableHttpError)
8450
+ throw err;
8451
+ if (isTransientFailure(err)) {
8452
+ lastError = err;
8453
+ attempt++;
8454
+ continue;
8455
+ }
8456
+ throw err;
8457
+ }
8458
+ }
8459
+ // If we exited the loop without success but never saw a transient response,
8460
+ // fall back to surfacing the last raw error. Otherwise raise the canonical
8461
+ // exhausted-attempts signal with the last seen status for telemetry.
8462
+ if (lastStatus === undefined && lastError !== undefined)
8463
+ throw lastError;
8464
+ throw new RetryExhaustedError(url, attempt, lastStatus);
8465
+ }
8466
+
8312
8467
  class PaywallRepository {
8313
8468
  async fetchPaywalls() {
8314
8469
  const authDevice = storageService.getDevice();
@@ -8359,17 +8514,22 @@ class PaywallRepository {
8359
8514
  return data?.results || [];
8360
8515
  }
8361
8516
  async fetchPaywallByUrl(url) {
8362
- return withRetry(url);
8517
+ return fetchWithCdnRetry(url);
8363
8518
  }
8364
8519
  async fetchPaywallsByUrls(urls) {
8365
8520
  const uniqueUrls = [...new Set(urls)];
8366
8521
  const results = await Promise.allSettled(uniqueUrls.map((url) => this.fetchPaywallByUrl(url)));
8367
8522
  const paywalls = [];
8523
+ let retryCount = 0;
8368
8524
  for (const result of results) {
8369
8525
  if (result.status === "fulfilled") {
8370
- paywalls.push(result.value);
8526
+ paywalls.push(result.value.data);
8527
+ retryCount += result.value.retryCount;
8371
8528
  }
8372
8529
  else {
8530
+ // Note: an exhausted-failure path contributed retries we can't see
8531
+ // from the rejected promise, so retryCount under-reports failures.
8532
+ // Acceptable for the telemetry signal; revisit if full accounting matters.
8373
8533
  logger.error(`Failed to fetch individual paywall: ${result.reason}`);
8374
8534
  }
8375
8535
  }
@@ -8378,9 +8538,9 @@ class PaywallRepository {
8378
8538
  if (valid.length > 0) {
8379
8539
  storageService.setPaywalls(exports.API_PAYWALLS, valid);
8380
8540
  }
8381
- return valid;
8541
+ return { paywalls: valid, retryCount };
8382
8542
  }
8383
- return paywalls;
8543
+ return { paywalls, retryCount };
8384
8544
  }
8385
8545
  validatePaywalls(paywalls) {
8386
8546
  return paywalls.filter((paywall) => {
@@ -11856,10 +12016,13 @@ class NamiRefs {
11856
12016
  const useIndividual = !campaignRepo.useLegacyPaywallFetch &&
11857
12017
  CampaignRuleRepository.hasPaywallUrls(rawCampaigns);
11858
12018
  let paywalls;
12019
+ let retryCount = 0;
11859
12020
  const paywallsStartTime = Date.now();
11860
12021
  if (useIndividual) {
11861
12022
  const urls = CampaignRuleRepository.extractPaywallUrls(rawCampaigns);
11862
- paywalls = await paywallRepo.fetchPaywallsByUrls(urls);
12023
+ const result = await paywallRepo.fetchPaywallsByUrls(urls);
12024
+ paywalls = result.paywalls;
12025
+ retryCount = result.retryCount;
11863
12026
  }
11864
12027
  else {
11865
12028
  paywalls = await paywallRepo.fetchPaywalls();
@@ -11868,7 +12031,8 @@ class NamiRefs {
11868
12031
  const totalDuration = Date.now() - startTime;
11869
12032
  logger.info(`Paywall fetch telemetry: strategy=${useIndividual ? "individual" : "bulk"}, ` +
11870
12033
  `count=${paywalls.length}, total=${totalDuration}ms, ` +
11871
- `campaigns=${campaignRulesDuration}ms, paywalls=${paywallsDuration}ms`);
12034
+ `campaigns=${campaignRulesDuration}ms, paywalls=${paywallsDuration}ms` +
12035
+ `${useIndividual ? `, retry_count=${retryCount}` : ""}`);
11872
12036
  return campaignRepo.finalizeCampaignRules(rawCampaigns, paywalls);
11873
12037
  }
11874
12038
  reRenderPaywall() {
@@ -12831,21 +12995,46 @@ class NamiProfileManager {
12831
12995
  }
12832
12996
  PaywallState.setIsLoggedIn(!!this.externalId);
12833
12997
  }
12834
- async login(externalId) {
12998
+ /**
12999
+ * Update local profile state for login: persist the external id and flip the
13000
+ * logged-in flag in PaywallState. No network I/O — pair with `loginRemote`
13001
+ * to perform the actual API call. Used by the async login/logout fast path.
13002
+ */
13003
+ loginLocal(externalId) {
13004
+ this.setExternalId(externalId).save();
13005
+ PaywallState.setIsLoggedIn(true);
13006
+ }
13007
+ /**
13008
+ * Perform the login network call only. If the server returns a different
13009
+ * `external_id` than the locally stored value, reconcile it into local state.
13010
+ */
13011
+ async loginRemote(externalId) {
12835
13012
  const loginData = await NamiAPI.instance.login(externalId);
12836
- if (loginData.external_id) {
12837
- PaywallState.setIsLoggedIn(true);
13013
+ if (loginData.external_id && loginData.external_id !== this.externalId) {
13014
+ this.setExternalId(loginData.external_id).save();
12838
13015
  }
12839
- this
12840
- .setExternalId(loginData.external_id)
12841
- .save();
12842
13016
  }
12843
- async logout() {
12844
- await NamiAPI.instance.logout();
13017
+ /**
13018
+ * Update local profile state for logout: clear the external id, persist,
13019
+ * and flip the logged-in flag in PaywallState. Used by the async fast path.
13020
+ */
13021
+ logoutLocal() {
13022
+ this.setExternalId(undefined).save();
12845
13023
  PaywallState.setIsLoggedIn(false);
12846
- this
12847
- .setExternalId(undefined)
12848
- .save();
13024
+ }
13025
+ /**
13026
+ * Perform the logout network call only.
13027
+ */
13028
+ async logoutRemote() {
13029
+ await NamiAPI.instance.logout();
13030
+ }
13031
+ async login(externalId) {
13032
+ this.loginLocal(externalId);
13033
+ await this.loginRemote(externalId);
13034
+ }
13035
+ async logout() {
13036
+ this.logoutLocal();
13037
+ await this.logoutRemote();
12849
13038
  }
12850
13039
  }
12851
13040
  NamiProfileManager.instance = new NamiProfileManager();
@@ -13165,6 +13354,21 @@ class NamiFlowManager {
13165
13354
  }
13166
13355
  }
13167
13356
 
13357
+ const SHOULD_SHOW_LOADING_INDICATOR = false;
13358
+ const DISABLE_ASYNC_LOGIN_LOGOUT = "disableAsyncLoginLogout";
13359
+ /**
13360
+ * Returns true when `disableAsyncLoginLogout` appears in the active `namiCommands` list,
13361
+ * meaning login/logout should await the underlying API call before notifying handlers.
13362
+ *
13363
+ * Default behavior (flag absent) is the async/fast path: update local state and notify
13364
+ * handlers immediately while dispatching the API call as fire-and-forget.
13365
+ *
13366
+ * Internal feature flag — opt-out via `Nami.configure({ namiCommands: [...] })`.
13367
+ */
13368
+ const isAsyncLoginLogoutDisabled = () => {
13369
+ return storageService.getNamiConfig()?.namiCommands?.includes(DISABLE_ASYNC_LOGIN_LOGOUT) ?? false;
13370
+ };
13371
+
13168
13372
  /**
13169
13373
  * @class NamiCustomerManager
13170
13374
  * Provides methods for managing customer-related functionality.
@@ -13226,6 +13430,30 @@ class NamiCustomerManager {
13226
13430
  this.invokeStateHandler(exports.AccountStateAction.LOGIN, false, error);
13227
13431
  throw error;
13228
13432
  }
13433
+ if (!isAsyncLoginLogoutDisabled()) {
13434
+ // Default sync/fast path: update local state, notify handlers, and fire
13435
+ // lifecycle immediately. Dispatch the underlying API call and entitlement
13436
+ // refresh fire-and-forget — failures are logged but do not surface.
13437
+ NamiProfileManager.instance.loginLocal(externalId);
13438
+ this.invokeStateHandler(exports.AccountStateAction.LOGIN, true);
13439
+ if (NamiFlowManager.instance.flowOpen) {
13440
+ const currentFlow = NamiFlowManager.instance.currentFlow;
13441
+ const currentStep = currentFlow?.currentFlowStep;
13442
+ if (currentStep) {
13443
+ if (Nami.instance.maxLogging) {
13444
+ logger.debug(`[NamiCustomerManager] async login success — triggering __login_success__ on step ${currentStep.id}`);
13445
+ }
13446
+ currentFlow.executeLifecycle(currentStep, NamiReservedActions.LOGIN_SUCCESS);
13447
+ }
13448
+ }
13449
+ void NamiProfileManager.instance.loginRemote(externalId).catch((err) => {
13450
+ logger.error('[NamiCustomerManager] async login API failed', err);
13451
+ });
13452
+ void EntitlementRepository.instance.fetchActiveEntitlements().catch((err) => {
13453
+ logger.error('[NamiCustomerManager] async login entitlement refresh failed', err);
13454
+ });
13455
+ return;
13456
+ }
13229
13457
  try {
13230
13458
  await NamiProfileManager.instance.login(externalId);
13231
13459
  await EntitlementRepository.instance.fetchActiveEntitlements();
@@ -13271,6 +13499,30 @@ class NamiCustomerManager {
13271
13499
  this.invokeStateHandler(exports.AccountStateAction.LOGOUT, false, error);
13272
13500
  throw error;
13273
13501
  }
13502
+ if (!isAsyncLoginLogoutDisabled()) {
13503
+ // Default sync/fast path: clear local state, notify handlers, and fire
13504
+ // lifecycle immediately. Dispatch the underlying API call and entitlement
13505
+ // refresh fire-and-forget — failures are logged but do not surface.
13506
+ NamiProfileManager.instance.logoutLocal();
13507
+ this.invokeStateHandler(exports.AccountStateAction.LOGOUT, true);
13508
+ if (NamiFlowManager.instance.flowOpen) {
13509
+ const currentFlow = NamiFlowManager.instance.currentFlow;
13510
+ const currentStep = currentFlow?.currentFlowStep;
13511
+ if (currentStep) {
13512
+ if (Nami.instance.maxLogging) {
13513
+ logger.debug(`[NamiCustomerManager] async logout success — triggering __logout_success__ on step ${currentStep.id}`);
13514
+ }
13515
+ currentFlow.executeLifecycle(currentStep, NamiReservedActions.LOGOUT_SUCCESS);
13516
+ }
13517
+ }
13518
+ void NamiProfileManager.instance.logoutRemote().catch((err) => {
13519
+ logger.error('[NamiCustomerManager] async logout API failed', err);
13520
+ });
13521
+ void EntitlementRepository.instance.fetchActiveEntitlements().catch((err) => {
13522
+ logger.error('[NamiCustomerManager] async logout entitlement refresh failed', err);
13523
+ });
13524
+ return;
13525
+ }
13274
13526
  try {
13275
13527
  await NamiProfileManager.instance.logout();
13276
13528
  await EntitlementRepository.instance.fetchActiveEntitlements();
@@ -14705,8 +14957,6 @@ exports.CampaignRuleConversionEventType = void 0;
14705
14957
  CampaignRuleConversionEventType["IN_APP"] = "in_app";
14706
14958
  })(exports.CampaignRuleConversionEventType || (exports.CampaignRuleConversionEventType = {}));
14707
14959
 
14708
- const SHOULD_SHOW_LOADING_INDICATOR = false;
14709
-
14710
14960
  function parseToSemver(versionString) {
14711
14961
  const [semVer, major, minor, patch, prerelease, buildmetadata] = versionString.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/) ?? [];
14712
14962
  return {
@@ -64181,6 +64431,7 @@ exports.ClientError = ClientError;
64181
64431
  exports.ConfigRepository = ConfigRepository;
64182
64432
  exports.ConflictError = ConflictError;
64183
64433
  exports.CustomerJourneyRepository = CustomerJourneyRepository;
64434
+ exports.DISABLE_ASYNC_LOGIN_LOGOUT = DISABLE_ASYNC_LOGIN_LOGOUT;
64184
64435
  exports.DeviceIDRequiredError = DeviceIDRequiredError;
64185
64436
  exports.DeviceRepository = DeviceRepository;
64186
64437
  exports.EntitlementRepository = EntitlementRepository;
package/dist/index.d.ts CHANGED
@@ -2133,6 +2133,26 @@ declare class NamiProfileManager {
2133
2133
  isLoggedIn(): boolean;
2134
2134
  private save;
2135
2135
  private load;
2136
+ /**
2137
+ * Update local profile state for login: persist the external id and flip the
2138
+ * logged-in flag in PaywallState. No network I/O — pair with `loginRemote`
2139
+ * to perform the actual API call. Used by the async login/logout fast path.
2140
+ */
2141
+ loginLocal(externalId: string): void;
2142
+ /**
2143
+ * Perform the login network call only. If the server returns a different
2144
+ * `external_id` than the locally stored value, reconcile it into local state.
2145
+ */
2146
+ loginRemote(externalId: string): Promise<void>;
2147
+ /**
2148
+ * Update local profile state for logout: clear the external id, persist,
2149
+ * and flip the logged-in flag in PaywallState. Used by the async fast path.
2150
+ */
2151
+ logoutLocal(): void;
2152
+ /**
2153
+ * Perform the logout network call only.
2154
+ */
2155
+ logoutRemote(): Promise<void>;
2136
2156
  login(externalId: string): Promise<void>;
2137
2157
  logout(): Promise<void>;
2138
2158
  }
@@ -2607,6 +2627,7 @@ declare const LIQUID_VARIABLE_REGEX: RegExp;
2607
2627
  declare const NAMI_STORAGE_KEYS: readonly string[];
2608
2628
 
2609
2629
  declare const SHOULD_SHOW_LOADING_INDICATOR = false;
2630
+ declare const DISABLE_ASYNC_LOGIN_LOGOUT = "disableAsyncLoginLogout";
2610
2631
 
2611
2632
  declare const getBaseUrl: (namiCommands?: string[]) => string;
2612
2633
  declare const getExtendedClientInfo: (namiCommands: string[]) => ExtendedPlatformInfo;
@@ -2974,13 +2995,26 @@ declare class EntitlementRepository {
2974
2995
  private invokeActiveEntitlementsHandler;
2975
2996
  }
2976
2997
 
2998
+ /**
2999
+ * Result of a single per-paywall CDN fetch — carries the parsed body
3000
+ * alongside the number of retries it took to land. Aggregated upstream
3001
+ * by PaywallRepository.fetchPaywallsByUrls and surfaced in telemetry.
3002
+ */
3003
+ type CdnFetchResult<T> = {
3004
+ data: T;
3005
+ retryCount: number;
3006
+ };
3007
+
2977
3008
  declare class PaywallRepository {
2978
3009
  static instance: PaywallRepository;
2979
3010
  fetchPaywalls(): Promise<IPaywall[]>;
2980
3011
  private getAnonymousPaywalls;
2981
3012
  private getPaywalls;
2982
- fetchPaywallByUrl(url: string): Promise<IPaywall>;
2983
- fetchPaywallsByUrls(urls: string[]): Promise<IPaywall[]>;
3013
+ fetchPaywallByUrl(url: string): Promise<CdnFetchResult<IPaywall>>;
3014
+ fetchPaywallsByUrls(urls: string[]): Promise<{
3015
+ paywalls: IPaywall[];
3016
+ retryCount: number;
3017
+ }>;
2984
3018
  private validatePaywalls;
2985
3019
  private fallbackData;
2986
3020
  }
@@ -3069,5 +3103,5 @@ declare const getBillingPeriodNumber: (billingPeriod: string) => number;
3069
3103
  declare const formattedPrice: (price: number) => number;
3070
3104
  declare function toDouble(num: number): number;
3071
3105
 
3072
- 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_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, DEVELOPMENT, DEVICE_API_TIMEOUT_LIMIT, DEVICE_ID_NOT_SET, DEVICE_ID_REQUIRED, 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, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager, NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager, NamiFlowStepType, NamiPaywallAction, NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiProfileManager, 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, STATUS_BAD_REQUEST, STATUS_CONFLICT, STATUS_INTERNAL_SERVER_ERROR, STATUS_NOT_FOUND, STATUS_SUCCESS, SessionService, SimpleEventTarget, StorageService, UNABLE_TO_UPDATE_CDP_ID, USE_STAGING_API, VALIDATE_PRODUCT_GROUPS, VAR_REGEX, activateEntitlementByPurchase, activeEntitlements, aggregateScreenreaderText, allCampaigns, allPaywalls, applyEntitlementActivation, audienceSplitPosition, bestUrlCampaignMatch, bigintToUuid, checkAnySkuHasPromoOffer, checkAnySkuHasTrialOffer, convertISO8601PeriodToText, convertLocale, convertOfferToPricingPhase, createNamiEntitlements, currentSku, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, 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, registerPlatformAdapters, registerPurchaseAdapter, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateMinSDKVersion };
3106
+ 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_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, 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, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager, NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager, NamiFlowStepType, NamiPaywallAction, NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiProfileManager, 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, STATUS_BAD_REQUEST, STATUS_CONFLICT, STATUS_INTERNAL_SERVER_ERROR, STATUS_NOT_FOUND, STATUS_SUCCESS, SessionService, SimpleEventTarget, StorageService, UNABLE_TO_UPDATE_CDP_ID, USE_STAGING_API, VALIDATE_PRODUCT_GROUPS, VAR_REGEX, activateEntitlementByPurchase, activeEntitlements, aggregateScreenreaderText, allCampaigns, allPaywalls, applyEntitlementActivation, audienceSplitPosition, bestUrlCampaignMatch, bigintToUuid, checkAnySkuHasPromoOffer, checkAnySkuHasTrialOffer, convertISO8601PeriodToText, convertLocale, convertOfferToPricingPhase, createNamiEntitlements, currentSku, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, 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, registerPlatformAdapters, registerPurchaseAdapter, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateMinSDKVersion };
3073
3107
  export type { AlignmentType, AmazonProduct, ApiResponse, AppleProduct, AvailableCampaignsResponseHandler, BorderLocationType, BorderSideType, Callback$1 as Callback, CloseHandler, CustomerJourneyState, DeepLinkUrlHandler, Device, DevicePayload, DeviceProfile, DirectionType, ExtendedPlatformInfo, FlexDirectionObject, FlowNavigationOptions, FontCollection, FontDetails, FormFactor, GoogleProduct, IConfig, IDeviceAdapter, IEntitlements$1 as IEntitlements, IPaywall, IPlatformAdapters, IProductsWithComponents, IPurchaseAdapter, ISkuMenu, IStorageAdapter, IUIAdapter, Impression, InitialConfig, InitialConfigCompressed, InitiateStateGroup, LoginResponse, NamiAnimation, NamiAnimationObjectSpec, NamiAnimationSpec, NamiAnonymousCampaign, NamiAppSuppliedVideoDetails, NamiCampaign, NamiCampaignSegment, NamiConfiguration, NamiConfigurationState, NamiEntitlement$1 as NamiEntitlement, NamiFlowAction, NamiFlowAnimation, NamiFlowCampaign, NamiFlowDTO, NamiFlowEventHandler, NamiFlowHandoffStepHandler, NamiFlowObjectDTO, NamiFlowOn, NamiFlowStep, NamiFlowTransition, NamiFlowTransitionDirection, NamiFlowWithObject, NamiInitialConfig, NamiLanguageCodes, NamiLogLevel, NamiPaywallActionHandler, NamiPaywallComponentChange, NamiPaywallEvent, NamiPaywallEventVideoMetadata, NamiPaywallLaunchContext, NamiPresentationStyle, NamiProductDetails, NamiProductOffer, NamiProfile, NamiPurchase, NamiPurchaseCompleteResult, NamiPurchaseDetails, NamiPurchasesState, NamiSKU, NamiSKUType, NamiSubscriptionInterval, NamiSubscriptionPeriod, None, NoneSpec, PaywallActionEvent, PaywallHandle, PaywallResultHandler, PaywallSKU, PricingPhase, ProductGroup, Pulse, PulseSpec, PurchaseContext, PurchaseResult, PurchaseValidationRequest, SKU, SKUActionHandler, ScreenInfo, Session, TBaseComponent, TButtonContainer, TCarouselContainer, TCarouselSlide, TCarouselSlidesState, TCollapseContainer, TComponent, TConditionalAttributes, TConditionalComponent, TContainer, TContainerPosition, TCountdownTimerTextComponent, TDevice, TDisabledButton, TField, TFieldSettings, TFlexProductContainer, THeaderFooter, TImageComponent, TInitialState, TMediaTypes, TOffer, TPages, TPaywallContext, TPaywallLaunchContext, TPaywallMedia, TPaywallTemplate, TPlayPauseButton, TProductContainer, TProductGroup, TProgressBarComponent, TProgressIndicatorComponent, TQRCodeComponent, TRadioButton, TRepeatingGrid, TResponsiveGrid, TSegmentPicker, TSegmentPickerItem, TSemverObj, TSpacerComponent, TStack, TSvgImageComponent, TSymbolComponent, TTestObject, TTextComponent, TTextLikeComponent, TTextListComponent, TToggleButtonComponent, TToggleSwitch, TVariablePattern, TVideoComponent, TVolumeButton, TimerState, TransactionRequest, UserAction, UserActionParameters, Wave, WaveSpec };
package/dist/index.mjs CHANGED
@@ -96,7 +96,7 @@ const {
96
96
  // version — stamped by scripts/version.sh
97
97
  NAMI_SDK_VERSION = "3.4.0",
98
98
  // full package version including dev suffix — stamped by scripts/version.sh
99
- NAMI_SDK_PACKAGE_VERSION = "3.4.0-dev.202605190129",
99
+ NAMI_SDK_PACKAGE_VERSION = "3.4.0-dev.202605191719",
100
100
  // environments
101
101
  PRODUCTION = "production", DEVELOPMENT = "development",
102
102
  // error messages
@@ -6847,7 +6847,7 @@ async function withRetry(url, options, timeout = API_TIMEOUT_LIMIT, retries = AP
6847
6847
  if (logTraffic && options?.body) {
6848
6848
  logger.debug(`[HTTP] Request body: ${options.body}`);
6849
6849
  }
6850
- const response = await timeoutRequest(url, options, timeout);
6850
+ const response = await timeoutRequest$1(url, options, timeout);
6851
6851
  if (!response.ok) {
6852
6852
  if (logTraffic) {
6853
6853
  const errorBody = await response.clone().text();
@@ -6877,7 +6877,7 @@ async function withRetry(url, options, timeout = API_TIMEOUT_LIMIT, retries = AP
6877
6877
  const response = await fetchWithRetry();
6878
6878
  return response.json();
6879
6879
  }
6880
- async function timeoutRequest(url, options = {}, timeout) {
6880
+ async function timeoutRequest$1(url, options = {}, timeout) {
6881
6881
  const controller = new AbortController();
6882
6882
  const timeoutId = setTimeout(() => controller.abort(), timeout);
6883
6883
  options["signal"] = controller.signal;
@@ -8307,6 +8307,161 @@ class ProductRepository {
8307
8307
  }
8308
8308
  ProductRepository.instance = new ProductRepository();
8309
8309
 
8310
+ const DEFAULTS = {
8311
+ maxAttempts: 3,
8312
+ baseDelayMs: 200,
8313
+ maxDelayMs: 2000,
8314
+ budgetMs: 5000,
8315
+ };
8316
+ const TRANSIENT_STATUSES = new Set([408, 425, 429]);
8317
+ const TRANSIENT_ERROR_CODES = new Set(["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT"]);
8318
+ class NonRetryableHttpError extends Error {
8319
+ constructor(status, url) {
8320
+ super(`CDN fetch failed with non-retryable status ${status} for ${url}`);
8321
+ this.name = "NonRetryableHttpError";
8322
+ this.status = status;
8323
+ this.url = url;
8324
+ }
8325
+ }
8326
+ class RetryExhaustedError extends Error {
8327
+ constructor(url, attempts, lastStatus) {
8328
+ super(`CDN fetch exhausted ${attempts} attempts for ${url}${lastStatus !== undefined ? ` (last status ${lastStatus})` : ""}`);
8329
+ this.name = "RetryExhaustedError";
8330
+ this.url = url;
8331
+ this.attempts = attempts;
8332
+ this.lastStatus = lastStatus;
8333
+ }
8334
+ }
8335
+ /**
8336
+ * Classify a failure as transient (worth retrying) or not.
8337
+ *
8338
+ * Transient:
8339
+ * - HTTP 408 / 425 / 429 / any 5xx (504 Gateway Timeout from the CDN edge
8340
+ * is the motivating production case)
8341
+ * - AbortError from the request timeout
8342
+ * - Any TypeError thrown by fetch — covers Safari's "Load failed",
8343
+ * Chrome's "Failed to fetch", Firefox's "NetworkError ...", and
8344
+ * CORS-blocked responses from the CDN edge (a 504 without
8345
+ * Access-Control-Allow-Origin surfaces as a TypeError, not a Response)
8346
+ * - ECONNRESET/ECONNREFUSED/ENOTFOUND/ETIMEDOUT in Node test envs
8347
+ *
8348
+ * Non-transient: every other status (including 404 — a missing paywall is
8349
+ * genuinely gone; retrying just delays the inevitable failure).
8350
+ */
8351
+ function isTransientFailure(err, status) {
8352
+ if (typeof status === "number") {
8353
+ return TRANSIENT_STATUSES.has(status) || status >= 500;
8354
+ }
8355
+ if (err && typeof err === "object") {
8356
+ const e = err;
8357
+ if (e.name === "AbortError")
8358
+ return true;
8359
+ if (e.code && TRANSIENT_ERROR_CODES.has(e.code))
8360
+ return true;
8361
+ // fetch() throws TypeError for network-layer failures (DNS/TCP/TLS,
8362
+ // CORS-blocked, edge dropped). HTTP status failures arrive as fulfilled
8363
+ // responses with !response.ok — never as a thrown TypeError. So any
8364
+ // TypeError reaching this catch is by definition transient. Don't filter
8365
+ // on message text: Safari ("Load failed"), Chrome ("Failed to fetch"),
8366
+ // and Firefox ("NetworkError ...") all differ, and any future variation
8367
+ // would silently be classified non-transient.
8368
+ if (err instanceof TypeError)
8369
+ return true;
8370
+ }
8371
+ return false;
8372
+ }
8373
+ async function timeoutRequest(url, timeout) {
8374
+ const controller = new AbortController();
8375
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
8376
+ try {
8377
+ return await fetch(url, { signal: controller.signal });
8378
+ }
8379
+ finally {
8380
+ clearTimeout(timeoutId);
8381
+ }
8382
+ }
8383
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8384
+ /**
8385
+ * Fetch a single CDN-hosted paywall with bounded retries on transient
8386
+ * failures. Exponential backoff (baseDelayMs * 2^n, capped at maxDelayMs)
8387
+ * with ±50% jitter; the whole loop is clamped to budgetMs wall-clock so
8388
+ * the parallel-fetch window can't balloon when one edge is degraded.
8389
+ *
8390
+ * Throws NonRetryableHttpError for hard 4xx (including 404), or
8391
+ * RetryExhaustedError when all attempts and/or the budget are spent.
8392
+ */
8393
+ async function fetchWithCdnRetry(url, opts = {}) {
8394
+ const maxAttempts = opts.maxAttempts ?? DEFAULTS.maxAttempts;
8395
+ const baseDelayMs = opts.baseDelayMs ?? DEFAULTS.baseDelayMs;
8396
+ const maxDelayMs = opts.maxDelayMs ?? DEFAULTS.maxDelayMs;
8397
+ const budgetMs = opts.budgetMs ?? DEFAULTS.budgetMs;
8398
+ const timeoutMs = opts.timeoutMs ?? API_TIMEOUT_LIMIT;
8399
+ const randomFn = opts.randomFn ?? Math.random;
8400
+ const sleepFn = opts.sleepFn ?? defaultSleep;
8401
+ const logRequests = shouldLogHTTPRequests();
8402
+ const logTraffic = shouldLogHTTPTraffic();
8403
+ const start = Date.now();
8404
+ let attempt = 0;
8405
+ let lastStatus;
8406
+ let lastError;
8407
+ while (attempt < maxAttempts) {
8408
+ if (attempt > 0) {
8409
+ const elapsed = Date.now() - start;
8410
+ if (elapsed >= budgetMs)
8411
+ break;
8412
+ const baseDelay = Math.min(baseDelayMs * Math.pow(2, attempt - 1), maxDelayMs);
8413
+ // ±50% jitter: random in [0,1) → factor in [-0.5, 0.5)
8414
+ const jitter = baseDelay * (randomFn() - 0.5);
8415
+ const remaining = Math.max(0, budgetMs - elapsed);
8416
+ const sleepFor = Math.min(Math.max(0, baseDelay + jitter), remaining);
8417
+ await sleepFn(sleepFor);
8418
+ // If the sleep consumed the rest of the budget, don't fire another fetch.
8419
+ if (Date.now() - start >= budgetMs)
8420
+ break;
8421
+ }
8422
+ try {
8423
+ if (logRequests || logTraffic) {
8424
+ logger.debug(`[HTTP] GET ${url} (cdn attempt ${attempt + 1}/${maxAttempts})`);
8425
+ }
8426
+ const response = await timeoutRequest(url, timeoutMs);
8427
+ if (!response.ok) {
8428
+ if (logTraffic) {
8429
+ const errorBody = await response.clone().text();
8430
+ logger.debug(`[HTTP] Response ${response.status}: ${errorBody}`);
8431
+ }
8432
+ lastStatus = response.status;
8433
+ if (isTransientFailure(undefined, response.status)) {
8434
+ attempt++;
8435
+ continue;
8436
+ }
8437
+ throw new NonRetryableHttpError(response.status, url);
8438
+ }
8439
+ if (logTraffic) {
8440
+ const responseBody = await response.clone().text();
8441
+ logger.debug(`[HTTP] Response ${response.status}: ${responseBody}`);
8442
+ }
8443
+ const data = (await response.json());
8444
+ return { data, retryCount: attempt };
8445
+ }
8446
+ catch (err) {
8447
+ if (err instanceof NonRetryableHttpError)
8448
+ throw err;
8449
+ if (isTransientFailure(err)) {
8450
+ lastError = err;
8451
+ attempt++;
8452
+ continue;
8453
+ }
8454
+ throw err;
8455
+ }
8456
+ }
8457
+ // If we exited the loop without success but never saw a transient response,
8458
+ // fall back to surfacing the last raw error. Otherwise raise the canonical
8459
+ // exhausted-attempts signal with the last seen status for telemetry.
8460
+ if (lastStatus === undefined && lastError !== undefined)
8461
+ throw lastError;
8462
+ throw new RetryExhaustedError(url, attempt, lastStatus);
8463
+ }
8464
+
8310
8465
  class PaywallRepository {
8311
8466
  async fetchPaywalls() {
8312
8467
  const authDevice = storageService.getDevice();
@@ -8357,17 +8512,22 @@ class PaywallRepository {
8357
8512
  return data?.results || [];
8358
8513
  }
8359
8514
  async fetchPaywallByUrl(url) {
8360
- return withRetry(url);
8515
+ return fetchWithCdnRetry(url);
8361
8516
  }
8362
8517
  async fetchPaywallsByUrls(urls) {
8363
8518
  const uniqueUrls = [...new Set(urls)];
8364
8519
  const results = await Promise.allSettled(uniqueUrls.map((url) => this.fetchPaywallByUrl(url)));
8365
8520
  const paywalls = [];
8521
+ let retryCount = 0;
8366
8522
  for (const result of results) {
8367
8523
  if (result.status === "fulfilled") {
8368
- paywalls.push(result.value);
8524
+ paywalls.push(result.value.data);
8525
+ retryCount += result.value.retryCount;
8369
8526
  }
8370
8527
  else {
8528
+ // Note: an exhausted-failure path contributed retries we can't see
8529
+ // from the rejected promise, so retryCount under-reports failures.
8530
+ // Acceptable for the telemetry signal; revisit if full accounting matters.
8371
8531
  logger.error(`Failed to fetch individual paywall: ${result.reason}`);
8372
8532
  }
8373
8533
  }
@@ -8376,9 +8536,9 @@ class PaywallRepository {
8376
8536
  if (valid.length > 0) {
8377
8537
  storageService.setPaywalls(API_PAYWALLS, valid);
8378
8538
  }
8379
- return valid;
8539
+ return { paywalls: valid, retryCount };
8380
8540
  }
8381
- return paywalls;
8541
+ return { paywalls, retryCount };
8382
8542
  }
8383
8543
  validatePaywalls(paywalls) {
8384
8544
  return paywalls.filter((paywall) => {
@@ -11854,10 +12014,13 @@ class NamiRefs {
11854
12014
  const useIndividual = !campaignRepo.useLegacyPaywallFetch &&
11855
12015
  CampaignRuleRepository.hasPaywallUrls(rawCampaigns);
11856
12016
  let paywalls;
12017
+ let retryCount = 0;
11857
12018
  const paywallsStartTime = Date.now();
11858
12019
  if (useIndividual) {
11859
12020
  const urls = CampaignRuleRepository.extractPaywallUrls(rawCampaigns);
11860
- paywalls = await paywallRepo.fetchPaywallsByUrls(urls);
12021
+ const result = await paywallRepo.fetchPaywallsByUrls(urls);
12022
+ paywalls = result.paywalls;
12023
+ retryCount = result.retryCount;
11861
12024
  }
11862
12025
  else {
11863
12026
  paywalls = await paywallRepo.fetchPaywalls();
@@ -11866,7 +12029,8 @@ class NamiRefs {
11866
12029
  const totalDuration = Date.now() - startTime;
11867
12030
  logger.info(`Paywall fetch telemetry: strategy=${useIndividual ? "individual" : "bulk"}, ` +
11868
12031
  `count=${paywalls.length}, total=${totalDuration}ms, ` +
11869
- `campaigns=${campaignRulesDuration}ms, paywalls=${paywallsDuration}ms`);
12032
+ `campaigns=${campaignRulesDuration}ms, paywalls=${paywallsDuration}ms` +
12033
+ `${useIndividual ? `, retry_count=${retryCount}` : ""}`);
11870
12034
  return campaignRepo.finalizeCampaignRules(rawCampaigns, paywalls);
11871
12035
  }
11872
12036
  reRenderPaywall() {
@@ -12829,21 +12993,46 @@ class NamiProfileManager {
12829
12993
  }
12830
12994
  PaywallState.setIsLoggedIn(!!this.externalId);
12831
12995
  }
12832
- async login(externalId) {
12996
+ /**
12997
+ * Update local profile state for login: persist the external id and flip the
12998
+ * logged-in flag in PaywallState. No network I/O — pair with `loginRemote`
12999
+ * to perform the actual API call. Used by the async login/logout fast path.
13000
+ */
13001
+ loginLocal(externalId) {
13002
+ this.setExternalId(externalId).save();
13003
+ PaywallState.setIsLoggedIn(true);
13004
+ }
13005
+ /**
13006
+ * Perform the login network call only. If the server returns a different
13007
+ * `external_id` than the locally stored value, reconcile it into local state.
13008
+ */
13009
+ async loginRemote(externalId) {
12833
13010
  const loginData = await NamiAPI.instance.login(externalId);
12834
- if (loginData.external_id) {
12835
- PaywallState.setIsLoggedIn(true);
13011
+ if (loginData.external_id && loginData.external_id !== this.externalId) {
13012
+ this.setExternalId(loginData.external_id).save();
12836
13013
  }
12837
- this
12838
- .setExternalId(loginData.external_id)
12839
- .save();
12840
13014
  }
12841
- async logout() {
12842
- await NamiAPI.instance.logout();
13015
+ /**
13016
+ * Update local profile state for logout: clear the external id, persist,
13017
+ * and flip the logged-in flag in PaywallState. Used by the async fast path.
13018
+ */
13019
+ logoutLocal() {
13020
+ this.setExternalId(undefined).save();
12843
13021
  PaywallState.setIsLoggedIn(false);
12844
- this
12845
- .setExternalId(undefined)
12846
- .save();
13022
+ }
13023
+ /**
13024
+ * Perform the logout network call only.
13025
+ */
13026
+ async logoutRemote() {
13027
+ await NamiAPI.instance.logout();
13028
+ }
13029
+ async login(externalId) {
13030
+ this.loginLocal(externalId);
13031
+ await this.loginRemote(externalId);
13032
+ }
13033
+ async logout() {
13034
+ this.logoutLocal();
13035
+ await this.logoutRemote();
12847
13036
  }
12848
13037
  }
12849
13038
  NamiProfileManager.instance = new NamiProfileManager();
@@ -13163,6 +13352,21 @@ class NamiFlowManager {
13163
13352
  }
13164
13353
  }
13165
13354
 
13355
+ const SHOULD_SHOW_LOADING_INDICATOR = false;
13356
+ const DISABLE_ASYNC_LOGIN_LOGOUT = "disableAsyncLoginLogout";
13357
+ /**
13358
+ * Returns true when `disableAsyncLoginLogout` appears in the active `namiCommands` list,
13359
+ * meaning login/logout should await the underlying API call before notifying handlers.
13360
+ *
13361
+ * Default behavior (flag absent) is the async/fast path: update local state and notify
13362
+ * handlers immediately while dispatching the API call as fire-and-forget.
13363
+ *
13364
+ * Internal feature flag — opt-out via `Nami.configure({ namiCommands: [...] })`.
13365
+ */
13366
+ const isAsyncLoginLogoutDisabled = () => {
13367
+ return storageService.getNamiConfig()?.namiCommands?.includes(DISABLE_ASYNC_LOGIN_LOGOUT) ?? false;
13368
+ };
13369
+
13166
13370
  /**
13167
13371
  * @class NamiCustomerManager
13168
13372
  * Provides methods for managing customer-related functionality.
@@ -13224,6 +13428,30 @@ class NamiCustomerManager {
13224
13428
  this.invokeStateHandler(AccountStateAction.LOGIN, false, error);
13225
13429
  throw error;
13226
13430
  }
13431
+ if (!isAsyncLoginLogoutDisabled()) {
13432
+ // Default sync/fast path: update local state, notify handlers, and fire
13433
+ // lifecycle immediately. Dispatch the underlying API call and entitlement
13434
+ // refresh fire-and-forget — failures are logged but do not surface.
13435
+ NamiProfileManager.instance.loginLocal(externalId);
13436
+ this.invokeStateHandler(AccountStateAction.LOGIN, true);
13437
+ if (NamiFlowManager.instance.flowOpen) {
13438
+ const currentFlow = NamiFlowManager.instance.currentFlow;
13439
+ const currentStep = currentFlow?.currentFlowStep;
13440
+ if (currentStep) {
13441
+ if (Nami.instance.maxLogging) {
13442
+ logger.debug(`[NamiCustomerManager] async login success — triggering __login_success__ on step ${currentStep.id}`);
13443
+ }
13444
+ currentFlow.executeLifecycle(currentStep, NamiReservedActions.LOGIN_SUCCESS);
13445
+ }
13446
+ }
13447
+ void NamiProfileManager.instance.loginRemote(externalId).catch((err) => {
13448
+ logger.error('[NamiCustomerManager] async login API failed', err);
13449
+ });
13450
+ void EntitlementRepository.instance.fetchActiveEntitlements().catch((err) => {
13451
+ logger.error('[NamiCustomerManager] async login entitlement refresh failed', err);
13452
+ });
13453
+ return;
13454
+ }
13227
13455
  try {
13228
13456
  await NamiProfileManager.instance.login(externalId);
13229
13457
  await EntitlementRepository.instance.fetchActiveEntitlements();
@@ -13269,6 +13497,30 @@ class NamiCustomerManager {
13269
13497
  this.invokeStateHandler(AccountStateAction.LOGOUT, false, error);
13270
13498
  throw error;
13271
13499
  }
13500
+ if (!isAsyncLoginLogoutDisabled()) {
13501
+ // Default sync/fast path: clear local state, notify handlers, and fire
13502
+ // lifecycle immediately. Dispatch the underlying API call and entitlement
13503
+ // refresh fire-and-forget — failures are logged but do not surface.
13504
+ NamiProfileManager.instance.logoutLocal();
13505
+ this.invokeStateHandler(AccountStateAction.LOGOUT, true);
13506
+ if (NamiFlowManager.instance.flowOpen) {
13507
+ const currentFlow = NamiFlowManager.instance.currentFlow;
13508
+ const currentStep = currentFlow?.currentFlowStep;
13509
+ if (currentStep) {
13510
+ if (Nami.instance.maxLogging) {
13511
+ logger.debug(`[NamiCustomerManager] async logout success — triggering __logout_success__ on step ${currentStep.id}`);
13512
+ }
13513
+ currentFlow.executeLifecycle(currentStep, NamiReservedActions.LOGOUT_SUCCESS);
13514
+ }
13515
+ }
13516
+ void NamiProfileManager.instance.logoutRemote().catch((err) => {
13517
+ logger.error('[NamiCustomerManager] async logout API failed', err);
13518
+ });
13519
+ void EntitlementRepository.instance.fetchActiveEntitlements().catch((err) => {
13520
+ logger.error('[NamiCustomerManager] async logout entitlement refresh failed', err);
13521
+ });
13522
+ return;
13523
+ }
13272
13524
  try {
13273
13525
  await NamiProfileManager.instance.logout();
13274
13526
  await EntitlementRepository.instance.fetchActiveEntitlements();
@@ -14703,8 +14955,6 @@ var CampaignRuleConversionEventType;
14703
14955
  CampaignRuleConversionEventType["IN_APP"] = "in_app";
14704
14956
  })(CampaignRuleConversionEventType || (CampaignRuleConversionEventType = {}));
14705
14957
 
14706
- const SHOULD_SHOW_LOADING_INDICATOR = false;
14707
-
14708
14958
  function parseToSemver(versionString) {
14709
14959
  const [semVer, major, minor, patch, prerelease, buildmetadata] = versionString.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/) ?? [];
14710
14960
  return {
@@ -64164,4 +64414,4 @@ function namiBuySKU(skuRefId) {
64164
64414
  return result;
64165
64415
  }
64166
64416
 
64167
- 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_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, DEVELOPMENT, DEVICE_API_TIMEOUT_LIMIT, DEVICE_ID_NOT_SET, DEVICE_ID_REQUIRED, 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, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager, NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager, NamiFlowStepType, NamiPaywallAction, NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiProfileManager, 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, STATUS_BAD_REQUEST, STATUS_CONFLICT, STATUS_INTERNAL_SERVER_ERROR, STATUS_NOT_FOUND, STATUS_SUCCESS, SessionService, SimpleEventTarget, StorageService, UNABLE_TO_UPDATE_CDP_ID, USE_STAGING_API, VALIDATE_PRODUCT_GROUPS, VAR_REGEX, activateEntitlementByPurchase, activeEntitlements, aggregateScreenreaderText, allCampaigns, allPaywalls, applyEntitlementActivation, audienceSplitPosition, bestUrlCampaignMatch, bigintToUuid, checkAnySkuHasPromoOffer, checkAnySkuHasTrialOffer, convertISO8601PeriodToText, convertLocale, convertOfferToPricingPhase, createNamiEntitlements, currentSku, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, 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, registerPlatformAdapters, registerPurchaseAdapter, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateMinSDKVersion };
64417
+ 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_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, 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, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager, NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager, NamiFlowStepType, NamiPaywallAction, NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiProfileManager, 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, STATUS_BAD_REQUEST, STATUS_CONFLICT, STATUS_INTERNAL_SERVER_ERROR, STATUS_NOT_FOUND, STATUS_SUCCESS, SessionService, SimpleEventTarget, StorageService, UNABLE_TO_UPDATE_CDP_ID, USE_STAGING_API, VALIDATE_PRODUCT_GROUPS, VAR_REGEX, activateEntitlementByPurchase, activeEntitlements, aggregateScreenreaderText, allCampaigns, allPaywalls, applyEntitlementActivation, audienceSplitPosition, bestUrlCampaignMatch, bigintToUuid, checkAnySkuHasPromoOffer, checkAnySkuHasTrialOffer, convertISO8601PeriodToText, convertLocale, convertOfferToPricingPhase, createNamiEntitlements, currentSku, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, 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, registerPlatformAdapters, registerPurchaseAdapter, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateMinSDKVersion };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namiml/sdk-core",
3
- "version": "3.4.0-dev.202605190129",
3
+ "version": "3.4.0-dev.202605191719",
4
4
  "description": "Platform-agnostic core for the Nami SDK — business logic, API, types, and state management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",