@namiml/sdk-core 3.4.12-dev.202609251500 → 3.4.12-dev.202609252349

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
@@ -106,7 +106,7 @@ const {
106
106
  // version — stamped by scripts/version.sh
107
107
  NAMI_SDK_VERSION: exports.NAMI_SDK_VERSION = "3.4.12",
108
108
  // full package version including dev suffix — stamped by scripts/version.sh
109
- NAMI_SDK_PACKAGE_VERSION: exports.NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.202609251500",
109
+ NAMI_SDK_PACKAGE_VERSION: exports.NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.202609252349",
110
110
  // environments
111
111
  PRODUCTION: exports.PRODUCTION = "production", DEVELOPMENT: exports.DEVELOPMENT = "development",
112
112
  // error messages
@@ -116,7 +116,19 @@ AUTH_DEVICE: exports.AUTH_DEVICE = "nami_auth_device", NAMI_CONFIGURATION: expor
116
116
  // API settings
117
117
  API_VERSION: exports.API_VERSION = "v3", BASE_URL_PATH: exports.BASE_URL_PATH = `sdk/${exports.API_VERSION}/platform`, BASE_URL: exports.BASE_URL = "https://app.namiml.com", BASE_STAGING_URL: exports.BASE_STAGING_URL = "https://app-staging.namiml.com", CUSTOM_HOST_PREFIX: exports.CUSTOM_HOST_PREFIX = "namiAPIHost=", USE_STAGING_API: exports.USE_STAGING_API = "useStagingAPI",
118
118
  // // extended client info
119
- EXTENDED_CLIENT_INFO_PREFIX: exports.EXTENDED_CLIENT_INFO_PREFIX = "extendedClientInfo", EXTENDED_CLIENT_INFO_DELIMITER: exports.EXTENDED_CLIENT_INFO_DELIMITER = ":", VALIDATE_PRODUCT_GROUPS: exports.VALIDATE_PRODUCT_GROUPS = "validateProductGroups", LOG_HTTP_REQUESTS: exports.LOG_HTTP_REQUESTS = "logHTTPRequests", LOG_HTTP_TRAFFIC: exports.LOG_HTTP_TRAFFIC = "logHTTPTraffic", API_TRAFFIC_OBSERVER: exports.API_TRAFFIC_OBSERVER = "apiTrafficObserver", STARTUP_TELEMETRY: exports.STARTUP_TELEMETRY = "startupTelemetry", EXTENDED_PLATFORM: exports.EXTENDED_PLATFORM = "extended-platform", EXTENDED_PLATFORM_VERSION: exports.EXTENDED_PLATFORM_VERSION = "extended-platform-version", API_MAX_CALLS_LIMIT: exports.API_MAX_CALLS_LIMIT = 2, API_RETRY_DELAY_SEC: exports.API_RETRY_DELAY_SEC = 2, API_TIMEOUT_LIMIT: exports.API_TIMEOUT_LIMIT = 20000, DEVICE_API_TIMEOUT_LIMIT: exports.DEVICE_API_TIMEOUT_LIMIT = 2000,
119
+ EXTENDED_CLIENT_INFO_PREFIX: exports.EXTENDED_CLIENT_INFO_PREFIX = "extendedClientInfo", EXTENDED_CLIENT_INFO_DELIMITER: exports.EXTENDED_CLIENT_INFO_DELIMITER = ":", VALIDATE_PRODUCT_GROUPS: exports.VALIDATE_PRODUCT_GROUPS = "validateProductGroups", LOG_HTTP_REQUESTS: exports.LOG_HTTP_REQUESTS = "logHTTPRequests", LOG_HTTP_TRAFFIC: exports.LOG_HTTP_TRAFFIC = "logHTTPTraffic", API_TRAFFIC_OBSERVER: exports.API_TRAFFIC_OBSERVER = "apiTrafficObserver", STARTUP_TELEMETRY: exports.STARTUP_TELEMETRY = "startupTelemetry",
120
+ // Test hook: every platform-config fetch attempt fails before the request leaves.
121
+ // Same namiCommand as Apple's and Android's, so the shared first-run flows drive it.
122
+ SIMULATE_CONFIG_FETCH_FAILURE: exports.SIMULATE_CONFIG_FETCH_FAILURE = "simulateConfigFetchFailure", EXTENDED_PLATFORM: exports.EXTENDED_PLATFORM = "extended-platform", EXTENDED_PLATFORM_VERSION: exports.EXTENDED_PLATFORM_VERSION = "extended-platform-version", API_MAX_CALLS_LIMIT: exports.API_MAX_CALLS_LIMIT = 2, API_RETRY_DELAY_SEC: exports.API_RETRY_DELAY_SEC = 2, API_TIMEOUT_LIMIT: exports.API_TIMEOUT_LIMIT = 20000, DEVICE_API_TIMEOUT_LIMIT: exports.DEVICE_API_TIMEOUT_LIMIT = 2000,
123
+ // Platform-config attempts when no initial config was supplied (Android's
124
+ // CONFIG_MAX_ATTEMPTS; Apple's retry(maxAttempts: 3)).
125
+ CONFIG_MAX_ATTEMPTS: exports.CONFIG_MAX_ATTEMPTS = 3,
126
+ // Per-attempt timeout for GET config/. Android's config client allows 5 s to connect plus 5 s
127
+ // to read (Apple allows 30 s); three attempts keep a total outage near 30 s.
128
+ CONFIG_API_TIMEOUT_LIMIT: exports.CONFIG_API_TIMEOUT_LIMIT = 10000,
129
+ // A foreground within this long of the last launch/resume run is skipped — Apple's
130
+ // NamiStartupCoordinator.shouldRunResume window.
131
+ RESUME_THROTTLE_MS: exports.RESUME_THROTTLE_MS = 30000,
120
132
  // status codes
121
133
  STATUS_SUCCESS: exports.STATUS_SUCCESS = 200, STATUS_BAD_REQUEST: exports.STATUS_BAD_REQUEST = 400, STATUS_NOT_FOUND: exports.STATUS_NOT_FOUND = 404, STATUS_CONFLICT: exports.STATUS_CONFLICT = 409, STATUS_INTERNAL_SERVER_ERROR: exports.STATUS_INTERNAL_SERVER_ERROR = 500,
122
134
  // configuration states
@@ -58033,27 +58045,125 @@ class PaywallRepository {
58033
58045
  PaywallRepository.instance = new PaywallRepository();
58034
58046
 
58035
58047
  class ConfigRepository {
58048
+ /**
58049
+ * One `GET config/` attempt. Persists the config on success and throws on any failure —
58050
+ * a non-2xx or a transport error alike. `withRetry`'s own retries are turned off, so the
58051
+ * attempt count belongs to {@link fetchConfigForStartup} alone.
58052
+ */
58036
58053
  async fetchConfig() {
58037
- let data;
58038
- try {
58039
- data = await NamiAPI.instance.fetchAPI("config/");
58040
- ;
58041
- }
58042
- catch (error) {
58043
- data = storageService.getAppConfig(exports.API_CONFIG)
58044
- || storageService.getAppConfig(exports.INITIAL_APP_CONFIG);
58045
- if (!data) {
58046
- logger.error(handleErrors(error.status, "config/"));
58047
- throw error;
58048
- }
58054
+ // Thrown from *inside* the attempt on purpose, like Apple's and Android's: the ladder then
58055
+ // treats it exactly as a real 500, which is the whole point of the mock.
58056
+ if (storageService.getNamiConfig()?.namiCommands?.includes(exports.SIMULATE_CONFIG_FETCH_FAILURE)) {
58057
+ throw new RetryLimitExceededError(exports.STATUS_INTERNAL_SERVER_ERROR, exports.SIMULATE_CONFIG_FETCH_FAILURE);
58049
58058
  }
58050
- if (data) {
58059
+ const platformId = storageService.getNamiConfig()?.appPlatformID;
58060
+ const data = await NamiAPI.instance.fetchAPI("config/", exports.CONFIG_API_TIMEOUT_LIMIT, 0);
58061
+ // A late answer (the startup await gave up on it) must not overwrite another platform's
58062
+ // config after a reset or a platform-switching reconfigure.
58063
+ if (storageService.getNamiConfig()?.appPlatformID === platformId) {
58051
58064
  storageService.setAppConfig(exports.API_CONFIG, data);
58052
58065
  }
58066
+ else {
58067
+ logger.debug("Discarding an app config response for a platform that is no longer configured");
58068
+ }
58053
58069
  return data;
58054
58070
  }
58071
+ /**
58072
+ * The startup platform-config step, converged on Apple's `NamiPlatformConfigService.fetch` and
58073
+ * Android's `fetchAppConfigOnce` (NAM-4470, NAM-4589):
58074
+ *
58075
+ * | case | outcome |
58076
+ * |---|---|
58077
+ * | an initial config was supplied | NOT awaited: one attempt runs in the background and stores the config when it lands; usable |
58078
+ * | no initial config, success, capabilities include `sdk` | usable |
58079
+ * | no initial config, success, capabilities lack `sdk` | not usable |
58080
+ * | 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`) |
58081
+ *
58082
+ * The two fallbacks are consulted at two different points, deliberately. An initial config
58083
+ * decides the case *before* any request: startup does not wait on the network at all, since the
58084
+ * initial config is what makes the SDK ready without it (Apple: "Initial config is available.
58085
+ * Skipping retry."). The config cached by a previous launch is consulted only *after* the
58086
+ * retries run out. A stored `INITIAL_APP_CONFIG` is not that cache.
58087
+ *
58088
+ * One check does apply with an initial config, because it costs no wait: a cached config that
58089
+ * withholds `sdk` still stops startup, as Apple's cache load re-applies `updateSdkFlags`.
58090
+ * Without it a kill switch could never reach an app that ships an initial config — the
58091
+ * background fetch lands after startup has already run, so it takes effect from the next launch.
58092
+ *
58093
+ * A `404` is treated as any other 4xx, matching Apple's effective behaviour (its dedicated 404
58094
+ * branch is unreachable, NAM-4471) and Android's.
58095
+ *
58096
+ * @param hasInitialConfig whether the host passed an `initialConfig` that decoded to content.
58097
+ */
58098
+ async fetchConfigForStartup(hasInitialConfig) {
58099
+ if (hasInitialConfig)
58100
+ return this.startupOnInitialConfig();
58101
+ const maxAttempts = exports.CONFIG_MAX_ATTEMPTS;
58102
+ let lastError;
58103
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
58104
+ if (attempt > 1) {
58105
+ logger.debug(`Retrying app config fetch (attempt ${attempt} of ${maxAttempts})`);
58106
+ }
58107
+ try {
58108
+ return outcomeOf(await this.fetchConfig());
58109
+ }
58110
+ catch (error) {
58111
+ lastError = error;
58112
+ }
58113
+ }
58114
+ const cached = storageService.getAppConfig(exports.API_CONFIG);
58115
+ if (cached) {
58116
+ // A cached kill switch still holds: Apple re-applies `updateSdkFlags` when it loads the
58117
+ // cached config, so one that withheld `sdk` keeps the SDK disabled through an outage.
58118
+ if (!grantsSdk(cached)) {
58119
+ return { usable: false, reason: "the cached app config does not grant the 'sdk' capability" };
58120
+ }
58121
+ logger.info(`App config fetch failed after ${maxAttempts} attempts - continuing on the cached config`);
58122
+ return { usable: true };
58123
+ }
58124
+ return {
58125
+ usable: false,
58126
+ reason: `app config fetch failed after ${maxAttempts} attempts (${describeFailure(lastError)}) ` +
58127
+ "and no initial or cached config is available",
58128
+ };
58129
+ }
58130
+ /**
58131
+ * The initial-config case: nothing is awaited. The cached kill switch is a storage read; the
58132
+ * config request is left running and persists its answer (`fetchConfig`) when it lands.
58133
+ */
58134
+ startupOnInitialConfig() {
58135
+ // Read before the request can land, so this launch is judged on what the last one stored.
58136
+ const cached = storageService.getAppConfig(exports.API_CONFIG);
58137
+ // Issued even when the cache holds a kill switch: that is how a LIFTED kill switch reaches
58138
+ // the next launch.
58139
+ this.fetchConfig().then((config) => {
58140
+ if (!grantsSdk(config)) {
58141
+ logger.warn("App config does not grant the 'sdk' capability - it takes effect from the next launch");
58142
+ }
58143
+ }, (error) => {
58144
+ logger.info(`App config fetch failed (${describeFailure(error)}). Initial config is available. Skipping retry.`);
58145
+ });
58146
+ if (cached && !grantsSdk(cached)) {
58147
+ return { usable: false, reason: "the cached app config does not grant the 'sdk' capability" };
58148
+ }
58149
+ return { usable: true };
58150
+ }
58055
58151
  }
58056
58152
  ConfigRepository.instance = new ConfigRepository();
58153
+ function outcomeOf(config) {
58154
+ return grantsSdk(config)
58155
+ ? { usable: true }
58156
+ : { usable: false, reason: "app config does not grant the 'sdk' capability" };
58157
+ }
58158
+ function grantsSdk(config) {
58159
+ return config.capabilities?.includes(exports.Capabilities.SDK) ?? false;
58160
+ }
58161
+ function describeFailure(error) {
58162
+ const status = error?.status;
58163
+ if (typeof status === "number")
58164
+ return `HTTP ${status}`;
58165
+ return error instanceof Error ? error.message : String(error);
58166
+ }
58057
58167
 
58058
58168
  // Global reference to avoid circular imports
58059
58169
  let namiRefsInstance = null;
@@ -60779,6 +60889,7 @@ const initialState = {
60779
60889
  isLoggedIn: false,
60780
60890
  darkMode: false,
60781
60891
  safeAreaTop: 0,
60892
+ safeAreaBottom: 0,
60782
60893
  groups: [],
60783
60894
  selectedProducts: {},
60784
60895
  formStates: {},
@@ -61147,6 +61258,9 @@ class PaywallState extends SimpleEventTarget {
61147
61258
  setSafeAreaTop(safeAreaTop) {
61148
61259
  this.setState({ ...this.state, safeAreaTop });
61149
61260
  }
61261
+ setSafeAreaBottom(safeAreaBottom) {
61262
+ this.setState({ ...this.state, safeAreaBottom });
61263
+ }
61150
61264
  setFullScreenPresentation(fullScreenPresentation) {
61151
61265
  this.setState({ ...this.state, fullScreenPresentation });
61152
61266
  }
@@ -61593,6 +61707,15 @@ class NamiRefs {
61593
61707
  this.inMemoryAnonymousMode = false;
61594
61708
  // Captured before device registration; true only on genuine fresh installs
61595
61709
  this.isFirstSession = false;
61710
+ // Set when the platform-config step leaves no usable config (see isStartupDisabled)
61711
+ this.startupDisabled = false;
61712
+ // Whether this configure() supplied an initial config (the config ladder's early exit)
61713
+ this.hasInitialConfig = false;
61714
+ // Resume bookkeeping: the configuration startup last ran with, when it (or a resume) last
61715
+ // ran, and whether one is running now.
61716
+ this.lastConfig = null;
61717
+ this.lastStartupRunAt = 0;
61718
+ this.startupInFlight = false;
61596
61719
  // Register this instance to avoid circular dependency issues
61597
61720
  setNamiRefsInstance(this);
61598
61721
  }
@@ -61610,13 +61733,101 @@ class NamiRefs {
61610
61733
  getIsFirstSession() {
61611
61734
  return this.isFirstSession;
61612
61735
  }
61736
+ /**
61737
+ * True when the platform-config step found no usable config and startup stopped after it —
61738
+ * the equivalent of Apple's `sdkEnabled = false` and Android's `NamiRefs.startupDisabled`.
61739
+ * Like both, it gates only the startup sequence, nothing else.
61740
+ */
61741
+ isStartupDisabled() {
61742
+ return this.startupDisabled;
61743
+ }
61613
61744
  async init(config) {
61614
61745
  this.isFirstSession = storageService.getDevice() === null && !storageService.getAnonymousMode();
61746
+ // Cleared per configure so a transient failure cannot disable a later run.
61747
+ this.startupDisabled = false;
61748
+ // `Nami.configure` has already normalised a missing or undecodable initialConfig to `{}`,
61749
+ // so "has content" is Apple's `initialConfig != nil`.
61750
+ this.hasInitialConfig = Object.keys(config.initialConfig ?? {}).length > 0;
61615
61751
  this.setInitialValues(config);
61616
- ConfigRepository.instance.fetchConfig();
61752
+ this.lastConfig = config;
61753
+ this.lastStartupRunAt = Date.now();
61754
+ this.startupInFlight = true;
61755
+ try {
61756
+ if (!(await this.fetchPlatformConfig()))
61757
+ return;
61758
+ await this.runStartupAfterConfig(config);
61759
+ }
61760
+ finally {
61761
+ this.startupInFlight = false;
61762
+ }
61763
+ }
61764
+ /**
61765
+ * The app returned to the foreground. Re-reads the platform config, as Apple's `.appResume`
61766
+ * and Android's `onAppComesFromBackground` do, so a config change — the `sdk` kill switch
61767
+ * included — reaches a long-lived tab or app without a cold start.
61768
+ *
61769
+ * - Skipped within {@link RESUME_THROTTLE_MS} of the last launch/resume, and while one is
61770
+ * still running.
61771
+ * - When the launch had **stopped** for want of a config and one is now usable, the rest of
61772
+ * startup runs — the steps an Apple resume re-runs because the launch never executed them.
61773
+ * - Otherwise this is a config refresh only. Apple and Android also re-run device, campaigns,
61774
+ * profile, products and the session start on resume; core does not yet, and has no
61775
+ * session end on background for a second session start to pair with (NAM-4623).
61776
+ */
61777
+ async resume() {
61778
+ if (!this.lastConfig || this.startupInFlight)
61779
+ return;
61780
+ if (Date.now() - this.lastStartupRunAt < exports.RESUME_THROTTLE_MS) {
61781
+ logger.debug("Resuming too quickly to call the service.");
61782
+ return;
61783
+ }
61784
+ const config = this.lastConfig;
61785
+ const wasStopped = this.startupDisabled;
61786
+ this.lastStartupRunAt = Date.now();
61787
+ this.startupInFlight = true;
61788
+ this.startupDisabled = false;
61789
+ try {
61790
+ if (!(await this.fetchPlatformConfig()) || !wasStopped)
61791
+ return;
61792
+ logger.info("Usable app config on resume - completing the startup that stopped at launch");
61793
+ await this.runStartupAfterConfig(config);
61794
+ }
61795
+ catch (error) {
61796
+ logger.warn(`Resume refresh failed: ${error}`);
61797
+ }
61798
+ finally {
61799
+ this.startupInFlight = false;
61800
+ }
61801
+ }
61802
+ /** Everything startup runs once the platform-config step has found a usable config. */
61803
+ async runStartupAfterConfig(config) {
61617
61804
  await this.initAndFetchRequiredData(config);
61618
61805
  SessionService.instance.startSession();
61619
61806
  }
61807
+ /** Forgets the configuration a resume would run against; called by `Nami.reset()`. */
61808
+ clearResumeState() {
61809
+ this.lastConfig = null;
61810
+ this.lastStartupRunAt = 0;
61811
+ this.startupDisabled = false;
61812
+ }
61813
+ /**
61814
+ * The platform-config startup step, ahead of device registration — the position it holds on
61815
+ * Apple (`.fetchPlatformConfig`) and Android (`fetchAppConfigOnce`) (NAM-4464, NAM-4589).
61816
+ * Without an initial config it is awaited, so everything downstream — `campaign_cache_ttl`
61817
+ * included — is evaluated against the config it leaves in storage. With one it resolves at
61818
+ * once and the request finishes in the background (see `fetchConfigForStartup`).
61819
+ *
61820
+ * @returns false when there is no usable config, in which case startup must stop.
61821
+ */
61822
+ async fetchPlatformConfig() {
61823
+ const outcome = await ConfigRepository.instance.fetchConfigForStartup(this.hasInitialConfig);
61824
+ if (!outcome.usable) {
61825
+ this.startupDisabled = true;
61826
+ logger.error(`Nami startup stopped: ${outcome.reason}`);
61827
+ return false;
61828
+ }
61829
+ return true;
61830
+ }
61620
61831
  setInitialValues(config) {
61621
61832
  const baseConfig = { ...config };
61622
61833
  delete baseConfig.initialConfig;
@@ -61877,7 +62088,7 @@ class LastLaunchStore {
61877
62088
  }
61878
62089
  }
61879
62090
 
61880
- var _Nami_isInitialized;
62091
+ var _Nami_isInitialized, _Nami_removeForegroundListener;
61881
62092
  // NamiFlowManager is intentionally NOT imported at top level — it
61882
62093
  // transitively imports back to this module (`Nami` for logging), and
61883
62094
  // pulling it in here causes a load-order cycle that breaks tests
@@ -61885,6 +62096,7 @@ var _Nami_isInitialized;
61885
62096
  class Nami {
61886
62097
  constructor() {
61887
62098
  _Nami_isInitialized.set(this, false);
62099
+ _Nami_removeForegroundListener.set(this, null);
61888
62100
  this.maxLogging = false;
61889
62101
  }
61890
62102
  get isInitialized() {
@@ -61908,9 +62120,13 @@ class Nami {
61908
62120
  * integrating app, consistent with the Apple and Android SDKs.
61909
62121
  */
61910
62122
  static async reset() {
62123
+ var _a;
61911
62124
  storageService.clearAll();
61912
62125
  clearInMemoryAnonymousMode();
61913
62126
  __classPrivateFieldSet(Nami.instance, _Nami_isInitialized, false, "f");
62127
+ __classPrivateFieldGet((_a = Nami.instance), _Nami_removeForegroundListener, "f")?.call(_a);
62128
+ __classPrivateFieldSet(Nami.instance, _Nami_removeForegroundListener, null, "f");
62129
+ NamiRefs.instance.clearResumeState();
61914
62130
  // In-memory singleton state that previously survived reset and
61915
62131
  // bled into the next configure() cycle.
61916
62132
  NamiProfileManager$1.instance.setExternalId(undefined);
@@ -61973,6 +62189,18 @@ class Nami {
61973
62189
  }
61974
62190
  config.initialConfig = {};
61975
62191
  }
62192
+ /**
62193
+ * Subscribes the resume config refresh to the host's foreground signal, once per process —
62194
+ * a reconfigure must not stack a second listener (the Android defect NAM-4541 fixed).
62195
+ * Registered before startup runs; `resume()` itself skips while startup is in flight.
62196
+ */
62197
+ listenForAppForeground() {
62198
+ if (__classPrivateFieldGet(this, _Nami_removeForegroundListener, "f"))
62199
+ return;
62200
+ __classPrivateFieldSet(this, _Nami_removeForegroundListener, getPlatformAdapters().device.onAppForeground?.(() => {
62201
+ void NamiRefs.instance.resume();
62202
+ }) ?? null, "f");
62203
+ }
61976
62204
  async initializeSDK(config) {
61977
62205
  let fullReconfig = false;
61978
62206
  let partialReconfig = false;
@@ -62013,6 +62241,7 @@ class Nami {
62013
62241
  // configure SDK with new config
62014
62242
  __classPrivateFieldSet(this, _Nami_isInitialized, true, "f");
62015
62243
  NamiAPI.configure(config);
62244
+ this.listenForAppForeground();
62016
62245
  await NamiRefs.instance.init(config);
62017
62246
  logger.info("SDK successfully initialized!");
62018
62247
  const state = partialReconfig || fullReconfig ? exports.RECONFIG_SUCCESS : exports.INITIAL_SUCCESS;
@@ -62024,7 +62253,7 @@ class Nami {
62024
62253
  };
62025
62254
  }
62026
62255
  }
62027
- _Nami_isInitialized = new WeakMap();
62256
+ _Nami_isInitialized = new WeakMap(), _Nami_removeForegroundListener = new WeakMap();
62028
62257
  Nami.instance = new Nami();
62029
62258
 
62030
62259
  const VALID_STYLES = new Set(['fullscreen', 'sheet', 'compact_sheet', 'modal']);
package/dist/index.d.ts CHANGED
@@ -65,6 +65,13 @@ interface IDeviceAdapter {
65
65
  generateUUID(): string;
66
66
  getScreenInfo(): ScreenInfo;
67
67
  getLanguage(): string;
68
+ /**
69
+ * Calls `listener` each time the app returns to the foreground — a browser tab becoming
70
+ * visible, a React Native app going `active` — and returns a function that stops listening.
71
+ * Drives core's resume config refresh (NAM-4589). Optional: a host with no foreground
72
+ * concept (the CLI) omits it and simply never resumes.
73
+ */
74
+ onAppForeground?(listener: () => void): () => void;
68
75
  }
69
76
 
70
77
  interface FormFactor {
@@ -1828,6 +1835,7 @@ type TPaywallContext = TInitialState & {
1828
1835
  anySkuHasIntroOffer: boolean;
1829
1836
  anySkuHasPromoOffer: boolean;
1830
1837
  safeAreaTop: number;
1838
+ safeAreaBottom: number;
1831
1839
  slides?: TCarouselSlidesState;
1832
1840
  currentPage: string;
1833
1841
  pageHistory: string[];
@@ -2121,6 +2129,12 @@ declare class Nami {
2121
2129
  */
2122
2130
  static configure(options: NamiConfiguration): Promise<NamiConfigurationState>;
2123
2131
  private static setInitialConfig;
2132
+ /**
2133
+ * Subscribes the resume config refresh to the host's foreground signal, once per process —
2134
+ * a reconfigure must not stack a second listener (the Android defect NAM-4541 fixed).
2135
+ * Registered before startup runs; `resume()` itself skips while startup is in flight.
2136
+ */
2137
+ private listenForAppForeground;
2124
2138
  private initializeSDK;
2125
2139
  }
2126
2140
 
@@ -2551,6 +2565,7 @@ declare class PaywallState extends SimpleEventTarget {
2551
2565
  setIsLoggedIn(isLoggedIn: boolean): void;
2552
2566
  setMediaList(media: TPaywallMedia[]): void;
2553
2567
  setSafeAreaTop(safeAreaTop: number): void;
2568
+ setSafeAreaBottom(safeAreaBottom: number): void;
2554
2569
  setFullScreenPresentation(fullScreenPresentation: boolean): void;
2555
2570
  setFormFactor(formFactor: TDevice): void;
2556
2571
  getFormFactor(): TDevice;
@@ -3181,12 +3196,16 @@ declare const LOG_HTTP_REQUESTS: string;
3181
3196
  declare const LOG_HTTP_TRAFFIC: string;
3182
3197
  declare const API_TRAFFIC_OBSERVER: string;
3183
3198
  declare const STARTUP_TELEMETRY: string;
3199
+ declare const SIMULATE_CONFIG_FETCH_FAILURE: string;
3184
3200
  declare const EXTENDED_PLATFORM: string;
3185
3201
  declare const EXTENDED_PLATFORM_VERSION: string;
3186
3202
  declare const API_MAX_CALLS_LIMIT: number;
3187
3203
  declare const API_RETRY_DELAY_SEC: number;
3188
3204
  declare const API_TIMEOUT_LIMIT: number;
3189
3205
  declare const DEVICE_API_TIMEOUT_LIMIT: number;
3206
+ declare const CONFIG_MAX_ATTEMPTS: number;
3207
+ declare const CONFIG_API_TIMEOUT_LIMIT: number;
3208
+ declare const RESUME_THROTTLE_MS: number;
3190
3209
  declare const STATUS_SUCCESS: number;
3191
3210
  declare const STATUS_BAD_REQUEST: number;
3192
3211
  declare const STATUS_NOT_FOUND: number;
@@ -3390,12 +3409,51 @@ declare class NamiRefs {
3390
3409
  static instance: NamiRefs;
3391
3410
  private inMemoryAnonymousMode;
3392
3411
  private isFirstSession;
3412
+ private startupDisabled;
3413
+ private hasInitialConfig;
3414
+ private lastConfig;
3415
+ private lastStartupRunAt;
3416
+ private startupInFlight;
3393
3417
  constructor();
3394
3418
  setInMemoryAnonymousMode(enabled: boolean): void;
3395
3419
  isInMemoryAnonymousMode(): boolean;
3396
3420
  isAnonymousMode(): boolean;
3397
3421
  getIsFirstSession(): boolean;
3422
+ /**
3423
+ * True when the platform-config step found no usable config and startup stopped after it —
3424
+ * the equivalent of Apple's `sdkEnabled = false` and Android's `NamiRefs.startupDisabled`.
3425
+ * Like both, it gates only the startup sequence, nothing else.
3426
+ */
3427
+ isStartupDisabled(): boolean;
3398
3428
  init(config: NamiConfiguration): Promise<void>;
3429
+ /**
3430
+ * The app returned to the foreground. Re-reads the platform config, as Apple's `.appResume`
3431
+ * and Android's `onAppComesFromBackground` do, so a config change — the `sdk` kill switch
3432
+ * included — reaches a long-lived tab or app without a cold start.
3433
+ *
3434
+ * - Skipped within {@link RESUME_THROTTLE_MS} of the last launch/resume, and while one is
3435
+ * still running.
3436
+ * - When the launch had **stopped** for want of a config and one is now usable, the rest of
3437
+ * startup runs — the steps an Apple resume re-runs because the launch never executed them.
3438
+ * - Otherwise this is a config refresh only. Apple and Android also re-run device, campaigns,
3439
+ * profile, products and the session start on resume; core does not yet, and has no
3440
+ * session end on background for a second session start to pair with (NAM-4623).
3441
+ */
3442
+ resume(): Promise<void>;
3443
+ /** Everything startup runs once the platform-config step has found a usable config. */
3444
+ private runStartupAfterConfig;
3445
+ /** Forgets the configuration a resume would run against; called by `Nami.reset()`. */
3446
+ clearResumeState(): void;
3447
+ /**
3448
+ * The platform-config startup step, ahead of device registration — the position it holds on
3449
+ * Apple (`.fetchPlatformConfig`) and Android (`fetchAppConfigOnce`) (NAM-4464, NAM-4589).
3450
+ * Without an initial config it is awaited, so everything downstream — `campaign_cache_ttl`
3451
+ * included — is evaluated against the config it leaves in storage. With one it resolves at
3452
+ * once and the request finishes in the background (see `fetchConfigForStartup`).
3453
+ *
3454
+ * @returns false when there is no usable config, in which case startup must stop.
3455
+ */
3456
+ private fetchPlatformConfig;
3399
3457
  private setInitialValues;
3400
3458
  private initAnonymousDevice;
3401
3459
  private initIdentifiedDevice;
@@ -3600,9 +3658,56 @@ declare class CampaignRuleRepository {
3600
3658
  private fallbackData;
3601
3659
  }
3602
3660
 
3661
+ /**
3662
+ * Whether startup may continue past the platform-config step; `reason` explains a `false`.
3663
+ * A plain shape rather than a discriminated union: core does not compile with
3664
+ * `strictNullChecks`, so a union on `usable` would not narrow.
3665
+ */
3666
+ type ConfigStartupOutcome = {
3667
+ usable: boolean;
3668
+ reason?: string;
3669
+ };
3603
3670
  declare class ConfigRepository {
3604
3671
  static instance: ConfigRepository;
3605
- fetchConfig(): Promise<IConfig | null>;
3672
+ /**
3673
+ * One `GET config/` attempt. Persists the config on success and throws on any failure —
3674
+ * a non-2xx or a transport error alike. `withRetry`'s own retries are turned off, so the
3675
+ * attempt count belongs to {@link fetchConfigForStartup} alone.
3676
+ */
3677
+ fetchConfig(): Promise<IConfig>;
3678
+ /**
3679
+ * The startup platform-config step, converged on Apple's `NamiPlatformConfigService.fetch` and
3680
+ * Android's `fetchAppConfigOnce` (NAM-4470, NAM-4589):
3681
+ *
3682
+ * | case | outcome |
3683
+ * |---|---|
3684
+ * | an initial config was supplied | NOT awaited: one attempt runs in the background and stores the config when it lands; usable |
3685
+ * | no initial config, success, capabilities include `sdk` | usable |
3686
+ * | no initial config, success, capabilities lack `sdk` | not usable |
3687
+ * | 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`) |
3688
+ *
3689
+ * The two fallbacks are consulted at two different points, deliberately. An initial config
3690
+ * decides the case *before* any request: startup does not wait on the network at all, since the
3691
+ * initial config is what makes the SDK ready without it (Apple: "Initial config is available.
3692
+ * Skipping retry."). The config cached by a previous launch is consulted only *after* the
3693
+ * retries run out. A stored `INITIAL_APP_CONFIG` is not that cache.
3694
+ *
3695
+ * One check does apply with an initial config, because it costs no wait: a cached config that
3696
+ * withholds `sdk` still stops startup, as Apple's cache load re-applies `updateSdkFlags`.
3697
+ * Without it a kill switch could never reach an app that ships an initial config — the
3698
+ * background fetch lands after startup has already run, so it takes effect from the next launch.
3699
+ *
3700
+ * A `404` is treated as any other 4xx, matching Apple's effective behaviour (its dedicated 404
3701
+ * branch is unreachable, NAM-4471) and Android's.
3702
+ *
3703
+ * @param hasInitialConfig whether the host passed an `initialConfig` that decoded to content.
3704
+ */
3705
+ fetchConfigForStartup(hasInitialConfig: boolean): Promise<ConfigStartupOutcome>;
3706
+ /**
3707
+ * The initial-config case: nothing is awaited. The cached kill switch is a storage read; the
3708
+ * config request is left running and persists its answer (`fetchConfig`) when it lands.
3709
+ */
3710
+ private startupOnInitialConfig;
3606
3711
  }
3607
3712
 
3608
3713
  declare class CustomerJourneyRepository {
@@ -4115,5 +4220,5 @@ declare namespace internal {
4115
4220
  };
4116
4221
  }
4117
4222
 
4118
- 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$2 as NamiCampaignManager, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager$2 as NamiCustomerManager, NamiEntitlementManager$2 as NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager$1 as NamiFlowManager, NamiFlowStepType, NamiHandoffTag, NamiPaywallAction, NamiPaywallManager$2 as NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiPermissionOutcome, NamiPurchaseManager$2 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 };
4223
+ 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, 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$2 as NamiCampaignManager, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager$2 as NamiCustomerManager, NamiEntitlementManager$2 as NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager$1 as NamiFlowManager, NamiFlowStepType, NamiHandoffTag, NamiPaywallAction, NamiPaywallManager$2 as NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiPermissionOutcome, NamiPurchaseManager$2 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, 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 };
4119
4224
  export type { AccountStateHandler$1 as AccountStateHandler, AlignmentType, AmazonProduct, ApiResponse, AppleProduct, AvailableCampaignsResponseHandler, BorderLocationType, BorderSideType, Callback$1 as Callback, CloseHandler, CustomerJourneyState, DeepLinkUrlHandler, Device, DevicePayload, DeviceProfile, DirectionType, ExtendedPlatformInfo, FlexDirectionObject, FlowNavigationOptions, FontCollection, FontDetails, FormFactor, FormFieldValidator, GoogleProduct, IConfig, IDeviceAdapter, IEntitlements$1 as IEntitlements, IPaywall, IPlatformAdapters, IProductsWithComponents, IPurchaseAdapter, ISkuMenu, IStorageAdapter, IUIAdapter, Impression, InitialConfig, InitialConfigCompressed, InitiateStateGroup, JourneySettings, LoginResponse, NamiAPITrafficKind, NamiAPITrafficObserver, NamiAPITrafficRecord, NamiAnimation, NamiAnimationObjectSpec, NamiAnimationSpec, NamiAnonymousCampaign, NamiAppSuppliedVideoDetails, NamiCampaign, NamiCampaignManagerStatic, NamiCampaignSegment, NamiCompletionResult, NamiConfiguration, NamiConfigurationState, NamiCustomAttributeValue, NamiCustomerManagerStatic, NamiEntitlement$1 as NamiEntitlement, NamiEntitlementManagerStatic, NamiFlowAction, NamiFlowAnimation, NamiFlowCampaign, NamiFlowCompletionCondition, NamiFlowDTO, NamiFlowEventHandler, NamiFlowHandoffStepHandler, NamiFlowManagerStatic, NamiFlowObjectDTO, NamiFlowOn, NamiFlowStep, NamiFlowTransition, NamiFlowTransitionDirection, NamiFlowWithObject, NamiHandoffAttributeValue, NamiHandoffComplete, NamiHandoffOutcome, NamiHandoffTagValue, NamiInitialConfig, NamiKnownHandoffTag, NamiLanguageCodes, NamiLogLevel, NamiLoginSuccess, NamiPaywallActionHandler, NamiPaywallComponentChange, NamiPaywallEvent, NamiPaywallEventVideoMetadata, NamiPaywallLaunchContext, NamiPaywallManagerStatic, NamiPresentationStyle, NamiProductDetails, NamiProductOffer, NamiProfile, NamiPurchase, NamiPurchaseCompleteResult, NamiPurchaseDetails, NamiPurchaseManagerStatic, NamiPurchaseSuccess, NamiPurchasesState, NamiSKU, NamiSKUType, NamiSubscriptionInterval, NamiSubscriptionPeriod, NamiUserDataEnvelope, 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, TSlot, TSlotGroupSelector, TSpacerComponent, TStack, TSvgImageComponent, TSymbolComponent, TTestObject, TTextComponent, TTextInputComponent, TTextInputType, TTextLikeComponent, TTextListComponent, TTimelineRail, TToggleButtonComponent, TToggleSwitch, TVariablePattern, TVideoComponent, TVolumeButton, TimerState, TransactionRequest, UserAction, UserActionParameters, Wave, WaveSpec };
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.202609251500",
107
+ NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.202609252349",
108
108
  // environments
109
109
  PRODUCTION = "production", DEVELOPMENT = "development",
110
110
  // error messages
@@ -114,7 +114,19 @@ 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", 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,
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", 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,
121
+ // Platform-config attempts when no initial config was supplied (Android's
122
+ // CONFIG_MAX_ATTEMPTS; Apple's retry(maxAttempts: 3)).
123
+ CONFIG_MAX_ATTEMPTS = 3,
124
+ // Per-attempt timeout for GET config/. Android's config client allows 5 s to connect plus 5 s
125
+ // to read (Apple allows 30 s); three attempts keep a total outage near 30 s.
126
+ CONFIG_API_TIMEOUT_LIMIT = 10000,
127
+ // A foreground within this long of the last launch/resume run is skipped — Apple's
128
+ // NamiStartupCoordinator.shouldRunResume window.
129
+ RESUME_THROTTLE_MS = 30000,
118
130
  // status codes
119
131
  STATUS_SUCCESS = 200, STATUS_BAD_REQUEST = 400, STATUS_NOT_FOUND = 404, STATUS_CONFLICT = 409, STATUS_INTERNAL_SERVER_ERROR = 500,
120
132
  // configuration states
@@ -58031,27 +58043,125 @@ class PaywallRepository {
58031
58043
  PaywallRepository.instance = new PaywallRepository();
58032
58044
 
58033
58045
  class ConfigRepository {
58046
+ /**
58047
+ * One `GET config/` attempt. Persists the config on success and throws on any failure —
58048
+ * a non-2xx or a transport error alike. `withRetry`'s own retries are turned off, so the
58049
+ * attempt count belongs to {@link fetchConfigForStartup} alone.
58050
+ */
58034
58051
  async fetchConfig() {
58035
- let data;
58036
- try {
58037
- data = await NamiAPI.instance.fetchAPI("config/");
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
- }
58052
+ // Thrown from *inside* the attempt on purpose, like Apple's and Android's: the ladder then
58053
+ // treats it exactly as a real 500, which is the whole point of the mock.
58054
+ if (storageService.getNamiConfig()?.namiCommands?.includes(SIMULATE_CONFIG_FETCH_FAILURE)) {
58055
+ throw new RetryLimitExceededError(STATUS_INTERNAL_SERVER_ERROR, SIMULATE_CONFIG_FETCH_FAILURE);
58047
58056
  }
58048
- if (data) {
58057
+ const platformId = storageService.getNamiConfig()?.appPlatformID;
58058
+ const data = await NamiAPI.instance.fetchAPI("config/", CONFIG_API_TIMEOUT_LIMIT, 0);
58059
+ // A late answer (the startup await gave up on it) must not overwrite another platform's
58060
+ // config after a reset or a platform-switching reconfigure.
58061
+ if (storageService.getNamiConfig()?.appPlatformID === platformId) {
58049
58062
  storageService.setAppConfig(API_CONFIG, data);
58050
58063
  }
58064
+ else {
58065
+ logger.debug("Discarding an app config response for a platform that is no longer configured");
58066
+ }
58051
58067
  return data;
58052
58068
  }
58069
+ /**
58070
+ * The startup platform-config step, converged on Apple's `NamiPlatformConfigService.fetch` and
58071
+ * Android's `fetchAppConfigOnce` (NAM-4470, NAM-4589):
58072
+ *
58073
+ * | case | outcome |
58074
+ * |---|---|
58075
+ * | an initial config was supplied | NOT awaited: one attempt runs in the background and stores the config when it lands; usable |
58076
+ * | no initial config, success, capabilities include `sdk` | usable |
58077
+ * | no initial config, success, capabilities lack `sdk` | not usable |
58078
+ * | 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`) |
58079
+ *
58080
+ * The two fallbacks are consulted at two different points, deliberately. An initial config
58081
+ * decides the case *before* any request: startup does not wait on the network at all, since the
58082
+ * initial config is what makes the SDK ready without it (Apple: "Initial config is available.
58083
+ * Skipping retry."). The config cached by a previous launch is consulted only *after* the
58084
+ * retries run out. A stored `INITIAL_APP_CONFIG` is not that cache.
58085
+ *
58086
+ * One check does apply with an initial config, because it costs no wait: a cached config that
58087
+ * withholds `sdk` still stops startup, as Apple's cache load re-applies `updateSdkFlags`.
58088
+ * Without it a kill switch could never reach an app that ships an initial config — the
58089
+ * background fetch lands after startup has already run, so it takes effect from the next launch.
58090
+ *
58091
+ * A `404` is treated as any other 4xx, matching Apple's effective behaviour (its dedicated 404
58092
+ * branch is unreachable, NAM-4471) and Android's.
58093
+ *
58094
+ * @param hasInitialConfig whether the host passed an `initialConfig` that decoded to content.
58095
+ */
58096
+ async fetchConfigForStartup(hasInitialConfig) {
58097
+ if (hasInitialConfig)
58098
+ return this.startupOnInitialConfig();
58099
+ const maxAttempts = CONFIG_MAX_ATTEMPTS;
58100
+ let lastError;
58101
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
58102
+ if (attempt > 1) {
58103
+ logger.debug(`Retrying app config fetch (attempt ${attempt} of ${maxAttempts})`);
58104
+ }
58105
+ try {
58106
+ return outcomeOf(await this.fetchConfig());
58107
+ }
58108
+ catch (error) {
58109
+ lastError = error;
58110
+ }
58111
+ }
58112
+ const cached = storageService.getAppConfig(API_CONFIG);
58113
+ if (cached) {
58114
+ // A cached kill switch still holds: Apple re-applies `updateSdkFlags` when it loads the
58115
+ // cached config, so one that withheld `sdk` keeps the SDK disabled through an outage.
58116
+ if (!grantsSdk(cached)) {
58117
+ return { usable: false, reason: "the cached app config does not grant the 'sdk' capability" };
58118
+ }
58119
+ logger.info(`App config fetch failed after ${maxAttempts} attempts - continuing on the cached config`);
58120
+ return { usable: true };
58121
+ }
58122
+ return {
58123
+ usable: false,
58124
+ reason: `app config fetch failed after ${maxAttempts} attempts (${describeFailure(lastError)}) ` +
58125
+ "and no initial or cached config is available",
58126
+ };
58127
+ }
58128
+ /**
58129
+ * The initial-config case: nothing is awaited. The cached kill switch is a storage read; the
58130
+ * config request is left running and persists its answer (`fetchConfig`) when it lands.
58131
+ */
58132
+ startupOnInitialConfig() {
58133
+ // Read before the request can land, so this launch is judged on what the last one stored.
58134
+ const cached = storageService.getAppConfig(API_CONFIG);
58135
+ // Issued even when the cache holds a kill switch: that is how a LIFTED kill switch reaches
58136
+ // the next launch.
58137
+ this.fetchConfig().then((config) => {
58138
+ if (!grantsSdk(config)) {
58139
+ logger.warn("App config does not grant the 'sdk' capability - it takes effect from the next launch");
58140
+ }
58141
+ }, (error) => {
58142
+ logger.info(`App config fetch failed (${describeFailure(error)}). Initial config is available. Skipping retry.`);
58143
+ });
58144
+ if (cached && !grantsSdk(cached)) {
58145
+ return { usable: false, reason: "the cached app config does not grant the 'sdk' capability" };
58146
+ }
58147
+ return { usable: true };
58148
+ }
58053
58149
  }
58054
58150
  ConfigRepository.instance = new ConfigRepository();
58151
+ function outcomeOf(config) {
58152
+ return grantsSdk(config)
58153
+ ? { usable: true }
58154
+ : { usable: false, reason: "app config does not grant the 'sdk' capability" };
58155
+ }
58156
+ function grantsSdk(config) {
58157
+ return config.capabilities?.includes(Capabilities.SDK) ?? false;
58158
+ }
58159
+ function describeFailure(error) {
58160
+ const status = error?.status;
58161
+ if (typeof status === "number")
58162
+ return `HTTP ${status}`;
58163
+ return error instanceof Error ? error.message : String(error);
58164
+ }
58055
58165
 
58056
58166
  // Global reference to avoid circular imports
58057
58167
  let namiRefsInstance = null;
@@ -60777,6 +60887,7 @@ const initialState = {
60777
60887
  isLoggedIn: false,
60778
60888
  darkMode: false,
60779
60889
  safeAreaTop: 0,
60890
+ safeAreaBottom: 0,
60780
60891
  groups: [],
60781
60892
  selectedProducts: {},
60782
60893
  formStates: {},
@@ -61145,6 +61256,9 @@ class PaywallState extends SimpleEventTarget {
61145
61256
  setSafeAreaTop(safeAreaTop) {
61146
61257
  this.setState({ ...this.state, safeAreaTop });
61147
61258
  }
61259
+ setSafeAreaBottom(safeAreaBottom) {
61260
+ this.setState({ ...this.state, safeAreaBottom });
61261
+ }
61148
61262
  setFullScreenPresentation(fullScreenPresentation) {
61149
61263
  this.setState({ ...this.state, fullScreenPresentation });
61150
61264
  }
@@ -61591,6 +61705,15 @@ class NamiRefs {
61591
61705
  this.inMemoryAnonymousMode = false;
61592
61706
  // Captured before device registration; true only on genuine fresh installs
61593
61707
  this.isFirstSession = false;
61708
+ // Set when the platform-config step leaves no usable config (see isStartupDisabled)
61709
+ this.startupDisabled = false;
61710
+ // Whether this configure() supplied an initial config (the config ladder's early exit)
61711
+ this.hasInitialConfig = false;
61712
+ // Resume bookkeeping: the configuration startup last ran with, when it (or a resume) last
61713
+ // ran, and whether one is running now.
61714
+ this.lastConfig = null;
61715
+ this.lastStartupRunAt = 0;
61716
+ this.startupInFlight = false;
61594
61717
  // Register this instance to avoid circular dependency issues
61595
61718
  setNamiRefsInstance(this);
61596
61719
  }
@@ -61608,13 +61731,101 @@ class NamiRefs {
61608
61731
  getIsFirstSession() {
61609
61732
  return this.isFirstSession;
61610
61733
  }
61734
+ /**
61735
+ * True when the platform-config step found no usable config and startup stopped after it —
61736
+ * the equivalent of Apple's `sdkEnabled = false` and Android's `NamiRefs.startupDisabled`.
61737
+ * Like both, it gates only the startup sequence, nothing else.
61738
+ */
61739
+ isStartupDisabled() {
61740
+ return this.startupDisabled;
61741
+ }
61611
61742
  async init(config) {
61612
61743
  this.isFirstSession = storageService.getDevice() === null && !storageService.getAnonymousMode();
61744
+ // Cleared per configure so a transient failure cannot disable a later run.
61745
+ this.startupDisabled = false;
61746
+ // `Nami.configure` has already normalised a missing or undecodable initialConfig to `{}`,
61747
+ // so "has content" is Apple's `initialConfig != nil`.
61748
+ this.hasInitialConfig = Object.keys(config.initialConfig ?? {}).length > 0;
61613
61749
  this.setInitialValues(config);
61614
- ConfigRepository.instance.fetchConfig();
61750
+ this.lastConfig = config;
61751
+ this.lastStartupRunAt = Date.now();
61752
+ this.startupInFlight = true;
61753
+ try {
61754
+ if (!(await this.fetchPlatformConfig()))
61755
+ return;
61756
+ await this.runStartupAfterConfig(config);
61757
+ }
61758
+ finally {
61759
+ this.startupInFlight = false;
61760
+ }
61761
+ }
61762
+ /**
61763
+ * The app returned to the foreground. Re-reads the platform config, as Apple's `.appResume`
61764
+ * and Android's `onAppComesFromBackground` do, so a config change — the `sdk` kill switch
61765
+ * included — reaches a long-lived tab or app without a cold start.
61766
+ *
61767
+ * - Skipped within {@link RESUME_THROTTLE_MS} of the last launch/resume, and while one is
61768
+ * still running.
61769
+ * - When the launch had **stopped** for want of a config and one is now usable, the rest of
61770
+ * startup runs — the steps an Apple resume re-runs because the launch never executed them.
61771
+ * - Otherwise this is a config refresh only. Apple and Android also re-run device, campaigns,
61772
+ * profile, products and the session start on resume; core does not yet, and has no
61773
+ * session end on background for a second session start to pair with (NAM-4623).
61774
+ */
61775
+ async resume() {
61776
+ if (!this.lastConfig || this.startupInFlight)
61777
+ return;
61778
+ if (Date.now() - this.lastStartupRunAt < RESUME_THROTTLE_MS) {
61779
+ logger.debug("Resuming too quickly to call the service.");
61780
+ return;
61781
+ }
61782
+ const config = this.lastConfig;
61783
+ const wasStopped = this.startupDisabled;
61784
+ this.lastStartupRunAt = Date.now();
61785
+ this.startupInFlight = true;
61786
+ this.startupDisabled = false;
61787
+ try {
61788
+ if (!(await this.fetchPlatformConfig()) || !wasStopped)
61789
+ return;
61790
+ logger.info("Usable app config on resume - completing the startup that stopped at launch");
61791
+ await this.runStartupAfterConfig(config);
61792
+ }
61793
+ catch (error) {
61794
+ logger.warn(`Resume refresh failed: ${error}`);
61795
+ }
61796
+ finally {
61797
+ this.startupInFlight = false;
61798
+ }
61799
+ }
61800
+ /** Everything startup runs once the platform-config step has found a usable config. */
61801
+ async runStartupAfterConfig(config) {
61615
61802
  await this.initAndFetchRequiredData(config);
61616
61803
  SessionService.instance.startSession();
61617
61804
  }
61805
+ /** Forgets the configuration a resume would run against; called by `Nami.reset()`. */
61806
+ clearResumeState() {
61807
+ this.lastConfig = null;
61808
+ this.lastStartupRunAt = 0;
61809
+ this.startupDisabled = false;
61810
+ }
61811
+ /**
61812
+ * The platform-config startup step, ahead of device registration — the position it holds on
61813
+ * Apple (`.fetchPlatformConfig`) and Android (`fetchAppConfigOnce`) (NAM-4464, NAM-4589).
61814
+ * Without an initial config it is awaited, so everything downstream — `campaign_cache_ttl`
61815
+ * included — is evaluated against the config it leaves in storage. With one it resolves at
61816
+ * once and the request finishes in the background (see `fetchConfigForStartup`).
61817
+ *
61818
+ * @returns false when there is no usable config, in which case startup must stop.
61819
+ */
61820
+ async fetchPlatformConfig() {
61821
+ const outcome = await ConfigRepository.instance.fetchConfigForStartup(this.hasInitialConfig);
61822
+ if (!outcome.usable) {
61823
+ this.startupDisabled = true;
61824
+ logger.error(`Nami startup stopped: ${outcome.reason}`);
61825
+ return false;
61826
+ }
61827
+ return true;
61828
+ }
61618
61829
  setInitialValues(config) {
61619
61830
  const baseConfig = { ...config };
61620
61831
  delete baseConfig.initialConfig;
@@ -61875,7 +62086,7 @@ class LastLaunchStore {
61875
62086
  }
61876
62087
  }
61877
62088
 
61878
- var _Nami_isInitialized;
62089
+ var _Nami_isInitialized, _Nami_removeForegroundListener;
61879
62090
  // NamiFlowManager is intentionally NOT imported at top level — it
61880
62091
  // transitively imports back to this module (`Nami` for logging), and
61881
62092
  // pulling it in here causes a load-order cycle that breaks tests
@@ -61883,6 +62094,7 @@ var _Nami_isInitialized;
61883
62094
  class Nami {
61884
62095
  constructor() {
61885
62096
  _Nami_isInitialized.set(this, false);
62097
+ _Nami_removeForegroundListener.set(this, null);
61886
62098
  this.maxLogging = false;
61887
62099
  }
61888
62100
  get isInitialized() {
@@ -61906,9 +62118,13 @@ class Nami {
61906
62118
  * integrating app, consistent with the Apple and Android SDKs.
61907
62119
  */
61908
62120
  static async reset() {
62121
+ var _a;
61909
62122
  storageService.clearAll();
61910
62123
  clearInMemoryAnonymousMode();
61911
62124
  __classPrivateFieldSet(Nami.instance, _Nami_isInitialized, false, "f");
62125
+ __classPrivateFieldGet((_a = Nami.instance), _Nami_removeForegroundListener, "f")?.call(_a);
62126
+ __classPrivateFieldSet(Nami.instance, _Nami_removeForegroundListener, null, "f");
62127
+ NamiRefs.instance.clearResumeState();
61912
62128
  // In-memory singleton state that previously survived reset and
61913
62129
  // bled into the next configure() cycle.
61914
62130
  NamiProfileManager$1.instance.setExternalId(undefined);
@@ -61971,6 +62187,18 @@ class Nami {
61971
62187
  }
61972
62188
  config.initialConfig = {};
61973
62189
  }
62190
+ /**
62191
+ * Subscribes the resume config refresh to the host's foreground signal, once per process —
62192
+ * a reconfigure must not stack a second listener (the Android defect NAM-4541 fixed).
62193
+ * Registered before startup runs; `resume()` itself skips while startup is in flight.
62194
+ */
62195
+ listenForAppForeground() {
62196
+ if (__classPrivateFieldGet(this, _Nami_removeForegroundListener, "f"))
62197
+ return;
62198
+ __classPrivateFieldSet(this, _Nami_removeForegroundListener, getPlatformAdapters().device.onAppForeground?.(() => {
62199
+ void NamiRefs.instance.resume();
62200
+ }) ?? null, "f");
62201
+ }
61974
62202
  async initializeSDK(config) {
61975
62203
  let fullReconfig = false;
61976
62204
  let partialReconfig = false;
@@ -62011,6 +62239,7 @@ class Nami {
62011
62239
  // configure SDK with new config
62012
62240
  __classPrivateFieldSet(this, _Nami_isInitialized, true, "f");
62013
62241
  NamiAPI.configure(config);
62242
+ this.listenForAppForeground();
62014
62243
  await NamiRefs.instance.init(config);
62015
62244
  logger.info("SDK successfully initialized!");
62016
62245
  const state = partialReconfig || fullReconfig ? RECONFIG_SUCCESS : INITIAL_SUCCESS;
@@ -62022,7 +62251,7 @@ class Nami {
62022
62251
  };
62023
62252
  }
62024
62253
  }
62025
- _Nami_isInitialized = new WeakMap();
62254
+ _Nami_isInitialized = new WeakMap(), _Nami_removeForegroundListener = new WeakMap();
62026
62255
  Nami.instance = new Nami();
62027
62256
 
62028
62257
  const VALID_STYLES = new Set(['fullscreen', 'sheet', 'compact_sheet', 'modal']);
@@ -65992,4 +66221,4 @@ var internal = /*#__PURE__*/Object.freeze({
65992
66221
  NamiPurchaseManager: NamiPurchaseManager
65993
66222
  });
65994
66223
 
65995
- 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 };
66224
+ 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, 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, 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namiml/sdk-core",
3
- "version": "3.4.12-dev.202609251500",
3
+ "version": "3.4.12-dev.202609252349",
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",