@namiml/sdk-core 3.4.4-dev.202606302245 → 3.4.4-dev.202606302327

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -98,7 +98,7 @@ const {
98
98
  // version — stamped by scripts/version.sh
99
99
  NAMI_SDK_VERSION: exports.NAMI_SDK_VERSION = "3.4.4",
100
100
  // full package version including dev suffix — stamped by scripts/version.sh
101
- NAMI_SDK_PACKAGE_VERSION: exports.NAMI_SDK_PACKAGE_VERSION = "3.4.4-dev.202606302245",
101
+ NAMI_SDK_PACKAGE_VERSION: exports.NAMI_SDK_PACKAGE_VERSION = "3.4.4-dev.202606302327",
102
102
  // environments
103
103
  PRODUCTION: exports.PRODUCTION = "production", DEVELOPMENT: exports.DEVELOPMENT = "development",
104
104
  // error messages
@@ -12392,6 +12392,38 @@ let NamiProfileManager$1 = class NamiProfileManager {
12392
12392
  };
12393
12393
  NamiProfileManager$1.instance = new NamiProfileManager$1();
12394
12394
 
12395
+ /**
12396
+ * Two-slot, session-scoped store of the most recent launches.
12397
+ *
12398
+ * On `record(rec)` the existing `current` slot is demoted to `previous` and
12399
+ * `rec` becomes the new `current`. Flow conditions read `previous`, which
12400
+ * structurally excludes the placement that is launching right now (the PPO
12401
+ * requirement): the first launch in a session leaves `previous` undefined.
12402
+ *
12403
+ * State is in-memory only and cleared via `reset()` (wired into `Nami.reset()`).
12404
+ */
12405
+ class LastLaunchStore {
12406
+ static get shared() {
12407
+ if (!this._shared)
12408
+ this._shared = new LastLaunchStore();
12409
+ return this._shared;
12410
+ }
12411
+ /** The Last Launch — the launch immediately before the current one. */
12412
+ get previous() {
12413
+ return this._previous;
12414
+ }
12415
+ /** Demote `current` → `previous`, then set `current` to `rec`. */
12416
+ record(rec) {
12417
+ this._previous = this._current;
12418
+ this._current = rec;
12419
+ }
12420
+ /** Clear both slots. */
12421
+ reset() {
12422
+ this._current = undefined;
12423
+ this._previous = undefined;
12424
+ }
12425
+ }
12426
+
12395
12427
  var _Nami_isInitialized;
12396
12428
  // NamiFlowManager is intentionally NOT imported at top level — it
12397
12429
  // transitively imports back to this module (`Nami` for logging), and
@@ -12432,6 +12464,7 @@ class Nami {
12432
12464
  PaywallState.reset();
12433
12465
  NamiAPI.reset();
12434
12466
  CampaignRuleRepository.instance.reset();
12467
+ LastLaunchStore.shared.reset();
12435
12468
  // Lazy import to avoid a load-order cycle (NamiFlowManager imports
12436
12469
  // back to this module for `Nami.instance.maxLogging`).
12437
12470
  const { NamiFlowManager } = await Promise.resolve().then(function () { return NamiFlowManager$3; });
@@ -12732,6 +12765,7 @@ const FilterOperator = {
12732
12765
  NOT_EQUALS: 'not_equals',
12733
12766
  NOT_I_EQUALS: 'not_i_equals',
12734
12767
  NOT_CONTAINS: 'not_contains',
12768
+ CONTAINS: 'contains',
12735
12769
  SET: 'set',
12736
12770
  NOT_SET: 'not_set',
12737
12771
  GREATER_THAN: 'greater_than',
@@ -12864,14 +12898,24 @@ class NamiConditionEvaluator {
12864
12898
  case FilterOperator.NOT_I_EQUALS: {
12865
12899
  return filter.values.every(expected => !this.strictEquals(resolvedValue, expected, true));
12866
12900
  }
12901
+ case FilterOperator.CONTAINS: {
12902
+ // Array (tags): ANY-match by exact membership. String: case-sensitive substring.
12903
+ // `includes` covers both (Array.prototype.includes / String.prototype.includes).
12904
+ if (Array.isArray(resolvedValue) || typeof resolvedValue === 'string') {
12905
+ return filter.values.some(expected => typeof expected === 'string' && resolvedValue.includes(expected));
12906
+ }
12907
+ return false;
12908
+ }
12867
12909
  case FilterOperator.NOT_CONTAINS: {
12868
- const result = filter.values.every(expected => {
12910
+ if (Array.isArray(resolvedValue)) {
12911
+ return filter.values.every(expected => !(typeof expected === 'string' && resolvedValue.includes(expected)));
12912
+ }
12913
+ return filter.values.every(expected => {
12869
12914
  if (typeof resolvedValue === 'string' && typeof expected === 'string') {
12870
12915
  return !resolvedValue.includes(expected);
12871
12916
  }
12872
12917
  return true;
12873
12918
  });
12874
- return result;
12875
12919
  }
12876
12920
  case FilterOperator.GREATER_THAN:
12877
12921
  case FilterOperator.GREATER_THAN_OR_EQUAL_TO:
@@ -13016,14 +13060,22 @@ class BaseNamespaceResolver {
13016
13060
 
13017
13061
  // import { logger } from "../../services/logger.service";
13018
13062
  class LaunchContextResolver extends BaseNamespaceResolver {
13019
- constructor(context) {
13063
+ constructor(context, campaign) {
13020
13064
  super();
13021
13065
  this.namespace = 'LaunchContext';
13022
13066
  this.context = context;
13067
+ this.campaign = campaign;
13023
13068
  this.register();
13024
13069
  }
13025
13070
  resolveValue(keyPath) {
13026
13071
  // logger.debug('[LaunchContextResolver]', 'Resolving keyPath', keyPath);
13072
+ // Current-launch placement metadata sourced from the launching campaign.
13073
+ if (keyPath === 'placement') {
13074
+ return this.campaign?.value ?? undefined;
13075
+ }
13076
+ if (keyPath === 'tags') {
13077
+ return this.campaign?.placement_tags ?? undefined;
13078
+ }
13027
13079
  if (keyPath.startsWith('customAttributes.')) {
13028
13080
  const innerPath = keyPath.substring('customAttributes.'.length);
13029
13081
  return this.resolveKeyPath(innerPath, this.context.customAttributes);
@@ -13446,6 +13498,50 @@ class PlacementLabelResolver extends BaseNamespaceResolver {
13446
13498
  }
13447
13499
  }
13448
13500
 
13501
+ /**
13502
+ * Resolves flow conditions against the *previous* launch (the "Last Launch"),
13503
+ * exposed under the `LastLaunchContext` namespace (NAM-1937):
13504
+ *
13505
+ * - `LastLaunchContext.placement` → previous launch's placement label
13506
+ * - `LastLaunchContext.tags` → previous launch's placement tags
13507
+ * - `LastLaunchContext.customAttributes.<k>` → a custom attribute from the previous launch
13508
+ *
13509
+ * When there is no previous launch (first launch of the session), every key
13510
+ * resolves to `undefined`. The condition evaluator's existing null-state
13511
+ * handling then makes `equals`/`set` false and `not_set` true — so the
13512
+ * resolver deliberately does not special-case any operator.
13513
+ */
13514
+ class LastLaunchContextResolver extends BaseNamespaceResolver {
13515
+ constructor() {
13516
+ super(...arguments);
13517
+ this.namespace = "LastLaunchContext";
13518
+ }
13519
+ // Exposed publicly so callers register explicitly (consistent with how this
13520
+ // resolver is wired from NamiFlow and exercised in tests). Unlike the other
13521
+ // resolvers it does not auto-register from its constructor because it reads
13522
+ // live store state rather than capturing per-launch context.
13523
+ register() {
13524
+ super.register();
13525
+ }
13526
+ resolveValue(keyPath) {
13527
+ const previous = LastLaunchStore.shared.previous;
13528
+ if (!previous) {
13529
+ return undefined;
13530
+ }
13531
+ if (keyPath === "placement") {
13532
+ return previous.placementLabel;
13533
+ }
13534
+ if (keyPath === "tags") {
13535
+ return previous.tags;
13536
+ }
13537
+ if (keyPath.startsWith("customAttributes.")) {
13538
+ const innerPath = keyPath.substring("customAttributes.".length);
13539
+ return this.resolveKeyPath(innerPath, previous.customAttributes);
13540
+ }
13541
+ return undefined;
13542
+ }
13543
+ }
13544
+
13449
13545
  /**
13450
13546
  * Represents the possible account actions states that can be returned by callback registered from
13451
13547
  * [NamiCustomerManager.registerAccountStateHandler]
@@ -14749,7 +14845,7 @@ class NamiFlow extends BasicNamiFlow {
14749
14845
  applyLaunchContextAttributes(attrs) {
14750
14846
  if (!this.context) {
14751
14847
  this.context = { customAttributes: {} };
14752
- new LaunchContextResolver(this.context);
14848
+ new LaunchContextResolver(this.context, this.campaign);
14753
14849
  }
14754
14850
  // Guard against a runtime-supplied context that omits customAttributes
14755
14851
  // (the field is required by the type but may be absent in untyped JSON).
@@ -14759,12 +14855,13 @@ class NamiFlow extends BasicNamiFlow {
14759
14855
  }
14760
14856
  registerResolvers(context) {
14761
14857
  if (context) {
14762
- new LaunchContextResolver(context);
14858
+ new LaunchContextResolver(context, this.campaign);
14763
14859
  }
14764
14860
  new DeviceResolver();
14765
14861
  new URLParamsResolver();
14766
14862
  new FormStateResolver();
14767
14863
  new PlacementLabelResolver(this.campaign);
14864
+ new LastLaunchContextResolver().register();
14768
14865
  NamiConditionEvaluator.shared.registerNamespaceResolver('Flow', (identifier) => {
14769
14866
  switch (identifier) {
14770
14867
  case 'Flow.stepcrumbs':
@@ -15617,7 +15714,11 @@ let NamiCampaignManager$2 = class NamiCampaignManager {
15617
15714
  const data = startupTelemetry.firstLaunchLookup(value, () => getPaywallDataFromLabel(value, type));
15618
15715
  let paywall = data.paywall;
15619
15716
  const campaign = data.campaign;
15620
- if (!campaign || (!paywall && !campaign.flow)) {
15717
+ // Captured before the degenerate branch below clobbers `paywall` to `{}`,
15718
+ // so a not-found launch is correctly distinguishable from a real one when
15719
+ // deciding whether to record into LastLaunchStore (NAM-1937).
15720
+ const campaignNotFound = !campaign || (!paywall && !campaign.flow);
15721
+ if (campaignNotFound) {
15621
15722
  let error;
15622
15723
  if (!label && !withUrl) {
15623
15724
  error = exports.LaunchCampaignError.DEFAULT_CAMPAIGN_NOT_FOUND;
@@ -15668,6 +15769,23 @@ let NamiCampaignManager$2 = class NamiCampaignManager {
15668
15769
  resultCallback(false, exports.LaunchCampaignError.FLOW_SCREEN_DATA_UNAVILABLE);
15669
15770
  throw new FlowScreensNotAvailableError();
15670
15771
  }
15772
+ // Record this launch into the two-slot Last Launch store BEFORE the flow
15773
+ // resolvers read it. The just-launched placement becomes `current`; the
15774
+ // prior launch is demoted to `previous`, which flow conditions read via
15775
+ // the LastLaunchContext namespace (NAM-1937).
15776
+ //
15777
+ // Only record when a real placement is actually being launched. The
15778
+ // not-found/degenerate path above sets `paywall = {}` and falls through
15779
+ // (the throw is commented out); recording there would store a bogus
15780
+ // failed-launch record AND demote the real `current`, breaking the
15781
+ // two-slot/PPO guarantee for the next real launch.
15782
+ if (!campaignNotFound) {
15783
+ LastLaunchStore.shared.record({
15784
+ placementLabel: campaign?.value ?? value ?? "",
15785
+ tags: campaign?.placement_tags ?? [],
15786
+ customAttributes: (context?.customAttributes ?? {}),
15787
+ });
15788
+ }
15671
15789
  const component = getPlatformAdapters().ui.createPaywall(type, value, context);
15672
15790
  // Generate and store launch ID for successful campaign launch
15673
15791
  const launchId = generateUUID();
package/dist/index.d.ts CHANGED
@@ -91,6 +91,7 @@ declare const FilterOperator: {
91
91
  readonly NOT_EQUALS: "not_equals";
92
92
  readonly NOT_I_EQUALS: "not_i_equals";
93
93
  readonly NOT_CONTAINS: "not_contains";
94
+ readonly CONTAINS: "contains";
94
95
  readonly SET: "set";
95
96
  readonly NOT_SET: "not_set";
96
97
  readonly GREATER_THAN: "greater_than";
@@ -1055,6 +1056,7 @@ interface NamiCampaign {
1055
1056
  page_urls?: Record<string, string> | null;
1056
1057
  type: string | NamiCampaignRuleType;
1057
1058
  value?: string | null;
1059
+ placement_tags?: string[];
1058
1060
  form_factors: FormFactor[];
1059
1061
  external_segment: string | null;
1060
1062
  conversion_event_type?: CampaignRuleConversionEventType | null;
@@ -3513,7 +3515,8 @@ declare abstract class BaseNamespaceResolver {
3513
3515
  declare class LaunchContextResolver extends BaseNamespaceResolver {
3514
3516
  protected readonly namespace = "LaunchContext";
3515
3517
  private context;
3516
- constructor(context: NamiPaywallLaunchContext);
3518
+ private campaign?;
3519
+ constructor(context: NamiPaywallLaunchContext, campaign?: NamiCampaign);
3517
3520
  protected resolveValue(keyPath: string): any;
3518
3521
  }
3519
3522
 
package/dist/index.mjs CHANGED
@@ -96,7 +96,7 @@ const {
96
96
  // version — stamped by scripts/version.sh
97
97
  NAMI_SDK_VERSION = "3.4.4",
98
98
  // full package version including dev suffix — stamped by scripts/version.sh
99
- NAMI_SDK_PACKAGE_VERSION = "3.4.4-dev.202606302245",
99
+ NAMI_SDK_PACKAGE_VERSION = "3.4.4-dev.202606302327",
100
100
  // environments
101
101
  PRODUCTION = "production", DEVELOPMENT = "development",
102
102
  // error messages
@@ -12390,6 +12390,38 @@ let NamiProfileManager$1 = class NamiProfileManager {
12390
12390
  };
12391
12391
  NamiProfileManager$1.instance = new NamiProfileManager$1();
12392
12392
 
12393
+ /**
12394
+ * Two-slot, session-scoped store of the most recent launches.
12395
+ *
12396
+ * On `record(rec)` the existing `current` slot is demoted to `previous` and
12397
+ * `rec` becomes the new `current`. Flow conditions read `previous`, which
12398
+ * structurally excludes the placement that is launching right now (the PPO
12399
+ * requirement): the first launch in a session leaves `previous` undefined.
12400
+ *
12401
+ * State is in-memory only and cleared via `reset()` (wired into `Nami.reset()`).
12402
+ */
12403
+ class LastLaunchStore {
12404
+ static get shared() {
12405
+ if (!this._shared)
12406
+ this._shared = new LastLaunchStore();
12407
+ return this._shared;
12408
+ }
12409
+ /** The Last Launch — the launch immediately before the current one. */
12410
+ get previous() {
12411
+ return this._previous;
12412
+ }
12413
+ /** Demote `current` → `previous`, then set `current` to `rec`. */
12414
+ record(rec) {
12415
+ this._previous = this._current;
12416
+ this._current = rec;
12417
+ }
12418
+ /** Clear both slots. */
12419
+ reset() {
12420
+ this._current = undefined;
12421
+ this._previous = undefined;
12422
+ }
12423
+ }
12424
+
12393
12425
  var _Nami_isInitialized;
12394
12426
  // NamiFlowManager is intentionally NOT imported at top level — it
12395
12427
  // transitively imports back to this module (`Nami` for logging), and
@@ -12430,6 +12462,7 @@ class Nami {
12430
12462
  PaywallState.reset();
12431
12463
  NamiAPI.reset();
12432
12464
  CampaignRuleRepository.instance.reset();
12465
+ LastLaunchStore.shared.reset();
12433
12466
  // Lazy import to avoid a load-order cycle (NamiFlowManager imports
12434
12467
  // back to this module for `Nami.instance.maxLogging`).
12435
12468
  const { NamiFlowManager } = await Promise.resolve().then(function () { return NamiFlowManager$3; });
@@ -12730,6 +12763,7 @@ const FilterOperator = {
12730
12763
  NOT_EQUALS: 'not_equals',
12731
12764
  NOT_I_EQUALS: 'not_i_equals',
12732
12765
  NOT_CONTAINS: 'not_contains',
12766
+ CONTAINS: 'contains',
12733
12767
  SET: 'set',
12734
12768
  NOT_SET: 'not_set',
12735
12769
  GREATER_THAN: 'greater_than',
@@ -12862,14 +12896,24 @@ class NamiConditionEvaluator {
12862
12896
  case FilterOperator.NOT_I_EQUALS: {
12863
12897
  return filter.values.every(expected => !this.strictEquals(resolvedValue, expected, true));
12864
12898
  }
12899
+ case FilterOperator.CONTAINS: {
12900
+ // Array (tags): ANY-match by exact membership. String: case-sensitive substring.
12901
+ // `includes` covers both (Array.prototype.includes / String.prototype.includes).
12902
+ if (Array.isArray(resolvedValue) || typeof resolvedValue === 'string') {
12903
+ return filter.values.some(expected => typeof expected === 'string' && resolvedValue.includes(expected));
12904
+ }
12905
+ return false;
12906
+ }
12865
12907
  case FilterOperator.NOT_CONTAINS: {
12866
- const result = filter.values.every(expected => {
12908
+ if (Array.isArray(resolvedValue)) {
12909
+ return filter.values.every(expected => !(typeof expected === 'string' && resolvedValue.includes(expected)));
12910
+ }
12911
+ return filter.values.every(expected => {
12867
12912
  if (typeof resolvedValue === 'string' && typeof expected === 'string') {
12868
12913
  return !resolvedValue.includes(expected);
12869
12914
  }
12870
12915
  return true;
12871
12916
  });
12872
- return result;
12873
12917
  }
12874
12918
  case FilterOperator.GREATER_THAN:
12875
12919
  case FilterOperator.GREATER_THAN_OR_EQUAL_TO:
@@ -13014,14 +13058,22 @@ class BaseNamespaceResolver {
13014
13058
 
13015
13059
  // import { logger } from "../../services/logger.service";
13016
13060
  class LaunchContextResolver extends BaseNamespaceResolver {
13017
- constructor(context) {
13061
+ constructor(context, campaign) {
13018
13062
  super();
13019
13063
  this.namespace = 'LaunchContext';
13020
13064
  this.context = context;
13065
+ this.campaign = campaign;
13021
13066
  this.register();
13022
13067
  }
13023
13068
  resolveValue(keyPath) {
13024
13069
  // logger.debug('[LaunchContextResolver]', 'Resolving keyPath', keyPath);
13070
+ // Current-launch placement metadata sourced from the launching campaign.
13071
+ if (keyPath === 'placement') {
13072
+ return this.campaign?.value ?? undefined;
13073
+ }
13074
+ if (keyPath === 'tags') {
13075
+ return this.campaign?.placement_tags ?? undefined;
13076
+ }
13025
13077
  if (keyPath.startsWith('customAttributes.')) {
13026
13078
  const innerPath = keyPath.substring('customAttributes.'.length);
13027
13079
  return this.resolveKeyPath(innerPath, this.context.customAttributes);
@@ -13444,6 +13496,50 @@ class PlacementLabelResolver extends BaseNamespaceResolver {
13444
13496
  }
13445
13497
  }
13446
13498
 
13499
+ /**
13500
+ * Resolves flow conditions against the *previous* launch (the "Last Launch"),
13501
+ * exposed under the `LastLaunchContext` namespace (NAM-1937):
13502
+ *
13503
+ * - `LastLaunchContext.placement` → previous launch's placement label
13504
+ * - `LastLaunchContext.tags` → previous launch's placement tags
13505
+ * - `LastLaunchContext.customAttributes.<k>` → a custom attribute from the previous launch
13506
+ *
13507
+ * When there is no previous launch (first launch of the session), every key
13508
+ * resolves to `undefined`. The condition evaluator's existing null-state
13509
+ * handling then makes `equals`/`set` false and `not_set` true — so the
13510
+ * resolver deliberately does not special-case any operator.
13511
+ */
13512
+ class LastLaunchContextResolver extends BaseNamespaceResolver {
13513
+ constructor() {
13514
+ super(...arguments);
13515
+ this.namespace = "LastLaunchContext";
13516
+ }
13517
+ // Exposed publicly so callers register explicitly (consistent with how this
13518
+ // resolver is wired from NamiFlow and exercised in tests). Unlike the other
13519
+ // resolvers it does not auto-register from its constructor because it reads
13520
+ // live store state rather than capturing per-launch context.
13521
+ register() {
13522
+ super.register();
13523
+ }
13524
+ resolveValue(keyPath) {
13525
+ const previous = LastLaunchStore.shared.previous;
13526
+ if (!previous) {
13527
+ return undefined;
13528
+ }
13529
+ if (keyPath === "placement") {
13530
+ return previous.placementLabel;
13531
+ }
13532
+ if (keyPath === "tags") {
13533
+ return previous.tags;
13534
+ }
13535
+ if (keyPath.startsWith("customAttributes.")) {
13536
+ const innerPath = keyPath.substring("customAttributes.".length);
13537
+ return this.resolveKeyPath(innerPath, previous.customAttributes);
13538
+ }
13539
+ return undefined;
13540
+ }
13541
+ }
13542
+
13447
13543
  /**
13448
13544
  * Represents the possible account actions states that can be returned by callback registered from
13449
13545
  * [NamiCustomerManager.registerAccountStateHandler]
@@ -14747,7 +14843,7 @@ class NamiFlow extends BasicNamiFlow {
14747
14843
  applyLaunchContextAttributes(attrs) {
14748
14844
  if (!this.context) {
14749
14845
  this.context = { customAttributes: {} };
14750
- new LaunchContextResolver(this.context);
14846
+ new LaunchContextResolver(this.context, this.campaign);
14751
14847
  }
14752
14848
  // Guard against a runtime-supplied context that omits customAttributes
14753
14849
  // (the field is required by the type but may be absent in untyped JSON).
@@ -14757,12 +14853,13 @@ class NamiFlow extends BasicNamiFlow {
14757
14853
  }
14758
14854
  registerResolvers(context) {
14759
14855
  if (context) {
14760
- new LaunchContextResolver(context);
14856
+ new LaunchContextResolver(context, this.campaign);
14761
14857
  }
14762
14858
  new DeviceResolver();
14763
14859
  new URLParamsResolver();
14764
14860
  new FormStateResolver();
14765
14861
  new PlacementLabelResolver(this.campaign);
14862
+ new LastLaunchContextResolver().register();
14766
14863
  NamiConditionEvaluator.shared.registerNamespaceResolver('Flow', (identifier) => {
14767
14864
  switch (identifier) {
14768
14865
  case 'Flow.stepcrumbs':
@@ -15615,7 +15712,11 @@ let NamiCampaignManager$2 = class NamiCampaignManager {
15615
15712
  const data = startupTelemetry.firstLaunchLookup(value, () => getPaywallDataFromLabel(value, type));
15616
15713
  let paywall = data.paywall;
15617
15714
  const campaign = data.campaign;
15618
- if (!campaign || (!paywall && !campaign.flow)) {
15715
+ // Captured before the degenerate branch below clobbers `paywall` to `{}`,
15716
+ // so a not-found launch is correctly distinguishable from a real one when
15717
+ // deciding whether to record into LastLaunchStore (NAM-1937).
15718
+ const campaignNotFound = !campaign || (!paywall && !campaign.flow);
15719
+ if (campaignNotFound) {
15619
15720
  let error;
15620
15721
  if (!label && !withUrl) {
15621
15722
  error = LaunchCampaignError.DEFAULT_CAMPAIGN_NOT_FOUND;
@@ -15666,6 +15767,23 @@ let NamiCampaignManager$2 = class NamiCampaignManager {
15666
15767
  resultCallback(false, LaunchCampaignError.FLOW_SCREEN_DATA_UNAVILABLE);
15667
15768
  throw new FlowScreensNotAvailableError();
15668
15769
  }
15770
+ // Record this launch into the two-slot Last Launch store BEFORE the flow
15771
+ // resolvers read it. The just-launched placement becomes `current`; the
15772
+ // prior launch is demoted to `previous`, which flow conditions read via
15773
+ // the LastLaunchContext namespace (NAM-1937).
15774
+ //
15775
+ // Only record when a real placement is actually being launched. The
15776
+ // not-found/degenerate path above sets `paywall = {}` and falls through
15777
+ // (the throw is commented out); recording there would store a bogus
15778
+ // failed-launch record AND demote the real `current`, breaking the
15779
+ // two-slot/PPO guarantee for the next real launch.
15780
+ if (!campaignNotFound) {
15781
+ LastLaunchStore.shared.record({
15782
+ placementLabel: campaign?.value ?? value ?? "",
15783
+ tags: campaign?.placement_tags ?? [],
15784
+ customAttributes: (context?.customAttributes ?? {}),
15785
+ });
15786
+ }
15669
15787
  const component = getPlatformAdapters().ui.createPaywall(type, value, context);
15670
15788
  // Generate and store launch ID for successful campaign launch
15671
15789
  const launchId = generateUUID();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namiml/sdk-core",
3
- "version": "3.4.4-dev.202606302245",
3
+ "version": "3.4.4-dev.202606302327",
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",