@namiml/sdk-core 3.4.4-dev.202606241934 → 3.4.4-dev.202606241952

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.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.202606241934",
99
+ NAMI_SDK_PACKAGE_VERSION = "3.4.4-dev.202606241952",
100
100
  // environments
101
101
  PRODUCTION = "production", DEVELOPMENT = "development",
102
102
  // error messages
@@ -6984,8 +6984,21 @@ class NamiAPI {
6984
6984
  const resp = await this.requestBodyAPI(path, options, "POST", true);
6985
6985
  if (resp.id) {
6986
6986
  storageService.setLastImpressionId(resp.id);
6987
+ // Notify the flow-completion engine (no-op unless a flow is open) so it
6988
+ // can bind this impression to the current screen's completion context.
6989
+ NamiAPI.impressionListener?.(resp.id);
6987
6990
  }
6988
6991
  }
6992
+ /**
6993
+ * Reports a flow completion result against a prior impression
6994
+ * (NAM-1545 / Flow Completion Step). UPDATE-only on the server; the
6995
+ * impression must already exist. `keepalive` so a dismiss-triggered PATCH
6996
+ * survives page unload on web.
6997
+ */
6998
+ async patchImpressionCompletion(impressionId, completion_result, completion_result_date) {
6999
+ const path = `device/${this.deviceID}/impression/${impressionId}/`;
7000
+ await this.requestBodyAPI(path, { completion_result, completion_result_date }, "PATCH", true);
7001
+ }
6989
7002
  async postConversion(options) {
6990
7003
  const path = `device/${this.deviceID}/transaction/`;
6991
7004
  await this.requestBodyAPI(path, options, "POST", true);
@@ -7024,6 +7037,9 @@ __decorate([
7024
7037
  __decorate([
7025
7038
  requireDeviceID
7026
7039
  ], NamiAPI.prototype, "postImpression", null);
7040
+ __decorate([
7041
+ requireDeviceID
7042
+ ], NamiAPI.prototype, "patchImpressionCompletion", null);
7027
7043
  __decorate([
7028
7044
  requireDeviceID
7029
7045
  ], NamiAPI.prototype, "postConversion", null);
@@ -12621,6 +12637,70 @@ var NamiFlowActionFunction;
12621
12637
  const HandoffTag = {
12622
12638
  SEQUENCE: '__handoff_sequence__',
12623
12639
  };
12640
+ /**
12641
+ * Prefix marking SDK-reserved handoff tags. Reserved tags are consumed
12642
+ * internally and are NEVER delivered to host-registered handoff handlers.
12643
+ */
12644
+ const RESERVED_HANDOFF_TAG_PREFIX = '__';
12645
+ /**
12646
+ * Customer-facing handoff tag vocabulary.
12647
+ *
12648
+ * Keep in sync with `FlowHandoffTag` in nami-platform
12649
+ * `services/frontend/src/api/types/flow.types.tsx` (minus the `__*__`
12650
+ * internal tags, which never reach host handlers). The builder constrains
12651
+ * authoring to this set, but the vocabulary evolves — treat unknown tags as
12652
+ * pass-through, never as errors.
12653
+ *
12654
+ * Note `Tracking` is the legacy `att` authoring convention; the permission
12655
+ * enum value is `tracking` and the completion wire value is
12656
+ * `tracking_authorized`. Aliasing is intentional — do not rename.
12657
+ */
12658
+ const NamiHandoffTag = {
12659
+ Push: 'push',
12660
+ Location: 'location',
12661
+ Tracking: 'att',
12662
+ Deeplink: 'deeplink',
12663
+ Complete: 'complete',
12664
+ SignIn: 'signin',
12665
+ Restore: 'restore',
12666
+ SignInMvpd: 'signin_mvpd',
12667
+ BuySku: 'buysku',
12668
+ UserData: 'userdata',
12669
+ };
12670
+ /**
12671
+ * Permission types reportable through a handoff outcome.
12672
+ *
12673
+ * Tier 1 (`push`, `location`, `tracking`) map to impression
12674
+ * `completion_result` wire values; the remaining tier-2 types are accepted
12675
+ * (flow state + lifecycle only) and gain wire values via backend tickets
12676
+ * without SDK API churn.
12677
+ */
12678
+ const NamiPermissionOutcome = {
12679
+ Push: 'push',
12680
+ Location: 'location',
12681
+ Tracking: 'tracking',
12682
+ Camera: 'camera',
12683
+ Microphone: 'microphone',
12684
+ Photos: 'photos',
12685
+ Contacts: 'contacts',
12686
+ Bluetooth: 'bluetooth',
12687
+ Calendar: 'calendar',
12688
+ Biometrics: 'biometrics',
12689
+ Health: 'health',
12690
+ Motion: 'motion',
12691
+ Speech: 'speech',
12692
+ LocalNetwork: 'localNetwork',
12693
+ };
12694
+ /**
12695
+ * Tier-1 permission → impression completion_result wire value. Keep in sync
12696
+ * with omaha `Impression.CompletionResult` / breakwater
12697
+ * `ValidCompletionResults`.
12698
+ */
12699
+ const COMPLETION_MAPPED_PERMISSIONS = {
12700
+ [NamiPermissionOutcome.Push]: 'push_authorized',
12701
+ [NamiPermissionOutcome.Location]: 'location_authorized',
12702
+ [NamiPermissionOutcome.Tracking]: 'tracking_authorized',
12703
+ };
12624
12704
  const NamiReservedActions = {
12625
12705
  BUY_SKU: '__buy_sku__',
12626
12706
  DEFAULT: '__default__',
@@ -12635,6 +12715,7 @@ const NamiReservedActions = {
12635
12715
  RESUME: '__resume__',
12636
12716
  REMOTE_BACK: '__remoteback__',
12637
12717
  TAG_UPDATE: '__tag_update__',
12718
+ PERMISSION_GRANTED: '__permission_granted__',
12638
12719
  };
12639
12720
 
12640
12721
  const LogicalOperator = {
@@ -13383,6 +13464,198 @@ var AccountStateAction;
13383
13464
  AccountStateAction["NAMI_DEVICE_ID_CLEARED"] = "nami_device_id_cleared";
13384
13465
  })(AccountStateAction || (AccountStateAction = {}));
13385
13466
 
13467
+ const postConversion = async (transactionInfo) => {
13468
+ if (isAnonymousMode()) {
13469
+ logger.info("Skipping transaction post - anonymous mode");
13470
+ return;
13471
+ }
13472
+ try {
13473
+ await NamiAPI.instance.postConversion(transactionInfo);
13474
+ }
13475
+ catch (e) {
13476
+ logger.error("Error posting conversion", e);
13477
+ }
13478
+ };
13479
+
13480
+ /**
13481
+ * @class NamiPaywallManager
13482
+ * Provides methods for managing all aspects of a paywall in the Nami SDK.
13483
+ */
13484
+ let NamiPaywallManager$2 = class NamiPaywallManager {
13485
+ constructor() {
13486
+ this.emitter = NamiEventEmitter.getInstance();
13487
+ }
13488
+ /**
13489
+ * @returns {IPaywall[]} a list of Paywall
13490
+ */
13491
+ static allPaywalls() {
13492
+ if (!this.instance.sdkInitialized) {
13493
+ throw new SDKNotInitializedError();
13494
+ }
13495
+ return allPaywalls();
13496
+ }
13497
+ /**
13498
+ * Used to set product details when store products are unavailable. For advanced use cases only.
13499
+ */
13500
+ static setProductDetails(productDetails) {
13501
+ this.instance.productDetails = productDetails;
13502
+ PaywallState.setProductDetails(productDetails);
13503
+ }
13504
+ /**
13505
+ * Register a callback which would be invoked when user will sign-in
13506
+ */
13507
+ static registerSignInHandler(handler) {
13508
+ this.instance.emitter.addListener(PaywallManagerEvents.SignIn, handler);
13509
+ return () => {
13510
+ this.instance.emitter.removeListener(PaywallManagerEvents.SignIn, handler);
13511
+ };
13512
+ }
13513
+ /**
13514
+ * Register a callback which would be invoked when user close a paywall raised by Nami system
13515
+ */
13516
+ static registerCloseHandler(handler) {
13517
+ this.instance.emitter.addListener(PaywallManagerEvents.Close, handler);
13518
+ return () => {
13519
+ this.instance.emitter.removeListener(PaywallManagerEvents.Close, handler);
13520
+ };
13521
+ }
13522
+ /**
13523
+ * Register a callback which would be invoked when user will take action on deeplink
13524
+ */
13525
+ static registerDeeplinkActionHandler(handler) {
13526
+ this.instance.emitter.addListener(PaywallManagerEvents.DeeplinkAction, handler);
13527
+ return () => {
13528
+ this.instance.emitter.removeListener(PaywallManagerEvents.DeeplinkAction, handler);
13529
+ };
13530
+ }
13531
+ /**
13532
+ * Register a [NamiBuySkuHandler] which would be invoked when a user triggers
13533
+ * a buy sku action on a paywall.
13534
+ *
13535
+ * Only available for plans where Nami is not handling subscription & IAP management.
13536
+ */
13537
+ static registerBuySkuHandler(handler) {
13538
+ if (hasPurchaseManagement()) {
13539
+ logger.debug("This handler will not be invoked because this account's plan includes " +
13540
+ "built-in purchase management. Contact support@namiml.com for details.");
13541
+ }
13542
+ this.instance.emitter.addListener(PaywallManagerEvents.BuySku, handler);
13543
+ return () => {
13544
+ this.instance.emitter.removeListener(PaywallManagerEvents.BuySku, handler);
13545
+ };
13546
+ }
13547
+ /**
13548
+ * Register a callback which would be invoked when user restore a product
13549
+ */
13550
+ static registerRestoreHandler(handler) {
13551
+ this.instance.emitter.addListener(PaywallManagerEvents.Restore, handler);
13552
+ return () => {
13553
+ this.instance.emitter.removeListener(PaywallManagerEvents.Restore, handler);
13554
+ };
13555
+ }
13556
+ /**
13557
+ * Notify the NamiPaywallManager that a purchase initiated from the
13558
+ * [NamiBuySkuHandler] is complete.
13559
+ *
13560
+ * Only available for plans where Nami is not handling subscription & IAP management.
13561
+ *
13562
+ * @returns {Promise<void>} A Promise that resolves when buying SKU will be complete.
13563
+ */
13564
+ static async buySkuComplete(purchase) {
13565
+ // clear loading indicator
13566
+ PaywallState.setPurchaseInProgress(false);
13567
+ if (NamiFlowManager$2.instance.flowOpen) {
13568
+ const flow = NamiFlowManager$2.instance.currentFlow;
13569
+ const currentStep = flow?.currentFlowStep;
13570
+ if (flow && currentStep) {
13571
+ flow.executeLifecycle(currentStep, NamiReservedActions.PURCHASE_SUCCESS);
13572
+ }
13573
+ }
13574
+ if (NamiCustomerManager$2.inAnonymousMode()) {
13575
+ logger.debug("Skipping purchase validation - anonymous mode");
13576
+ return;
13577
+ }
13578
+ const session = storageService.getSessionId();
13579
+ const impression = storageService.getPurchaseImpression();
13580
+ if (!impression) {
13581
+ logger.debug("Not sending paywall conversion call complete due to unknown impression");
13582
+ return;
13583
+ }
13584
+ // send conversion
13585
+ try {
13586
+ await postConversion({
13587
+ purchase_env: "production",
13588
+ app_env: "production",
13589
+ impression,
13590
+ session,
13591
+ amount: purchase.amount,
13592
+ currency: purchase.currency,
13593
+ transaction_id: purchase.transactionId,
13594
+ sku: purchase.skuId,
13595
+ });
13596
+ }
13597
+ catch (e) {
13598
+ logger.error('Error posting conversion', e);
13599
+ }
13600
+ // validate purchase
13601
+ if (!hasCapability(Capabilities.THIRD_PARTY_TRANSACTIONS)) {
13602
+ try {
13603
+ await NamiAPI.instance.purchaseValidation({
13604
+ app_env: "production",
13605
+ payload: '', // token to be added when here a direct integration is done
13606
+ sku: purchase.skuId,
13607
+ });
13608
+ }
13609
+ catch (e) {
13610
+ logger.error("Error validating purchase", e);
13611
+ }
13612
+ }
13613
+ }
13614
+ /**
13615
+ * Notify the NamiPaywallManager that purchase flow handled by you is cancelled.
13616
+ * Used to disable product purchase-in-progress loading indicators
13617
+ */
13618
+ static buySkuCancel() {
13619
+ if (!hasCapability(Capabilities.THIRD_PARTY_TRANSACTIONS)) {
13620
+ logger.warn("Call this method is not needed for this app because Nami is responsible for purchase management. Contact support@nami.ml for details.");
13621
+ return;
13622
+ }
13623
+ PaywallState.setPurchase(false);
13624
+ }
13625
+ /**
13626
+ * Set the video details for the app supplied video
13627
+ * @param url The URL of the video
13628
+ * @param name The name of the video
13629
+ */
13630
+ static setAppSuppliedVideoDetails(url, name) {
13631
+ if (isValidUrl(url)) {
13632
+ logger.debug(`NamiPaywallManager: setAppSuppliedVideoDetails received valid url ${url}`);
13633
+ PaywallState.setAppSuppliedVideoDetails({ url, name });
13634
+ }
13635
+ else {
13636
+ logger.debug(`NamiPaywallManager: setAppSuppliedVideoDetails received invalid url ${url}`);
13637
+ }
13638
+ }
13639
+ /**
13640
+ * Enables or disables user interaction on Nami paywalls.
13641
+ *
13642
+ * @param {boolean} allowed - Whether user interaction should be allowed.
13643
+ * @returns {void}
13644
+ */
13645
+ static allowUserInteraction(allowed) {
13646
+ logger.debug(`NamiPaywallManager: allowUserInteraction called with ${allowed}`);
13647
+ PaywallState.setUserInteractionEnabled(allowed);
13648
+ }
13649
+ /**
13650
+ * Private Instance Methods
13651
+ */
13652
+ get sdkInitialized() {
13653
+ return Nami.instance.isInitialized;
13654
+ }
13655
+ };
13656
+ NamiPaywallManager$2.instance = new NamiPaywallManager$2();
13657
+ NamiPaywallManager$2.emitter = NamiEventEmitter.getInstance();
13658
+
13386
13659
  let NamiFlowManager$2 = class NamiFlowManager {
13387
13660
  static get instance() {
13388
13661
  if (!NamiFlowManager._instance) {
@@ -13411,10 +13684,24 @@ let NamiFlowManager$2 = class NamiFlowManager {
13411
13684
  i.flowOpen = false;
13412
13685
  i.lastAnimatedFlowProgress = new Map();
13413
13686
  i.navGraphCache = new WeakMap();
13687
+ i.pendingHandoff = undefined;
13414
13688
  }
13415
13689
  /**
13416
13690
  * Registers a handoff step handler and returns an unsubscribe callback.
13417
13691
  *
13692
+ * The handler receives `(tag, data, complete)`. Call `complete(outcome)`
13693
+ * exactly once with a typed {@link NamiHandoffOutcome}: completing
13694
+ * reports the outcome AND resumes the flow in one call. A bare
13695
+ * `complete()` is equivalent to `complete({ kind: 'done' })`. The
13696
+ * outcome's routing (lifecycles, purchase/login machinery) runs before
13697
+ * the resume, matching the pre-outcome `buySkuComplete(); resume()`
13698
+ * call order.
13699
+ *
13700
+ * Handlers written against the old `(tag, data)` signature still run —
13701
+ * the extra argument is ignored at runtime, and their existing
13702
+ * `resume()` call degrades to `complete({kind:'done'})` with a
13703
+ * migration warning.
13704
+ *
13418
13705
  * The returned callback clears the registered handler only if it is still
13419
13706
  * the active one (identity-check guard). This protects against React 18
13420
13707
  * StrictMode dev-mode double-mount where a stale unsubscribe from the
@@ -13437,6 +13724,131 @@ let NamiFlowManager$2 = class NamiFlowManager {
13437
13724
  }
13438
13725
  };
13439
13726
  }
13727
+ /** Whether a handoff handler is registered. */
13728
+ get hasHandoffHandler() {
13729
+ return !!this.handoffStepHandler;
13730
+ }
13731
+ /**
13732
+ * Single delivery point for handoffs reaching the host app. Flow code must
13733
+ * route through here (never invoke the handler field directly) so every
13734
+ * handler receives a working `complete` and reserved-tag guarding stays
13735
+ * upstream in the dispatcher.
13736
+ */
13737
+ deliverHandoff(tag, data) {
13738
+ const handler = this.handoffStepHandler;
13739
+ if (!handler)
13740
+ return;
13741
+ const pending = { tag, completed: false };
13742
+ this.pendingHandoff = pending;
13743
+ const complete = (outcome) => {
13744
+ if (pending.completed) {
13745
+ logger.warn(`[NamiFlowManager] complete() called more than once for handoff '${tag}' — ignored`);
13746
+ return;
13747
+ }
13748
+ const current = NamiFlowManager.instance.pendingHandoff;
13749
+ if (current && current !== pending) {
13750
+ logger.warn(`[NamiFlowManager] stale complete() for handoff '${tag}' — a newer handoff ('${current.tag}') is pending; ignored`);
13751
+ return;
13752
+ }
13753
+ pending.completed = true;
13754
+ if (this.pendingHandoff === pending) {
13755
+ this.pendingHandoff = undefined;
13756
+ }
13757
+ // No argument means "nothing to report" — the done outcome.
13758
+ NamiFlowManager.routeHandoffOutcome(outcome ?? { kind: 'done' });
13759
+ NamiFlowManager.resume();
13760
+ };
13761
+ handler(tag, data, complete);
13762
+ }
13763
+ /**
13764
+ * Applies a typed handoff outcome to the SDK before the flow resumes.
13765
+ * Ordering contract: outcome routing first (its lifecycles may navigate),
13766
+ * `resume()` last — identical to the legacy integrator call order.
13767
+ */
13768
+ static routeHandoffOutcome(outcome) {
13769
+ const flow = NamiFlowManager.instance.currentFlow;
13770
+ const step = flow?.currentFlowStep;
13771
+ switch (outcome.kind) {
13772
+ case 'done':
13773
+ return;
13774
+ case 'permission': {
13775
+ if (!flow)
13776
+ return;
13777
+ flow.handoffState.permissions[outcome.permission] = outcome.granted;
13778
+ if (!outcome.granted)
13779
+ return;
13780
+ if (step)
13781
+ flow.executeLifecycle(step, NamiReservedActions.PERMISSION_GRANTED);
13782
+ const wireValue = COMPLETION_MAPPED_PERMISSIONS[outcome.permission];
13783
+ if (wireValue) {
13784
+ // Permissions have no flow lifecycle to ride, so the completion
13785
+ // signal is fired directly here (purchase/login ride
13786
+ // executeLifecycle instead — see below).
13787
+ flow.completionSignal(wireValue);
13788
+ }
13789
+ else {
13790
+ logger.debug(`[NamiFlowManager] permission '${outcome.permission}' granted — no completion_result mapping yet (state/lifecycle only)`);
13791
+ }
13792
+ return;
13793
+ }
13794
+ case 'purchase': {
13795
+ if (outcome.result === 'success') {
13796
+ // buySkuComplete fires __purchase_success__ (and the conversion)
13797
+ // itself; executeLifecycle raises the completion signal. Don't
13798
+ // double-dispatch here.
13799
+ void NamiPaywallManager$2.buySkuComplete(outcome.details);
13800
+ }
13801
+ else {
13802
+ PaywallState.setPurchase(false);
13803
+ if (flow && step)
13804
+ flow.executeLifecycle(step, NamiReservedActions.PURCHASE_FAILURE);
13805
+ }
13806
+ return;
13807
+ }
13808
+ case 'login': {
13809
+ if (outcome.result === 'success') {
13810
+ const { details } = outcome;
13811
+ if (flow) {
13812
+ // Open-ended host-asserted experience data — exposed to branch
13813
+ // conditions and smart text via the `login.*` namespace using
13814
+ // the host's own key names (e.g. login.subscriberStatus).
13815
+ flow.handoffState.login = { ...(details.attributes ?? {}) };
13816
+ }
13817
+ // Identity writes are idempotent: skip when the asserted ids
13818
+ // already match what the SDK holds, so re-asserting an existing
13819
+ // login doesn't re-run profile sync or device updates.
13820
+ if (NamiCustomerManager$2.loggedInId() === details.loginId) {
13821
+ // Already this user — login() would be a no-op network-wise,
13822
+ // but the flow still needs its __login_success__ actions
13823
+ // (login() owns that dispatch on the fresh-login path).
13824
+ if (flow && step)
13825
+ flow.executeLifecycle(step, NamiReservedActions.LOGIN_SUCCESS);
13826
+ }
13827
+ else {
13828
+ // login() fires __login_success__/__login_failure__ itself — do
13829
+ // not double-dispatch lifecycles here.
13830
+ void Promise.resolve(NamiCustomerManager$2.login(details.loginId)).catch((err) => {
13831
+ logger.error('[NamiFlowManager] login from handoff outcome failed', err);
13832
+ });
13833
+ }
13834
+ const currentCdpId = storageService.getDevice()?.customer_data_platform_id;
13835
+ if (details.cdpId && details.cdpId !== currentCdpId) {
13836
+ void Promise.resolve(NamiCustomerManager$2.setCustomerDataPlatformId(details.cdpId)).catch((err) => {
13837
+ logger.error('[NamiFlowManager] CDP id write-through from handoff outcome failed', err);
13838
+ });
13839
+ }
13840
+ // Both the fresh-login and idempotent-skip paths above dispatch
13841
+ // __login_success__ via executeLifecycle, which raises the
13842
+ // completion signal — no direct call needed here.
13843
+ }
13844
+ else {
13845
+ if (flow && step)
13846
+ flow.executeLifecycle(step, NamiReservedActions.LOGIN_FAILURE);
13847
+ }
13848
+ return;
13849
+ }
13850
+ }
13851
+ }
13440
13852
  /**
13441
13853
  * Registers an event handler and returns an unsubscribe callback.
13442
13854
  *
@@ -13462,6 +13874,14 @@ let NamiFlowManager$2 = class NamiFlowManager {
13462
13874
  if (Nami.instance.maxLogging) {
13463
13875
  logger.debug(`[NamiFlowManager] resume() — flowOpen=${flowOpen}, hasFlow=${!!currentFlow}, currentStep=${currentFlow?.currentFlowStep?.id ?? 'none'}, isPaused=${currentFlow?.isPaused ?? 'N/A'}`);
13464
13876
  }
13877
+ // A v2 handler calling bare resume() instead of complete() is treated as
13878
+ // complete({kind:'done'}) — flow continues, but nudge toward migration.
13879
+ const pending = NamiFlowManager.instance.pendingHandoff;
13880
+ if (pending && !pending.completed) {
13881
+ pending.completed = true;
13882
+ NamiFlowManager.instance.pendingHandoff = undefined;
13883
+ logger.warn(`[NamiFlowManager] resume() called during handoff '${pending.tag}' with a v2 handler registered — treated as complete({kind:'done'}); prefer complete(outcome)`);
13884
+ }
13465
13885
  if (!flowOpen || !currentFlow || !currentFlow.currentFlowStep) {
13466
13886
  logger.warn('Cannot resume: no active flow or current step');
13467
13887
  return;
@@ -13483,8 +13903,11 @@ let NamiFlowManager$2 = class NamiFlowManager {
13483
13903
  logger.debug('Cannot finish: no active flow or current step');
13484
13904
  return;
13485
13905
  }
13906
+ // Host-initiated close is a dismissal — records dismiss_page against the
13907
+ // current screen's impression (R5) before teardown (no-op if a positive
13908
+ // completion was already recorded).
13486
13909
  logger.debug(`Finishing flow programmatically at step ${currentFlow.currentFlowStep.id}`);
13487
- currentFlow.finished();
13910
+ currentFlow.dismiss();
13488
13911
  }
13489
13912
  static pause() {
13490
13913
  const { flowOpen, currentFlow } = NamiFlowManager.instance;
@@ -13506,6 +13929,13 @@ let NamiFlowManager$2 = class NamiFlowManager {
13506
13929
  const flow = new NamiFlow(campaign, paywall, this, context);
13507
13930
  this.flowOpen = true;
13508
13931
  this.currentFlow = flow;
13932
+ // Bind each impression posted for this flow's screens to the active
13933
+ // flow's per-raise completion context (NAM-1545, R1). Registered here so
13934
+ // the listener is in place before the first screen's impression posts;
13935
+ // it no-ops for non-flow impressions because currentFlow clears on finish.
13936
+ NamiAPI.impressionListener = (impressionId) => {
13937
+ NamiFlowManager.instance.currentFlow?.bindImpression(impressionId);
13938
+ };
13509
13939
  return flow;
13510
13940
  }
13511
13941
  finishFlow() {
@@ -14294,6 +14724,14 @@ class NamiFlow extends BasicNamiFlow {
14294
14724
  this.isPaused = false;
14295
14725
  this.flowPath = "";
14296
14726
  this.timerStates = {};
14727
+ /**
14728
+ * Flow-scoped state written by typed handoff outcomes (Variant D).
14729
+ * Exposed to branch conditions and smart text via the `login.*` and
14730
+ * `permissions.*` namespaces registered in {@link registerResolvers}.
14731
+ * `login` holds the host's open-ended attributes verbatim, keyed by the
14732
+ * host's own names (e.g. `login.subscriberStatus`, `login.tier`).
14733
+ */
14734
+ this.handoffState = { permissions: {} };
14297
14735
  this.campaign = campaign;
14298
14736
  this.component = paywall;
14299
14737
  this.context = context;
@@ -14335,6 +14773,17 @@ class NamiFlow extends BasicNamiFlow {
14335
14773
  return undefined;
14336
14774
  }
14337
14775
  });
14776
+ // Variant D handoff-outcome state: `login.<attributeKey>` (host-named,
14777
+ // open-ended) and `permissions.<type>` for branch conditions and smart
14778
+ // text.
14779
+ NamiConditionEvaluator.shared.registerNamespaceResolver('login', (identifier) => {
14780
+ const key = identifier.replace(/^login\./, '');
14781
+ return this.handoffState.login?.[key];
14782
+ });
14783
+ NamiConditionEvaluator.shared.registerNamespaceResolver('permissions', (identifier) => {
14784
+ const key = identifier.replace(/^permissions\./, '');
14785
+ return this.handoffState.permissions[key];
14786
+ });
14338
14787
  NamiConditionEvaluator.shared.registerNamespaceResolver('__first_session__', () => {
14339
14788
  return NamiRefs.instance.getIsFirstSession();
14340
14789
  });
@@ -14385,6 +14834,18 @@ class NamiFlow extends BasicNamiFlow {
14385
14834
  this.manager.finishFlow();
14386
14835
  // UI is responsible for closing paywall when flow ends
14387
14836
  }
14837
+ /**
14838
+ * Dismiss the flow (hard close): records `dismiss_page` against the current
14839
+ * screen's impression (R5) before tearing down. The single choke point for
14840
+ * host-initiated dismissal, the `flowDismiss` action, back-button exits,
14841
+ * and platform swipe-down dismissals — anything that ends the flow without
14842
+ * a user "skip" or a programmatic `flowDone`. Never overwrites a positive
14843
+ * completion (R4 guard inside `signalNonCompletion`).
14844
+ */
14845
+ dismiss() {
14846
+ this.signalNonCompletion('dismiss_page');
14847
+ this.finished();
14848
+ }
14388
14849
  back() {
14389
14850
  if (this.previousFlowStep?.allow_back_to === false) {
14390
14851
  logger.debug(`Not allowed to go back to ${this.previousFlowStep.id}`);
@@ -14395,12 +14856,89 @@ class NamiFlow extends BasicNamiFlow {
14395
14856
  .slice(0, -1)
14396
14857
  .some(step => step.type === 'screen');
14397
14858
  if (!hasPreviousScreen) {
14398
- this.flowLog(`back() — no previous screen step, exiting flow`);
14399
- this.finished();
14859
+ this.flowLog(`back() — no previous screen step, dismissing flow`);
14860
+ this.dismiss();
14400
14861
  return;
14401
14862
  }
14402
14863
  this.backToPreviousScreenStep();
14403
14864
  }
14865
+ // ─── Flow completion (NAM-1545) ──────────────────────────────────────────
14866
+ /**
14867
+ * Binds a freshly-posted impression id to the current screen's raise.
14868
+ * Called via `NamiAPI.impressionListener` whenever an impression posts
14869
+ * while this flow is open. Starts a fresh completion context for the raise
14870
+ * (a re-render — e.g. back-navigation — rebinds with a clean result),
14871
+ * fires `page_view` completion immediately if applicable (R2), and flushes
14872
+ * any condition signals that arrived before the id (R1).
14873
+ */
14874
+ bindImpression(impressionId) {
14875
+ const step = this.currentFlowStep;
14876
+ if (!step)
14877
+ return;
14878
+ const ctx = this.completionContext;
14879
+ if (ctx && ctx.stepId === step.id && !ctx.impressionId) {
14880
+ // A signal already created the context for this raise — fill the id.
14881
+ ctx.impressionId = impressionId;
14882
+ }
14883
+ else {
14884
+ this.completionContext = { stepId: step.id, impressionId, resultSent: false, pending: [] };
14885
+ }
14886
+ const cs = step.completion_step;
14887
+ if (cs?.enabled && cs.condition === 'page_view') {
14888
+ this.completionSignal('page_view');
14889
+ }
14890
+ const queued = this.completionContext.pending;
14891
+ this.completionContext.pending = [];
14892
+ queued.forEach((condition) => this.completionSignal(condition));
14893
+ }
14894
+ /**
14895
+ * Reports that `condition` was satisfied on the current page. PATCHes the
14896
+ * completion result only when the current step is a matching, enabled
14897
+ * completion step and no result has been sent for this impression yet.
14898
+ * Signals arriving before the impression id is bound are queued (R1).
14899
+ */
14900
+ completionSignal(condition) {
14901
+ const step = this.currentFlowStep;
14902
+ const cs = step?.completion_step;
14903
+ if (!step || !cs?.enabled || cs.condition !== condition)
14904
+ return;
14905
+ let ctx = this.completionContext;
14906
+ if (!ctx || ctx.stepId !== step.id) {
14907
+ ctx = this.completionContext = { stepId: step.id, resultSent: false, pending: [] };
14908
+ }
14909
+ if (ctx.resultSent)
14910
+ return; // R4: one completion per impression
14911
+ if (!ctx.impressionId) {
14912
+ if (!ctx.pending.includes(condition))
14913
+ ctx.pending.push(condition);
14914
+ return; // R1: flushed once the impression id binds
14915
+ }
14916
+ ctx.resultSent = true;
14917
+ void this.patchCompletion(ctx.impressionId, condition);
14918
+ }
14919
+ /**
14920
+ * Records a non-completion outcome (`skip_page` / `dismiss_page`) against
14921
+ * the current page's impression when the flow terminates without
14922
+ * completing (R5 — feeds the flow-conversion cube). Applies to any page,
14923
+ * completion-step or not, but never overwrites a positive result (R4) and
14924
+ * no-ops when no impression is bound (e.g. anonymous mode).
14925
+ */
14926
+ signalNonCompletion(result) {
14927
+ const ctx = this.completionContext;
14928
+ if (!ctx || !ctx.impressionId || ctx.resultSent)
14929
+ return;
14930
+ ctx.resultSent = true;
14931
+ void this.patchCompletion(ctx.impressionId, result);
14932
+ }
14933
+ async patchCompletion(impressionId, result) {
14934
+ this.flowLog(`patchCompletion(${result}) — impression=${impressionId}, step=${this.currentFlowStep?.id}`);
14935
+ try {
14936
+ await NamiAPI.instance.patchImpressionCompletion(impressionId, result, new Date().toISOString());
14937
+ }
14938
+ catch (err) {
14939
+ logger.error(`[NamiFlow] completion PATCH failed for '${result}'`, err);
14940
+ }
14941
+ }
14404
14942
  next() {
14405
14943
  const nextStep = this.nextFlowStep;
14406
14944
  if (nextStep) {
@@ -14485,6 +15023,17 @@ class NamiFlow extends BasicNamiFlow {
14485
15023
  this.forward(resumeId);
14486
15024
  }
14487
15025
  executeLifecycle(step, key, data) {
15026
+ // Completion signals ride the lifecycle dispatch so every path that
15027
+ // raises these events — SDK-managed purchase, host-managed purchase,
15028
+ // login (fresh + idempotent-skip), buffered replay — funnels through one
15029
+ // site (NAM-1545). Fired before the no-actions early-return: completion
15030
+ // is about the event happening, not about the step defining flow actions.
15031
+ if (key === NamiReservedActions.PURCHASE_SUCCESS) {
15032
+ this.completionSignal('purchase_success');
15033
+ }
15034
+ else if (key === NamiReservedActions.LOGIN_SUCCESS) {
15035
+ this.completionSignal('login_success');
15036
+ }
14488
15037
  const lifecycles = step.actions[key];
14489
15038
  if (!lifecycles) {
14490
15039
  this.flowLog(`executeLifecycle(${key}) — no actions on step ${step.id}`);
@@ -14631,10 +15180,18 @@ class NamiFlow extends BasicNamiFlow {
14631
15180
  }
14632
15181
  break;
14633
15182
  case NamiFlowActionFunction.FINISHED:
14634
- case NamiFlowActionFunction.DISMISS:
15183
+ // "Navigate to end of flow" — not a dismissal, not a completion;
15184
+ // no completion result is recorded (R5).
14635
15185
  this.finished();
14636
15186
  break;
15187
+ case NamiFlowActionFunction.DISMISS:
15188
+ this.dismiss();
15189
+ break;
14637
15190
  case NamiFlowActionFunction.EXIT: {
15191
+ // User skip / exit-to-end-of-flow → skip_page (R5). Signalled here,
15192
+ // not at the exit step, because routing through the exit step's own
15193
+ // flowDone would otherwise overwrite the skip intent.
15194
+ this.signalNonCompletion('skip_page');
14638
15195
  const exit = this.steps.find((step) => step.type === NamiFlowStepType.EXIT);
14639
15196
  if (exit)
14640
15197
  this.forward(exit.id);
@@ -14646,34 +15203,58 @@ class NamiFlow extends BasicNamiFlow {
14646
15203
  case NamiFlowActionFunction.RESUME:
14647
15204
  this.resumeFromPause();
14648
15205
  break;
14649
- case NamiFlowActionFunction.HANDOFF:
14650
- if (action.parameters?.handoffTag) {
14651
- if (!this.manager.handoffStepHandler) {
14652
- logger.debug('No handoffStepHandler found', action);
14653
- }
14654
- else if (action.parameters.handoffData) {
14655
- logger.debug(`Invoking handoffStepHandler with ${action.parameters.handoffTag} ${JSON.stringify(action.parameters.handoffData)}`);
14656
- this.manager.handoffStepHandler(action.parameters.handoffTag, action.parameters.handoffData);
14657
- }
14658
- else if (data) {
14659
- logger.debug(`Invoking handoffStepHandler with ${action.parameters.handoffTag} ${JSON.stringify(data)}`);
14660
- this.manager.handoffStepHandler(action.parameters.handoffTag, data);
14661
- }
14662
- else if (action.parameters.handoffFormId) {
14663
- const formData = this.getFormData();
14664
- if (action.parameters.handoffTag === HandoffTag.SEQUENCE) {
14665
- this.flowHandoffFormSequence();
14666
- }
14667
- else {
14668
- this.manager.handoffStepHandler(action.parameters.handoffTag, formData);
14669
- }
15206
+ case NamiFlowActionFunction.HANDOFF: {
15207
+ const tag = action.parameters?.handoffTag;
15208
+ if (!tag)
15209
+ break;
15210
+ // Form-commit choke point: a flowHandoff carrying a form id IS the
15211
+ // form submit (R3). Fired before the reserved-tag early-out so the
15212
+ // `__handoff_sequence__` form path signals too; the condition guard
15213
+ // means it only completes on a `form_submitted` page (the push POC
15214
+ // also carries a form id — harmless, its condition is push_authorized).
15215
+ if (action.parameters?.handoffFormId) {
15216
+ this.completionSignal('form_submitted');
15217
+ }
15218
+ // Reserved tags are SDK-internal and must NEVER reach the host
15219
+ // handler checked FIRST, before any payload branching, so a
15220
+ // sequence action that also carries handoffData cannot leak
15221
+ // (NAM-1556).
15222
+ if (tag.startsWith(RESERVED_HANDOFF_TAG_PREFIX)) {
15223
+ if (tag === HandoffTag.SEQUENCE && action.parameters?.handoffFormId) {
15224
+ this.flowHandoffFormSequence();
14670
15225
  }
14671
15226
  else {
14672
- logger.debug(`Invoking handoffStepHandler with ${action.parameters.handoffTag} and no data`);
14673
- this.manager.handoffStepHandler(action.parameters.handoffTag);
15227
+ logger.warn(`Ignoring reserved handoff tag '${tag}' resuming flow`);
15228
+ this.manager.resume();
14674
15229
  }
15230
+ break;
14675
15231
  }
15232
+ if (!this.manager.hasHandoffHandler) {
15233
+ logger.debug('No handoffStepHandler found', action);
15234
+ break;
15235
+ }
15236
+ // userdata is an outbound data delivery: always a structured
15237
+ // envelope, never the bare form dict.
15238
+ if (tag === NamiHandoffTag.UserData && action.parameters?.handoffFormId) {
15239
+ this.manager.deliverHandoff(tag, this.buildUserDataEnvelope(action.parameters.handoffFormId));
15240
+ break;
15241
+ }
15242
+ // Legacy payload precedence, unchanged: embedded handoffData →
15243
+ // trigger data → form data (when a form id is present) → none.
15244
+ let payload;
15245
+ if (action.parameters?.handoffData) {
15246
+ payload = action.parameters.handoffData;
15247
+ }
15248
+ else if (data) {
15249
+ payload = data;
15250
+ }
15251
+ else if (action.parameters?.handoffFormId) {
15252
+ payload = this.getFormData();
15253
+ }
15254
+ logger.debug(`Invoking handoff handler with ${tag}${payload ? ` ${JSON.stringify(payload)}` : ' and no data'}`);
15255
+ this.manager.deliverHandoff(tag, payload);
14676
15256
  break;
15257
+ }
14677
15258
  case NamiFlowActionFunction.LOG:
14678
15259
  if (action.parameters?.eventName) {
14679
15260
  logger.info(`logEvent → ${action.parameters.eventName}`);
@@ -14744,9 +15325,19 @@ class NamiFlow extends BasicNamiFlow {
14744
15325
  }
14745
15326
  break;
14746
15327
  case NamiFlowActionFunction.SET_TAGS_FROM_FORM: {
15328
+ // Writes captured form values to customer attributes (Apple
15329
+ // parity: skip null / empty-string, KEEP false / 0 — enforced by
15330
+ // formTagValue). Optional `include` lists the only field keys
15331
+ // retained — the orthogonal half of userdata delivery: everything is
15332
+ // delivered to the host, only explicitly routed fields are retained
15333
+ // by Nami.
15334
+ const include = action.parameters?.include;
15335
+ const includeSet = Array.isArray(include) ? new Set(include) : undefined;
14747
15336
  const formData = this.getFormData();
14748
15337
  const tags = {};
14749
15338
  Object.entries(formData).forEach(([key, raw]) => {
15339
+ if (includeSet && !includeSet.has(key))
15340
+ return;
14750
15341
  const value = formTagValue(raw);
14751
15342
  if (value === undefined) {
14752
15343
  return; // skip null / "" (false and 0 are kept by formTagValue)
@@ -14858,6 +15449,19 @@ class NamiFlow extends BasicNamiFlow {
14858
15449
  getFormData() {
14859
15450
  return { ...PaywallState.currentProvider?.state?.formStates ?? {} };
14860
15451
  }
15452
+ /**
15453
+ * Structured payload for `userdata` handoffs: Nami delivering the host
15454
+ * app the data the user provided in a Nami-rendered form. Fields ride
15455
+ * verbatim; the customer owns interpretation.
15456
+ */
15457
+ buildUserDataEnvelope(formId) {
15458
+ return {
15459
+ form_id: formId,
15460
+ step_id: this.currentFlowStep?.id ?? '',
15461
+ collected_at: new Date().toISOString(),
15462
+ fields: this.getFormData(),
15463
+ };
15464
+ }
14861
15465
  flowHandoffFormSequence() {
14862
15466
  const formStates = this.currentScreenState?.formStates;
14863
15467
  if (!formStates) {
@@ -14900,8 +15504,9 @@ class NamiFlow extends BasicNamiFlow {
14900
15504
  logger.debug(`Starting handoff for ${nextKey} → ${String(value)}`);
14901
15505
  // persist updated sequence (remainingKeys mutated)
14902
15506
  this.activeHandoffSequence = sequence;
14903
- const handoffTag = `${nextKey}`;
14904
- this.manager.handoffStepHandler?.(handoffTag, undefined);
15507
+ // Route through the manager's single delivery point so v2 handlers get
15508
+ // a working complete() for sequence-key handoffs too.
15509
+ this.manager.deliverHandoff(`${nextKey}`, undefined);
14905
15510
  }
14906
15511
  }
14907
15512
 
@@ -15249,198 +15854,6 @@ const NamiEntitlementManager$1 = NamiEntitlementManager$2;
15249
15854
  // `src/managers/campaign.ts` and NAM-1207.
15250
15855
  const NamiFlowManager$1 = NamiFlowManager$2;
15251
15856
 
15252
- const postConversion = async (transactionInfo) => {
15253
- if (isAnonymousMode()) {
15254
- logger.info("Skipping transaction post - anonymous mode");
15255
- return;
15256
- }
15257
- try {
15258
- await NamiAPI.instance.postConversion(transactionInfo);
15259
- }
15260
- catch (e) {
15261
- logger.error("Error posting conversion", e);
15262
- }
15263
- };
15264
-
15265
- /**
15266
- * @class NamiPaywallManager
15267
- * Provides methods for managing all aspects of a paywall in the Nami SDK.
15268
- */
15269
- let NamiPaywallManager$2 = class NamiPaywallManager {
15270
- constructor() {
15271
- this.emitter = NamiEventEmitter.getInstance();
15272
- }
15273
- /**
15274
- * @returns {IPaywall[]} a list of Paywall
15275
- */
15276
- static allPaywalls() {
15277
- if (!this.instance.sdkInitialized) {
15278
- throw new SDKNotInitializedError();
15279
- }
15280
- return allPaywalls();
15281
- }
15282
- /**
15283
- * Used to set product details when store products are unavailable. For advanced use cases only.
15284
- */
15285
- static setProductDetails(productDetails) {
15286
- this.instance.productDetails = productDetails;
15287
- PaywallState.setProductDetails(productDetails);
15288
- }
15289
- /**
15290
- * Register a callback which would be invoked when user will sign-in
15291
- */
15292
- static registerSignInHandler(handler) {
15293
- this.instance.emitter.addListener(PaywallManagerEvents.SignIn, handler);
15294
- return () => {
15295
- this.instance.emitter.removeListener(PaywallManagerEvents.SignIn, handler);
15296
- };
15297
- }
15298
- /**
15299
- * Register a callback which would be invoked when user close a paywall raised by Nami system
15300
- */
15301
- static registerCloseHandler(handler) {
15302
- this.instance.emitter.addListener(PaywallManagerEvents.Close, handler);
15303
- return () => {
15304
- this.instance.emitter.removeListener(PaywallManagerEvents.Close, handler);
15305
- };
15306
- }
15307
- /**
15308
- * Register a callback which would be invoked when user will take action on deeplink
15309
- */
15310
- static registerDeeplinkActionHandler(handler) {
15311
- this.instance.emitter.addListener(PaywallManagerEvents.DeeplinkAction, handler);
15312
- return () => {
15313
- this.instance.emitter.removeListener(PaywallManagerEvents.DeeplinkAction, handler);
15314
- };
15315
- }
15316
- /**
15317
- * Register a [NamiBuySkuHandler] which would be invoked when a user triggers
15318
- * a buy sku action on a paywall.
15319
- *
15320
- * Only available for plans where Nami is not handling subscription & IAP management.
15321
- */
15322
- static registerBuySkuHandler(handler) {
15323
- if (hasPurchaseManagement()) {
15324
- logger.debug("This handler will not be invoked because this account's plan includes " +
15325
- "built-in purchase management. Contact support@namiml.com for details.");
15326
- }
15327
- this.instance.emitter.addListener(PaywallManagerEvents.BuySku, handler);
15328
- return () => {
15329
- this.instance.emitter.removeListener(PaywallManagerEvents.BuySku, handler);
15330
- };
15331
- }
15332
- /**
15333
- * Register a callback which would be invoked when user restore a product
15334
- */
15335
- static registerRestoreHandler(handler) {
15336
- this.instance.emitter.addListener(PaywallManagerEvents.Restore, handler);
15337
- return () => {
15338
- this.instance.emitter.removeListener(PaywallManagerEvents.Restore, handler);
15339
- };
15340
- }
15341
- /**
15342
- * Notify the NamiPaywallManager that a purchase initiated from the
15343
- * [NamiBuySkuHandler] is complete.
15344
- *
15345
- * Only available for plans where Nami is not handling subscription & IAP management.
15346
- *
15347
- * @returns {Promise<void>} A Promise that resolves when buying SKU will be complete.
15348
- */
15349
- static async buySkuComplete(purchase) {
15350
- // clear loading indicator
15351
- PaywallState.setPurchaseInProgress(false);
15352
- if (NamiFlowManager$2.instance.flowOpen) {
15353
- const flow = NamiFlowManager$2.instance.currentFlow;
15354
- const currentStep = flow?.currentFlowStep;
15355
- if (flow && currentStep) {
15356
- flow.executeLifecycle(currentStep, NamiReservedActions.PURCHASE_SUCCESS);
15357
- }
15358
- }
15359
- if (NamiCustomerManager$2.inAnonymousMode()) {
15360
- logger.debug("Skipping purchase validation - anonymous mode");
15361
- return;
15362
- }
15363
- const session = storageService.getSessionId();
15364
- const impression = storageService.getPurchaseImpression();
15365
- if (!impression) {
15366
- logger.debug("Not sending paywall conversion call complete due to unknown impression");
15367
- return;
15368
- }
15369
- // send conversion
15370
- try {
15371
- await postConversion({
15372
- purchase_env: "production",
15373
- app_env: "production",
15374
- impression,
15375
- session,
15376
- amount: purchase.amount,
15377
- currency: purchase.currency,
15378
- transaction_id: purchase.transactionId,
15379
- sku: purchase.skuId,
15380
- });
15381
- }
15382
- catch (e) {
15383
- logger.error('Error posting conversion', e);
15384
- }
15385
- // validate purchase
15386
- if (!hasCapability(Capabilities.THIRD_PARTY_TRANSACTIONS)) {
15387
- try {
15388
- await NamiAPI.instance.purchaseValidation({
15389
- app_env: "production",
15390
- payload: '', // token to be added when here a direct integration is done
15391
- sku: purchase.skuId,
15392
- });
15393
- }
15394
- catch (e) {
15395
- logger.error("Error validating purchase", e);
15396
- }
15397
- }
15398
- }
15399
- /**
15400
- * Notify the NamiPaywallManager that purchase flow handled by you is cancelled.
15401
- * Used to disable product purchase-in-progress loading indicators
15402
- */
15403
- static buySkuCancel() {
15404
- if (!hasCapability(Capabilities.THIRD_PARTY_TRANSACTIONS)) {
15405
- logger.warn("Call this method is not needed for this app because Nami is responsible for purchase management. Contact support@nami.ml for details.");
15406
- return;
15407
- }
15408
- PaywallState.setPurchase(false);
15409
- }
15410
- /**
15411
- * Set the video details for the app supplied video
15412
- * @param url The URL of the video
15413
- * @param name The name of the video
15414
- */
15415
- static setAppSuppliedVideoDetails(url, name) {
15416
- if (isValidUrl(url)) {
15417
- logger.debug(`NamiPaywallManager: setAppSuppliedVideoDetails received valid url ${url}`);
15418
- PaywallState.setAppSuppliedVideoDetails({ url, name });
15419
- }
15420
- else {
15421
- logger.debug(`NamiPaywallManager: setAppSuppliedVideoDetails received invalid url ${url}`);
15422
- }
15423
- }
15424
- /**
15425
- * Enables or disables user interaction on Nami paywalls.
15426
- *
15427
- * @param {boolean} allowed - Whether user interaction should be allowed.
15428
- * @returns {void}
15429
- */
15430
- static allowUserInteraction(allowed) {
15431
- logger.debug(`NamiPaywallManager: allowUserInteraction called with ${allowed}`);
15432
- PaywallState.setUserInteractionEnabled(allowed);
15433
- }
15434
- /**
15435
- * Private Instance Methods
15436
- */
15437
- get sdkInitialized() {
15438
- return Nami.instance.isInitialized;
15439
- }
15440
- };
15441
- NamiPaywallManager$2.instance = new NamiPaywallManager$2();
15442
- NamiPaywallManager$2.emitter = NamiEventEmitter.getInstance();
15443
-
15444
15857
  // Re-export the class itself, typed as the narrow public interface. Same
15445
15858
  // object identity as the underlying class so jest.spyOn works. See
15446
15859
  // `src/managers/campaign.ts` and NAM-1207.
@@ -65012,4 +65425,4 @@ var internal = /*#__PURE__*/Object.freeze({
65012
65425
  NamiPurchaseManager: NamiPurchaseManager
65013
65426
  });
65014
65427
 
65015
- export { ALREADY_CONFIGURED, ANONYMOUS_MODE, ANONYMOUS_MODE_ALREADY_OFF, ANONYMOUS_MODE_ALREADY_ON, ANONYMOUS_MODE_LOGIN_NOT_ALLOWED, ANONYMOUS_UUID, APIError, API_ACTIVE_ENTITLEMENTS, API_CAMPAIGN_RULES, API_CAMPAIGN_SESSION_TIMESTAMP, API_CONFIG, API_MAX_CALLS_LIMIT, API_PAYWALLS, API_PRODUCTS, API_RETRY_DELAY_SEC, API_TIMEOUT_LIMIT, API_VERSION, AUTH_DEVICE, AVAILABLE_ACTIVE_ENTITLEMENTS_CHANGED, AVAILABLE_CAMPAIGNS_CHANGED, AccountStateAction, AnonymousCDPError, AnonymousLoginError, AnonymousModeAlreadyOffError, AnonymousModeAlreadyOnError, BASE_STAGING_URL, BASE_URL, BASE_URL_PATH, BadRequestError, BasicNamiFlow, BorderMap, BorderSideMap, CAMPAIGN_NOT_AVAILABLE, CUSTOMER_ATTRIBUTES_KEY_PREFIX, CUSTOMER_JOURNEY_STATE_CHANGED, CUSTOM_HOST_PREFIX, CampaignNotAvailableError, CampaignRuleConversionEventType, CampaignRuleRepository, Capabilities, ClientError, ConfigRepository, ConflictError, CustomerJourneyRepository, DEVELOPMENT, DEVICE_API_TIMEOUT_LIMIT, DEVICE_ID_NOT_SET, DEVICE_ID_REQUIRED, DISABLE_ASYNC_LOGIN_LOGOUT, DeviceIDRequiredError, DeviceRepository, EXTENDED_CLIENT_INFO_DELIMITER, EXTENDED_CLIENT_INFO_PREFIX, EXTENDED_PLATFORM, EXTENDED_PLATFORM_VERSION, EXTERNAL_ID_REQUIRED, EntitlementRepository, EntitlementUtils, ExternalIDRequiredError, FLOW_SCREENS_NOT_AVAILABLE, FlowScreensNotAvailableError, HTML_REGEX, INITIAL_APP_CONFIG, INITIAL_CAMPAIGN_RULES, INITIAL_PAYWALLS, INITIAL_PRODUCTS, INITIAL_SESSION_COUNTER_VALUE, INITIAL_SUCCESS, InternalServerError, KEY_SESSION_COUNTER, LIQUID_VARIABLE_REGEX, LOCAL_NAMI_ENTITLEMENTS, LOG_HTTP_REQUESTS, LOG_HTTP_TRAFFIC, LaunchCampaignError, LaunchContextResolver, LogLevel, NAMI_CONFIGURATION, NAMI_CUSTOMER_JOURNEY_STATE, NAMI_LANGUAGE_CODE, NAMI_LAST_IMPRESSION_ID, NAMI_LAUNCH_ID, NAMI_PROFILE, NAMI_PURCHASE_CHANNEL, NAMI_PURCHASE_IMPRESSION_ID, NAMI_SDK_PACKAGE_VERSION, NAMI_SDK_VERSION, NAMI_SESSION_ID, NAMI_STORAGE_KEYS, Nami, NamiAPI, NamiAnimationType, NamiCampaignManager$1 as NamiCampaignManager, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager$1 as NamiCustomerManager, NamiEntitlementManager$1 as NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager$1 as NamiFlowManager, NamiFlowStepType, NamiPaywallAction, NamiPaywallManager$1 as NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, 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, 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, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, getPaywall, getPaywallDataFromLabel, getPercentagePriceDifference, getPeriodNumberInDays, getPeriodNumberInWeeks, getPlatformAdapters, getPriceDifference, getPricePerMonth, getProductDetail, getPurchaseAdapter, getReferenceSku, getSkuProductDetailKeys, getSkuSmartTextValue, getSlideSmartTextValue, getStandardBillingPeriod, getTranslate, getUrlParams, handleErrors, hasAllPaywalls, hasCapability, hasPurchaseManagement, initialState, invokeHandler, isAnonymousMode, isInitialConfigCompressed, isNamiFlowCampaign, isSubscription, isValidISODate, isValidUrl, logger, mapAnonymousCampaigns, namiBuySKU, normalizeLaunchContext, parseToSemver, postConversion, productDetail, registerPlatformAdapters, registerPurchaseAdapter, resolveLoopSource, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateForm, validateMinSDKVersion, validateTextInput };
65428
+ export { ALREADY_CONFIGURED, ANONYMOUS_MODE, ANONYMOUS_MODE_ALREADY_OFF, ANONYMOUS_MODE_ALREADY_ON, ANONYMOUS_MODE_LOGIN_NOT_ALLOWED, ANONYMOUS_UUID, APIError, API_ACTIVE_ENTITLEMENTS, API_CAMPAIGN_RULES, API_CAMPAIGN_SESSION_TIMESTAMP, API_CONFIG, API_MAX_CALLS_LIMIT, API_PAYWALLS, API_PRODUCTS, API_RETRY_DELAY_SEC, API_TIMEOUT_LIMIT, API_VERSION, AUTH_DEVICE, AVAILABLE_ACTIVE_ENTITLEMENTS_CHANGED, AVAILABLE_CAMPAIGNS_CHANGED, AccountStateAction, AnonymousCDPError, AnonymousLoginError, AnonymousModeAlreadyOffError, AnonymousModeAlreadyOnError, BASE_STAGING_URL, BASE_URL, BASE_URL_PATH, BadRequestError, BasicNamiFlow, BorderMap, BorderSideMap, CAMPAIGN_NOT_AVAILABLE, CUSTOMER_ATTRIBUTES_KEY_PREFIX, CUSTOMER_JOURNEY_STATE_CHANGED, CUSTOM_HOST_PREFIX, CampaignNotAvailableError, CampaignRuleConversionEventType, CampaignRuleRepository, Capabilities, ClientError, ConfigRepository, ConflictError, CustomerJourneyRepository, DEVELOPMENT, DEVICE_API_TIMEOUT_LIMIT, DEVICE_ID_NOT_SET, DEVICE_ID_REQUIRED, DISABLE_ASYNC_LOGIN_LOGOUT, DeviceIDRequiredError, DeviceRepository, EXTENDED_CLIENT_INFO_DELIMITER, EXTENDED_CLIENT_INFO_PREFIX, EXTENDED_PLATFORM, EXTENDED_PLATFORM_VERSION, EXTERNAL_ID_REQUIRED, EntitlementRepository, EntitlementUtils, ExternalIDRequiredError, FLOW_SCREENS_NOT_AVAILABLE, FlowScreensNotAvailableError, HTML_REGEX, INITIAL_APP_CONFIG, INITIAL_CAMPAIGN_RULES, INITIAL_PAYWALLS, INITIAL_PRODUCTS, INITIAL_SESSION_COUNTER_VALUE, INITIAL_SUCCESS, InternalServerError, KEY_SESSION_COUNTER, LIQUID_VARIABLE_REGEX, LOCAL_NAMI_ENTITLEMENTS, LOG_HTTP_REQUESTS, LOG_HTTP_TRAFFIC, LaunchCampaignError, LaunchContextResolver, LogLevel, NAMI_CONFIGURATION, NAMI_CUSTOMER_JOURNEY_STATE, NAMI_LANGUAGE_CODE, NAMI_LAST_IMPRESSION_ID, NAMI_LAUNCH_ID, NAMI_PROFILE, NAMI_PURCHASE_CHANNEL, NAMI_PURCHASE_IMPRESSION_ID, NAMI_SDK_PACKAGE_VERSION, NAMI_SDK_VERSION, NAMI_SESSION_ID, NAMI_STORAGE_KEYS, Nami, NamiAPI, NamiAnimationType, NamiCampaignManager$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, 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, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, getPaywall, getPaywallDataFromLabel, getPercentagePriceDifference, getPeriodNumberInDays, getPeriodNumberInWeeks, getPlatformAdapters, getPriceDifference, getPricePerMonth, getProductDetail, getPurchaseAdapter, getReferenceSku, getSkuProductDetailKeys, getSkuSmartTextValue, getSlideSmartTextValue, getStandardBillingPeriod, getTranslate, getUrlParams, handleErrors, hasAllPaywalls, hasCapability, hasPurchaseManagement, initialState, invokeHandler, isAnonymousMode, isInitialConfigCompressed, isNamiFlowCampaign, isSubscription, isValidISODate, isValidUrl, logger, mapAnonymousCampaigns, namiBuySKU, normalizeLaunchContext, parseToSemver, postConversion, productDetail, registerPlatformAdapters, registerPurchaseAdapter, resolveLoopSource, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateForm, validateMinSDKVersion, validateTextInput };