@namiml/sdk-core 3.4.12-dev.202609281806 → 3.4.12-dev.202609290126

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.202609281806",
109
+ NAMI_SDK_PACKAGE_VERSION: exports.NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.202609290126",
110
110
  // environments
111
111
  PRODUCTION: exports.PRODUCTION = "production", DEVELOPMENT: exports.DEVELOPMENT = "development",
112
112
  // error messages
@@ -7370,211 +7370,6 @@ async function timeoutRequest$1(url, options = {}, timeout) {
7370
7370
  return response;
7371
7371
  }
7372
7372
 
7373
- function requirePlatformID(target, propertyKey, descriptor) {
7374
- const originalMethod = descriptor.value;
7375
- descriptor.value = function (...args) {
7376
- if (!this.platformID) {
7377
- throw new PlatformIDRequiredError();
7378
- }
7379
- return originalMethod.apply(this, args);
7380
- };
7381
- return descriptor;
7382
- }
7383
- function requireDeviceID(target, propertyKey, descriptor) {
7384
- const originalMethod = descriptor.value;
7385
- descriptor.value = function (...args) {
7386
- const device = storageService.getDevice();
7387
- if (!device?.id) {
7388
- throw new DeviceIDRequiredError();
7389
- }
7390
- return originalMethod.apply(this, args);
7391
- };
7392
- return descriptor;
7393
- }
7394
-
7395
- class NamiAPI {
7396
- get deviceID() {
7397
- return storageService.getDevice()?.id;
7398
- }
7399
- constructor() {
7400
- const config = storageService.getNamiConfig();
7401
- if (!this.platformID && config) {
7402
- this.configure(config);
7403
- }
7404
- }
7405
- static configure(config) {
7406
- this.instance.configure(config);
7407
- }
7408
- configure(config) {
7409
- this.baseURL = getBaseUrl(config.namiCommands);
7410
- this.platformID = config.appPlatformID;
7411
- }
7412
- /**
7413
- * Clear cached baseURL and platformID so the next `configure()` call
7414
- * re-derives them from fresh storage. Called from `Nami.reset()`.
7415
- *
7416
- * Without this, the NamiAPI singleton holds the previous lifecycle's
7417
- * platform/base URL even after storage has been wiped — any in-flight
7418
- * or queued request after reset would hit the old endpoint.
7419
- */
7420
- static reset() {
7421
- this.instance.reset();
7422
- }
7423
- reset() {
7424
- this.baseURL = undefined;
7425
- this.platformID = undefined;
7426
- }
7427
- async login(externalId) {
7428
- if (!externalId) {
7429
- throw new ExternalIDRequiredError();
7430
- }
7431
- const path = `device/${this.deviceID}/login/`;
7432
- const data = { external_id: externalId };
7433
- return await this.requestBodyAPI(path, data, 'PUT');
7434
- }
7435
- async logout() {
7436
- const path = `device/${this.deviceID}/logout/`;
7437
- return await this.requestBodyAPI(path, {}, 'PUT');
7438
- }
7439
- async startSession(sessionStartTime) {
7440
- const path = `device/${this.deviceID}/session/`;
7441
- const resp = await this.requestBodyAPI(path, {
7442
- start_time: sessionStartTime,
7443
- app_env: "production",
7444
- });
7445
- storageService.setSessionCounter();
7446
- if (resp.id) {
7447
- storageService.setSessionId(resp.id);
7448
- }
7449
- }
7450
- async postImpression(options) {
7451
- const path = `device/${this.deviceID}/impression/`;
7452
- const resp = await this.requestBodyAPI(path, options, "POST", true);
7453
- if (resp.id) {
7454
- storageService.setLastImpressionId(resp.id);
7455
- // Notify the flow-completion engine (no-op unless a flow is open) so it
7456
- // can bind this impression to the current screen's completion context.
7457
- NamiAPI.impressionListener?.(resp.id);
7458
- }
7459
- }
7460
- /**
7461
- * Reports a flow completion result against a prior impression
7462
- * (NAM-1545 / Flow Completion Step). UPDATE-only on the server; the
7463
- * impression must already exist. `keepalive` so a dismiss-triggered PATCH
7464
- * survives page unload on web.
7465
- */
7466
- async patchImpressionCompletion(impressionId, completion_result, completion_result_date) {
7467
- const path = `device/${this.deviceID}/impression/${impressionId}/`;
7468
- await this.requestBodyAPI(path, { completion_result, completion_result_date }, "PATCH", true);
7469
- }
7470
- async postConversion(options) {
7471
- const path = `device/${this.deviceID}/transaction/`;
7472
- await this.requestBodyAPI(path, options, "POST", true);
7473
- }
7474
- async purchaseValidation(options) {
7475
- const path = `device/${this.deviceID}/purchase_event/`;
7476
- await this.requestBodyAPI(path, options, "PUT");
7477
- }
7478
- async fetchAPI(path, timeout, retries) {
7479
- const url = `${this.baseURL}/${this.platformID}/` + path;
7480
- return await withRetry(url, undefined, timeout, retries);
7481
- }
7482
- async requestBodyAPI(path, bodyData, method = "POST", keepalive = false, timeout, retries) {
7483
- const url = `${this.baseURL}/${this.platformID}/` + path;
7484
- const options = {
7485
- method,
7486
- headers: {
7487
- "Content-Type": "application/json",
7488
- },
7489
- body: JSON.stringify(bodyData),
7490
- keepalive,
7491
- };
7492
- return await withRetry(url, options, timeout, retries);
7493
- }
7494
- }
7495
- NamiAPI.instance = new NamiAPI();
7496
- __decorate([
7497
- requireDeviceID
7498
- ], NamiAPI.prototype, "login", null);
7499
- __decorate([
7500
- requireDeviceID
7501
- ], NamiAPI.prototype, "logout", null);
7502
- __decorate([
7503
- requireDeviceID
7504
- ], NamiAPI.prototype, "startSession", null);
7505
- __decorate([
7506
- requireDeviceID
7507
- ], NamiAPI.prototype, "postImpression", null);
7508
- __decorate([
7509
- requireDeviceID
7510
- ], NamiAPI.prototype, "patchImpressionCompletion", null);
7511
- __decorate([
7512
- requireDeviceID
7513
- ], NamiAPI.prototype, "postConversion", null);
7514
- __decorate([
7515
- requireDeviceID
7516
- ], NamiAPI.prototype, "purchaseValidation", null);
7517
- __decorate([
7518
- requirePlatformID
7519
- ], NamiAPI.prototype, "fetchAPI", null);
7520
- __decorate([
7521
- requirePlatformID
7522
- ], NamiAPI.prototype, "requestBodyAPI", null);
7523
-
7524
- function isNamiFlowCampaign(campaign) {
7525
- return campaign?.flow != null;
7526
- }
7527
- /**
7528
- * @enum NamiCampaignRuleType
7529
- * Types of Nami campaigns
7530
- */
7531
- exports.NamiCampaignRuleType = void 0;
7532
- (function (NamiCampaignRuleType) {
7533
- NamiCampaignRuleType["DEFAULT"] = "default";
7534
- NamiCampaignRuleType["LABEL"] = "label";
7535
- NamiCampaignRuleType["UNKNOWN"] = "unknown";
7536
- NamiCampaignRuleType["URL"] = "url";
7537
- })(exports.NamiCampaignRuleType || (exports.NamiCampaignRuleType = {}));
7538
- // NamiCampaignManagerStatic interface + const proxy live in
7539
- // `src/managers/campaign.ts`. They must stay out of this file: PaywallState
7540
- // type-imports from here, and pulling in the manager class triggers a
7541
- // constructor chain that runs before PaywallState finishes loading. See NAM-1207.
7542
-
7543
- /**
7544
- * Get initial paywalls from storage service.
7545
- * @returns An array of initial paywalls from storage service.
7546
- */
7547
- const getInitialPaywalls = () => {
7548
- return storageService.getPaywalls(exports.INITIAL_PAYWALLS) ?? [];
7549
- };
7550
- /**
7551
- * Get all API paywalls from storage service.
7552
- * @returns An array of API paywalls from storage service.
7553
- */
7554
- const getApiPaywalls = () => {
7555
- return storageService.getPaywalls(exports.API_PAYWALLS) ?? [];
7556
- };
7557
- /**
7558
- * Get all paywalls from both API and Initial sources, ensuring uniqueness.
7559
- * This function combines paywalls from both sources and filters out duplicates.
7560
- * API Data takes precedence over Initial Data.
7561
- *
7562
- * @returns A combined list of unique paywalls based on both API and Initial data.
7563
- */
7564
- const allPaywalls = () => {
7565
- const paywalls = compact([...getApiPaywalls(), ...getInitialPaywalls()]);
7566
- return uniqBy(paywalls, "id");
7567
- };
7568
- const getPaywall = (paywallId) => {
7569
- const paywalls = allPaywalls();
7570
- return paywalls.find((paywall) => paywall.id === paywallId);
7571
- };
7572
- const hasAllPaywalls = (paywallIds) => {
7573
- const paywalls = allPaywalls();
7574
- const paywallIdSet = new Set(paywalls.map(p => p.id));
7575
- return paywallIds.every(id => paywallIdSet.has(id));
7576
- };
7577
-
7578
7373
  const globalScope = typeof globalThis !== 'undefined' ? globalThis :
7579
7374
  typeof self !== 'undefined' ? self :
7580
7375
  typeof global !== 'undefined' ? global :
@@ -7798,6 +7593,227 @@ function uuidFromSplitPosition(namiCommands) {
7798
7593
  return value && !isNaN(numericalValue) ? generateUuidForSplitPosition(numericalValue) : undefined;
7799
7594
  }
7800
7595
 
7596
+ function requirePlatformID(target, propertyKey, descriptor) {
7597
+ const originalMethod = descriptor.value;
7598
+ descriptor.value = function (...args) {
7599
+ if (!this.platformID) {
7600
+ throw new PlatformIDRequiredError();
7601
+ }
7602
+ return originalMethod.apply(this, args);
7603
+ };
7604
+ return descriptor;
7605
+ }
7606
+ function requireDeviceID(target, propertyKey, descriptor) {
7607
+ const originalMethod = descriptor.value;
7608
+ descriptor.value = function (...args) {
7609
+ const device = storageService.getDevice();
7610
+ if (!device?.id) {
7611
+ throw new DeviceIDRequiredError();
7612
+ }
7613
+ return originalMethod.apply(this, args);
7614
+ };
7615
+ return descriptor;
7616
+ }
7617
+
7618
+ class NamiAPI {
7619
+ get deviceID() {
7620
+ return storageService.getDevice()?.id;
7621
+ }
7622
+ constructor() {
7623
+ const config = storageService.getNamiConfig();
7624
+ if (!this.platformID && config) {
7625
+ this.configure(config);
7626
+ }
7627
+ }
7628
+ static configure(config) {
7629
+ this.instance.configure(config);
7630
+ }
7631
+ configure(config) {
7632
+ this.baseURL = getBaseUrl(config.namiCommands);
7633
+ this.platformID = config.appPlatformID;
7634
+ }
7635
+ /**
7636
+ * Clear cached baseURL and platformID so the next `configure()` call
7637
+ * re-derives them from fresh storage. Called from `Nami.reset()`.
7638
+ *
7639
+ * Without this, the NamiAPI singleton holds the previous lifecycle's
7640
+ * platform/base URL even after storage has been wiped — any in-flight
7641
+ * or queued request after reset would hit the old endpoint.
7642
+ */
7643
+ static reset() {
7644
+ this.instance.reset();
7645
+ }
7646
+ reset() {
7647
+ this.baseURL = undefined;
7648
+ this.platformID = undefined;
7649
+ }
7650
+ async login(externalId) {
7651
+ if (!externalId) {
7652
+ throw new ExternalIDRequiredError();
7653
+ }
7654
+ const path = `device/${this.deviceID}/login/`;
7655
+ const data = { external_id: externalId };
7656
+ return await this.requestBodyAPI(path, data, 'PUT');
7657
+ }
7658
+ async logout() {
7659
+ const path = `device/${this.deviceID}/logout/`;
7660
+ return await this.requestBodyAPI(path, {}, 'PUT');
7661
+ }
7662
+ async startSession(sessionStartTime) {
7663
+ const path = `device/${this.deviceID}/session/`;
7664
+ const resp = await this.requestBodyAPI(path, {
7665
+ start_time: sessionStartTime,
7666
+ app_env: "production",
7667
+ });
7668
+ storageService.setSessionCounter();
7669
+ if (resp.id) {
7670
+ storageService.setSessionId(resp.id);
7671
+ }
7672
+ }
7673
+ async postImpression(options) {
7674
+ const path = `device/${this.deviceID}/impression/`;
7675
+ // Declare the id rather than taking it from the response. Three things depended on that
7676
+ // response before NAM-4421:
7677
+ //
7678
+ // 1. Retries were not idempotent. `withRetry` re-sends on any non-ok response, so a backend
7679
+ // (or a proxy) that created the impression and then returned 5xx got a *second* impression
7680
+ // per retry — up to three rows for one page view, inflating the denominator of paywall
7681
+ // conversion rate.
7682
+ // 2. Until the response landed, `getLastImpressionId()` still held the previous page's id, so
7683
+ // anything snapshotting in that window — including `setPurchaseImpression()` on a buy tap —
7684
+ // copied the wrong page.
7685
+ // 3. When every retry failed, the id was never published at all, so storage kept the previous
7686
+ // page's id and purchases here were attributed to the page before this one.
7687
+ const impressionId = options.id ?? generateUUID();
7688
+ const body = {
7689
+ ...options,
7690
+ id: impressionId,
7691
+ impression_date: options.impression_date ?? new Date().toISOString(),
7692
+ };
7693
+ storageService.setLastImpressionId(impressionId);
7694
+ // Notify the flow-completion engine (no-op unless a flow is open) so it
7695
+ // can bind this impression to the current screen's completion context.
7696
+ NamiAPI.impressionListener?.(impressionId);
7697
+ await this.requestBodyAPI(path, body, "POST", true);
7698
+ }
7699
+ /**
7700
+ * Reports a flow completion result against a prior impression
7701
+ * (NAM-1545 / Flow Completion Step). UPDATE-only on the server; the
7702
+ * impression must already exist. `keepalive` so a dismiss-triggered PATCH
7703
+ * survives page unload on web.
7704
+ */
7705
+ async patchImpressionCompletion(impressionId, completion_result, completion_result_date) {
7706
+ const path = `device/${this.deviceID}/impression/${impressionId}/`;
7707
+ await this.requestBodyAPI(path, { completion_result, completion_result_date }, "PATCH", true);
7708
+ }
7709
+ async postConversion(options) {
7710
+ const path = `device/${this.deviceID}/transaction/`;
7711
+ await this.requestBodyAPI(path, options, "POST", true);
7712
+ }
7713
+ async purchaseValidation(options) {
7714
+ const path = `device/${this.deviceID}/purchase_event/`;
7715
+ await this.requestBodyAPI(path, options, "PUT");
7716
+ }
7717
+ async fetchAPI(path, timeout, retries) {
7718
+ const url = `${this.baseURL}/${this.platformID}/` + path;
7719
+ return await withRetry(url, undefined, timeout, retries);
7720
+ }
7721
+ async requestBodyAPI(path, bodyData, method = "POST", keepalive = false, timeout, retries) {
7722
+ const url = `${this.baseURL}/${this.platformID}/` + path;
7723
+ const options = {
7724
+ method,
7725
+ headers: {
7726
+ "Content-Type": "application/json",
7727
+ },
7728
+ body: JSON.stringify(bodyData),
7729
+ keepalive,
7730
+ };
7731
+ return await withRetry(url, options, timeout, retries);
7732
+ }
7733
+ }
7734
+ NamiAPI.instance = new NamiAPI();
7735
+ __decorate([
7736
+ requireDeviceID
7737
+ ], NamiAPI.prototype, "login", null);
7738
+ __decorate([
7739
+ requireDeviceID
7740
+ ], NamiAPI.prototype, "logout", null);
7741
+ __decorate([
7742
+ requireDeviceID
7743
+ ], NamiAPI.prototype, "startSession", null);
7744
+ __decorate([
7745
+ requireDeviceID
7746
+ ], NamiAPI.prototype, "postImpression", null);
7747
+ __decorate([
7748
+ requireDeviceID
7749
+ ], NamiAPI.prototype, "patchImpressionCompletion", null);
7750
+ __decorate([
7751
+ requireDeviceID
7752
+ ], NamiAPI.prototype, "postConversion", null);
7753
+ __decorate([
7754
+ requireDeviceID
7755
+ ], NamiAPI.prototype, "purchaseValidation", null);
7756
+ __decorate([
7757
+ requirePlatformID
7758
+ ], NamiAPI.prototype, "fetchAPI", null);
7759
+ __decorate([
7760
+ requirePlatformID
7761
+ ], NamiAPI.prototype, "requestBodyAPI", null);
7762
+
7763
+ function isNamiFlowCampaign(campaign) {
7764
+ return campaign?.flow != null;
7765
+ }
7766
+ /**
7767
+ * @enum NamiCampaignRuleType
7768
+ * Types of Nami campaigns
7769
+ */
7770
+ exports.NamiCampaignRuleType = void 0;
7771
+ (function (NamiCampaignRuleType) {
7772
+ NamiCampaignRuleType["DEFAULT"] = "default";
7773
+ NamiCampaignRuleType["LABEL"] = "label";
7774
+ NamiCampaignRuleType["UNKNOWN"] = "unknown";
7775
+ NamiCampaignRuleType["URL"] = "url";
7776
+ })(exports.NamiCampaignRuleType || (exports.NamiCampaignRuleType = {}));
7777
+ // NamiCampaignManagerStatic interface + const proxy live in
7778
+ // `src/managers/campaign.ts`. They must stay out of this file: PaywallState
7779
+ // type-imports from here, and pulling in the manager class triggers a
7780
+ // constructor chain that runs before PaywallState finishes loading. See NAM-1207.
7781
+
7782
+ /**
7783
+ * Get initial paywalls from storage service.
7784
+ * @returns An array of initial paywalls from storage service.
7785
+ */
7786
+ const getInitialPaywalls = () => {
7787
+ return storageService.getPaywalls(exports.INITIAL_PAYWALLS) ?? [];
7788
+ };
7789
+ /**
7790
+ * Get all API paywalls from storage service.
7791
+ * @returns An array of API paywalls from storage service.
7792
+ */
7793
+ const getApiPaywalls = () => {
7794
+ return storageService.getPaywalls(exports.API_PAYWALLS) ?? [];
7795
+ };
7796
+ /**
7797
+ * Get all paywalls from both API and Initial sources, ensuring uniqueness.
7798
+ * This function combines paywalls from both sources and filters out duplicates.
7799
+ * API Data takes precedence over Initial Data.
7800
+ *
7801
+ * @returns A combined list of unique paywalls based on both API and Initial data.
7802
+ */
7803
+ const allPaywalls = () => {
7804
+ const paywalls = compact([...getApiPaywalls(), ...getInitialPaywalls()]);
7805
+ return uniqBy(paywalls, "id");
7806
+ };
7807
+ const getPaywall = (paywallId) => {
7808
+ const paywalls = allPaywalls();
7809
+ return paywalls.find((paywall) => paywall.id === paywallId);
7810
+ };
7811
+ const hasAllPaywalls = (paywallIds) => {
7812
+ const paywalls = allPaywalls();
7813
+ const paywallIdSet = new Set(paywalls.map(p => p.id));
7814
+ return paywallIds.every(id => paywallIdSet.has(id));
7815
+ };
7816
+
7801
7817
  const formatDistanceLocale$1j = {
7802
7818
  lessThanXSeconds: {
7803
7819
  one: "minder as 'n sekonde",
@@ -57070,7 +57086,16 @@ const selectSegment = (segments, splitPosition) => {
57070
57086
  const mapAnonymousCampaigns = (campaigns, splitPosition, formFactor) => {
57071
57087
  if (!campaigns)
57072
57088
  return [];
57089
+ // A rule without a `segments` array is not in the anonymous shape — the backend serves the
57090
+ // legacy resolved shape (a flat `segment`) when a request carries no usable sdk_version. Skip
57091
+ // it rather than throw: throwing here escapes configure() and stops startup (NAM-4653).
57092
+ const unsegmented = campaigns.filter((campaign) => !Array.isArray(campaign.segments)).length;
57093
+ if (unsegmented > 0) {
57094
+ logger.warn(`Skipping ${unsegmented} of ${campaigns.length} campaign rules with no segments array`);
57095
+ }
57073
57096
  return campaigns.flatMap((campaign) => {
57097
+ if (!Array.isArray(campaign.segments))
57098
+ return [];
57074
57099
  let segments;
57075
57100
  if (typeof formFactor === 'string') {
57076
57101
  segments = campaign.segments.filter(seg => seg.form_factors.some(f => f.form_factor === formFactor));
package/dist/index.d.ts CHANGED
@@ -3153,6 +3153,12 @@ type Session = {
3153
3153
  };
3154
3154
  type Impression = {
3155
3155
  id?: string;
3156
+ /**
3157
+ * When the page was presented, as an ISO-8601 string. Distinct from when the request is sent:
3158
+ * the POST is retried on failure, so arrival time would date a retried page view by its last
3159
+ * attempt and file it in the wrong reporting period (NAM-4421 / NAM-4419).
3160
+ */
3161
+ impression_date?: string;
3156
3162
  session?: string;
3157
3163
  segment?: string;
3158
3164
  call_to_action?: string;
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.202609281806",
107
+ NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.202609290126",
108
108
  // environments
109
109
  PRODUCTION = "production", DEVELOPMENT = "development",
110
110
  // error messages
@@ -7368,211 +7368,6 @@ async function timeoutRequest$1(url, options = {}, timeout) {
7368
7368
  return response;
7369
7369
  }
7370
7370
 
7371
- function requirePlatformID(target, propertyKey, descriptor) {
7372
- const originalMethod = descriptor.value;
7373
- descriptor.value = function (...args) {
7374
- if (!this.platformID) {
7375
- throw new PlatformIDRequiredError();
7376
- }
7377
- return originalMethod.apply(this, args);
7378
- };
7379
- return descriptor;
7380
- }
7381
- function requireDeviceID(target, propertyKey, descriptor) {
7382
- const originalMethod = descriptor.value;
7383
- descriptor.value = function (...args) {
7384
- const device = storageService.getDevice();
7385
- if (!device?.id) {
7386
- throw new DeviceIDRequiredError();
7387
- }
7388
- return originalMethod.apply(this, args);
7389
- };
7390
- return descriptor;
7391
- }
7392
-
7393
- class NamiAPI {
7394
- get deviceID() {
7395
- return storageService.getDevice()?.id;
7396
- }
7397
- constructor() {
7398
- const config = storageService.getNamiConfig();
7399
- if (!this.platformID && config) {
7400
- this.configure(config);
7401
- }
7402
- }
7403
- static configure(config) {
7404
- this.instance.configure(config);
7405
- }
7406
- configure(config) {
7407
- this.baseURL = getBaseUrl(config.namiCommands);
7408
- this.platformID = config.appPlatformID;
7409
- }
7410
- /**
7411
- * Clear cached baseURL and platformID so the next `configure()` call
7412
- * re-derives them from fresh storage. Called from `Nami.reset()`.
7413
- *
7414
- * Without this, the NamiAPI singleton holds the previous lifecycle's
7415
- * platform/base URL even after storage has been wiped — any in-flight
7416
- * or queued request after reset would hit the old endpoint.
7417
- */
7418
- static reset() {
7419
- this.instance.reset();
7420
- }
7421
- reset() {
7422
- this.baseURL = undefined;
7423
- this.platformID = undefined;
7424
- }
7425
- async login(externalId) {
7426
- if (!externalId) {
7427
- throw new ExternalIDRequiredError();
7428
- }
7429
- const path = `device/${this.deviceID}/login/`;
7430
- const data = { external_id: externalId };
7431
- return await this.requestBodyAPI(path, data, 'PUT');
7432
- }
7433
- async logout() {
7434
- const path = `device/${this.deviceID}/logout/`;
7435
- return await this.requestBodyAPI(path, {}, 'PUT');
7436
- }
7437
- async startSession(sessionStartTime) {
7438
- const path = `device/${this.deviceID}/session/`;
7439
- const resp = await this.requestBodyAPI(path, {
7440
- start_time: sessionStartTime,
7441
- app_env: "production",
7442
- });
7443
- storageService.setSessionCounter();
7444
- if (resp.id) {
7445
- storageService.setSessionId(resp.id);
7446
- }
7447
- }
7448
- async postImpression(options) {
7449
- const path = `device/${this.deviceID}/impression/`;
7450
- const resp = await this.requestBodyAPI(path, options, "POST", true);
7451
- if (resp.id) {
7452
- storageService.setLastImpressionId(resp.id);
7453
- // Notify the flow-completion engine (no-op unless a flow is open) so it
7454
- // can bind this impression to the current screen's completion context.
7455
- NamiAPI.impressionListener?.(resp.id);
7456
- }
7457
- }
7458
- /**
7459
- * Reports a flow completion result against a prior impression
7460
- * (NAM-1545 / Flow Completion Step). UPDATE-only on the server; the
7461
- * impression must already exist. `keepalive` so a dismiss-triggered PATCH
7462
- * survives page unload on web.
7463
- */
7464
- async patchImpressionCompletion(impressionId, completion_result, completion_result_date) {
7465
- const path = `device/${this.deviceID}/impression/${impressionId}/`;
7466
- await this.requestBodyAPI(path, { completion_result, completion_result_date }, "PATCH", true);
7467
- }
7468
- async postConversion(options) {
7469
- const path = `device/${this.deviceID}/transaction/`;
7470
- await this.requestBodyAPI(path, options, "POST", true);
7471
- }
7472
- async purchaseValidation(options) {
7473
- const path = `device/${this.deviceID}/purchase_event/`;
7474
- await this.requestBodyAPI(path, options, "PUT");
7475
- }
7476
- async fetchAPI(path, timeout, retries) {
7477
- const url = `${this.baseURL}/${this.platformID}/` + path;
7478
- return await withRetry(url, undefined, timeout, retries);
7479
- }
7480
- async requestBodyAPI(path, bodyData, method = "POST", keepalive = false, timeout, retries) {
7481
- const url = `${this.baseURL}/${this.platformID}/` + path;
7482
- const options = {
7483
- method,
7484
- headers: {
7485
- "Content-Type": "application/json",
7486
- },
7487
- body: JSON.stringify(bodyData),
7488
- keepalive,
7489
- };
7490
- return await withRetry(url, options, timeout, retries);
7491
- }
7492
- }
7493
- NamiAPI.instance = new NamiAPI();
7494
- __decorate([
7495
- requireDeviceID
7496
- ], NamiAPI.prototype, "login", null);
7497
- __decorate([
7498
- requireDeviceID
7499
- ], NamiAPI.prototype, "logout", null);
7500
- __decorate([
7501
- requireDeviceID
7502
- ], NamiAPI.prototype, "startSession", null);
7503
- __decorate([
7504
- requireDeviceID
7505
- ], NamiAPI.prototype, "postImpression", null);
7506
- __decorate([
7507
- requireDeviceID
7508
- ], NamiAPI.prototype, "patchImpressionCompletion", null);
7509
- __decorate([
7510
- requireDeviceID
7511
- ], NamiAPI.prototype, "postConversion", null);
7512
- __decorate([
7513
- requireDeviceID
7514
- ], NamiAPI.prototype, "purchaseValidation", null);
7515
- __decorate([
7516
- requirePlatformID
7517
- ], NamiAPI.prototype, "fetchAPI", null);
7518
- __decorate([
7519
- requirePlatformID
7520
- ], NamiAPI.prototype, "requestBodyAPI", null);
7521
-
7522
- function isNamiFlowCampaign(campaign) {
7523
- return campaign?.flow != null;
7524
- }
7525
- /**
7526
- * @enum NamiCampaignRuleType
7527
- * Types of Nami campaigns
7528
- */
7529
- var NamiCampaignRuleType;
7530
- (function (NamiCampaignRuleType) {
7531
- NamiCampaignRuleType["DEFAULT"] = "default";
7532
- NamiCampaignRuleType["LABEL"] = "label";
7533
- NamiCampaignRuleType["UNKNOWN"] = "unknown";
7534
- NamiCampaignRuleType["URL"] = "url";
7535
- })(NamiCampaignRuleType || (NamiCampaignRuleType = {}));
7536
- // NamiCampaignManagerStatic interface + const proxy live in
7537
- // `src/managers/campaign.ts`. They must stay out of this file: PaywallState
7538
- // type-imports from here, and pulling in the manager class triggers a
7539
- // constructor chain that runs before PaywallState finishes loading. See NAM-1207.
7540
-
7541
- /**
7542
- * Get initial paywalls from storage service.
7543
- * @returns An array of initial paywalls from storage service.
7544
- */
7545
- const getInitialPaywalls = () => {
7546
- return storageService.getPaywalls(INITIAL_PAYWALLS) ?? [];
7547
- };
7548
- /**
7549
- * Get all API paywalls from storage service.
7550
- * @returns An array of API paywalls from storage service.
7551
- */
7552
- const getApiPaywalls = () => {
7553
- return storageService.getPaywalls(API_PAYWALLS) ?? [];
7554
- };
7555
- /**
7556
- * Get all paywalls from both API and Initial sources, ensuring uniqueness.
7557
- * This function combines paywalls from both sources and filters out duplicates.
7558
- * API Data takes precedence over Initial Data.
7559
- *
7560
- * @returns A combined list of unique paywalls based on both API and Initial data.
7561
- */
7562
- const allPaywalls = () => {
7563
- const paywalls = compact([...getApiPaywalls(), ...getInitialPaywalls()]);
7564
- return uniqBy(paywalls, "id");
7565
- };
7566
- const getPaywall = (paywallId) => {
7567
- const paywalls = allPaywalls();
7568
- return paywalls.find((paywall) => paywall.id === paywallId);
7569
- };
7570
- const hasAllPaywalls = (paywallIds) => {
7571
- const paywalls = allPaywalls();
7572
- const paywallIdSet = new Set(paywalls.map(p => p.id));
7573
- return paywallIds.every(id => paywallIdSet.has(id));
7574
- };
7575
-
7576
7371
  const globalScope = typeof globalThis !== 'undefined' ? globalThis :
7577
7372
  typeof self !== 'undefined' ? self :
7578
7373
  typeof global !== 'undefined' ? global :
@@ -7796,6 +7591,227 @@ function uuidFromSplitPosition(namiCommands) {
7796
7591
  return value && !isNaN(numericalValue) ? generateUuidForSplitPosition(numericalValue) : undefined;
7797
7592
  }
7798
7593
 
7594
+ function requirePlatformID(target, propertyKey, descriptor) {
7595
+ const originalMethod = descriptor.value;
7596
+ descriptor.value = function (...args) {
7597
+ if (!this.platformID) {
7598
+ throw new PlatformIDRequiredError();
7599
+ }
7600
+ return originalMethod.apply(this, args);
7601
+ };
7602
+ return descriptor;
7603
+ }
7604
+ function requireDeviceID(target, propertyKey, descriptor) {
7605
+ const originalMethod = descriptor.value;
7606
+ descriptor.value = function (...args) {
7607
+ const device = storageService.getDevice();
7608
+ if (!device?.id) {
7609
+ throw new DeviceIDRequiredError();
7610
+ }
7611
+ return originalMethod.apply(this, args);
7612
+ };
7613
+ return descriptor;
7614
+ }
7615
+
7616
+ class NamiAPI {
7617
+ get deviceID() {
7618
+ return storageService.getDevice()?.id;
7619
+ }
7620
+ constructor() {
7621
+ const config = storageService.getNamiConfig();
7622
+ if (!this.platformID && config) {
7623
+ this.configure(config);
7624
+ }
7625
+ }
7626
+ static configure(config) {
7627
+ this.instance.configure(config);
7628
+ }
7629
+ configure(config) {
7630
+ this.baseURL = getBaseUrl(config.namiCommands);
7631
+ this.platformID = config.appPlatformID;
7632
+ }
7633
+ /**
7634
+ * Clear cached baseURL and platformID so the next `configure()` call
7635
+ * re-derives them from fresh storage. Called from `Nami.reset()`.
7636
+ *
7637
+ * Without this, the NamiAPI singleton holds the previous lifecycle's
7638
+ * platform/base URL even after storage has been wiped — any in-flight
7639
+ * or queued request after reset would hit the old endpoint.
7640
+ */
7641
+ static reset() {
7642
+ this.instance.reset();
7643
+ }
7644
+ reset() {
7645
+ this.baseURL = undefined;
7646
+ this.platformID = undefined;
7647
+ }
7648
+ async login(externalId) {
7649
+ if (!externalId) {
7650
+ throw new ExternalIDRequiredError();
7651
+ }
7652
+ const path = `device/${this.deviceID}/login/`;
7653
+ const data = { external_id: externalId };
7654
+ return await this.requestBodyAPI(path, data, 'PUT');
7655
+ }
7656
+ async logout() {
7657
+ const path = `device/${this.deviceID}/logout/`;
7658
+ return await this.requestBodyAPI(path, {}, 'PUT');
7659
+ }
7660
+ async startSession(sessionStartTime) {
7661
+ const path = `device/${this.deviceID}/session/`;
7662
+ const resp = await this.requestBodyAPI(path, {
7663
+ start_time: sessionStartTime,
7664
+ app_env: "production",
7665
+ });
7666
+ storageService.setSessionCounter();
7667
+ if (resp.id) {
7668
+ storageService.setSessionId(resp.id);
7669
+ }
7670
+ }
7671
+ async postImpression(options) {
7672
+ const path = `device/${this.deviceID}/impression/`;
7673
+ // Declare the id rather than taking it from the response. Three things depended on that
7674
+ // response before NAM-4421:
7675
+ //
7676
+ // 1. Retries were not idempotent. `withRetry` re-sends on any non-ok response, so a backend
7677
+ // (or a proxy) that created the impression and then returned 5xx got a *second* impression
7678
+ // per retry — up to three rows for one page view, inflating the denominator of paywall
7679
+ // conversion rate.
7680
+ // 2. Until the response landed, `getLastImpressionId()` still held the previous page's id, so
7681
+ // anything snapshotting in that window — including `setPurchaseImpression()` on a buy tap —
7682
+ // copied the wrong page.
7683
+ // 3. When every retry failed, the id was never published at all, so storage kept the previous
7684
+ // page's id and purchases here were attributed to the page before this one.
7685
+ const impressionId = options.id ?? generateUUID();
7686
+ const body = {
7687
+ ...options,
7688
+ id: impressionId,
7689
+ impression_date: options.impression_date ?? new Date().toISOString(),
7690
+ };
7691
+ storageService.setLastImpressionId(impressionId);
7692
+ // Notify the flow-completion engine (no-op unless a flow is open) so it
7693
+ // can bind this impression to the current screen's completion context.
7694
+ NamiAPI.impressionListener?.(impressionId);
7695
+ await this.requestBodyAPI(path, body, "POST", true);
7696
+ }
7697
+ /**
7698
+ * Reports a flow completion result against a prior impression
7699
+ * (NAM-1545 / Flow Completion Step). UPDATE-only on the server; the
7700
+ * impression must already exist. `keepalive` so a dismiss-triggered PATCH
7701
+ * survives page unload on web.
7702
+ */
7703
+ async patchImpressionCompletion(impressionId, completion_result, completion_result_date) {
7704
+ const path = `device/${this.deviceID}/impression/${impressionId}/`;
7705
+ await this.requestBodyAPI(path, { completion_result, completion_result_date }, "PATCH", true);
7706
+ }
7707
+ async postConversion(options) {
7708
+ const path = `device/${this.deviceID}/transaction/`;
7709
+ await this.requestBodyAPI(path, options, "POST", true);
7710
+ }
7711
+ async purchaseValidation(options) {
7712
+ const path = `device/${this.deviceID}/purchase_event/`;
7713
+ await this.requestBodyAPI(path, options, "PUT");
7714
+ }
7715
+ async fetchAPI(path, timeout, retries) {
7716
+ const url = `${this.baseURL}/${this.platformID}/` + path;
7717
+ return await withRetry(url, undefined, timeout, retries);
7718
+ }
7719
+ async requestBodyAPI(path, bodyData, method = "POST", keepalive = false, timeout, retries) {
7720
+ const url = `${this.baseURL}/${this.platformID}/` + path;
7721
+ const options = {
7722
+ method,
7723
+ headers: {
7724
+ "Content-Type": "application/json",
7725
+ },
7726
+ body: JSON.stringify(bodyData),
7727
+ keepalive,
7728
+ };
7729
+ return await withRetry(url, options, timeout, retries);
7730
+ }
7731
+ }
7732
+ NamiAPI.instance = new NamiAPI();
7733
+ __decorate([
7734
+ requireDeviceID
7735
+ ], NamiAPI.prototype, "login", null);
7736
+ __decorate([
7737
+ requireDeviceID
7738
+ ], NamiAPI.prototype, "logout", null);
7739
+ __decorate([
7740
+ requireDeviceID
7741
+ ], NamiAPI.prototype, "startSession", null);
7742
+ __decorate([
7743
+ requireDeviceID
7744
+ ], NamiAPI.prototype, "postImpression", null);
7745
+ __decorate([
7746
+ requireDeviceID
7747
+ ], NamiAPI.prototype, "patchImpressionCompletion", null);
7748
+ __decorate([
7749
+ requireDeviceID
7750
+ ], NamiAPI.prototype, "postConversion", null);
7751
+ __decorate([
7752
+ requireDeviceID
7753
+ ], NamiAPI.prototype, "purchaseValidation", null);
7754
+ __decorate([
7755
+ requirePlatformID
7756
+ ], NamiAPI.prototype, "fetchAPI", null);
7757
+ __decorate([
7758
+ requirePlatformID
7759
+ ], NamiAPI.prototype, "requestBodyAPI", null);
7760
+
7761
+ function isNamiFlowCampaign(campaign) {
7762
+ return campaign?.flow != null;
7763
+ }
7764
+ /**
7765
+ * @enum NamiCampaignRuleType
7766
+ * Types of Nami campaigns
7767
+ */
7768
+ var NamiCampaignRuleType;
7769
+ (function (NamiCampaignRuleType) {
7770
+ NamiCampaignRuleType["DEFAULT"] = "default";
7771
+ NamiCampaignRuleType["LABEL"] = "label";
7772
+ NamiCampaignRuleType["UNKNOWN"] = "unknown";
7773
+ NamiCampaignRuleType["URL"] = "url";
7774
+ })(NamiCampaignRuleType || (NamiCampaignRuleType = {}));
7775
+ // NamiCampaignManagerStatic interface + const proxy live in
7776
+ // `src/managers/campaign.ts`. They must stay out of this file: PaywallState
7777
+ // type-imports from here, and pulling in the manager class triggers a
7778
+ // constructor chain that runs before PaywallState finishes loading. See NAM-1207.
7779
+
7780
+ /**
7781
+ * Get initial paywalls from storage service.
7782
+ * @returns An array of initial paywalls from storage service.
7783
+ */
7784
+ const getInitialPaywalls = () => {
7785
+ return storageService.getPaywalls(INITIAL_PAYWALLS) ?? [];
7786
+ };
7787
+ /**
7788
+ * Get all API paywalls from storage service.
7789
+ * @returns An array of API paywalls from storage service.
7790
+ */
7791
+ const getApiPaywalls = () => {
7792
+ return storageService.getPaywalls(API_PAYWALLS) ?? [];
7793
+ };
7794
+ /**
7795
+ * Get all paywalls from both API and Initial sources, ensuring uniqueness.
7796
+ * This function combines paywalls from both sources and filters out duplicates.
7797
+ * API Data takes precedence over Initial Data.
7798
+ *
7799
+ * @returns A combined list of unique paywalls based on both API and Initial data.
7800
+ */
7801
+ const allPaywalls = () => {
7802
+ const paywalls = compact([...getApiPaywalls(), ...getInitialPaywalls()]);
7803
+ return uniqBy(paywalls, "id");
7804
+ };
7805
+ const getPaywall = (paywallId) => {
7806
+ const paywalls = allPaywalls();
7807
+ return paywalls.find((paywall) => paywall.id === paywallId);
7808
+ };
7809
+ const hasAllPaywalls = (paywallIds) => {
7810
+ const paywalls = allPaywalls();
7811
+ const paywallIdSet = new Set(paywalls.map(p => p.id));
7812
+ return paywallIds.every(id => paywallIdSet.has(id));
7813
+ };
7814
+
7799
7815
  const formatDistanceLocale$1j = {
7800
7816
  lessThanXSeconds: {
7801
7817
  one: "minder as 'n sekonde",
@@ -57068,7 +57084,16 @@ const selectSegment = (segments, splitPosition) => {
57068
57084
  const mapAnonymousCampaigns = (campaigns, splitPosition, formFactor) => {
57069
57085
  if (!campaigns)
57070
57086
  return [];
57087
+ // A rule without a `segments` array is not in the anonymous shape — the backend serves the
57088
+ // legacy resolved shape (a flat `segment`) when a request carries no usable sdk_version. Skip
57089
+ // it rather than throw: throwing here escapes configure() and stops startup (NAM-4653).
57090
+ const unsegmented = campaigns.filter((campaign) => !Array.isArray(campaign.segments)).length;
57091
+ if (unsegmented > 0) {
57092
+ logger.warn(`Skipping ${unsegmented} of ${campaigns.length} campaign rules with no segments array`);
57093
+ }
57071
57094
  return campaigns.flatMap((campaign) => {
57095
+ if (!Array.isArray(campaign.segments))
57096
+ return [];
57072
57097
  let segments;
57073
57098
  if (typeof formFactor === 'string') {
57074
57099
  segments = campaign.segments.filter(seg => seg.form_factors.some(f => f.form_factor === formFactor));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namiml/sdk-core",
3
- "version": "3.4.12-dev.202609281806",
3
+ "version": "3.4.12-dev.202609290126",
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",