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