@namiml/sdk-core 3.4.10-dev.202608230430 → 3.4.10-dev.202608232056

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -106,7 +106,7 @@ const {
106
106
  // version — stamped by scripts/version.sh
107
107
  NAMI_SDK_VERSION: exports.NAMI_SDK_VERSION = "3.4.10",
108
108
  // full package version including dev suffix — stamped by scripts/version.sh
109
- NAMI_SDK_PACKAGE_VERSION: exports.NAMI_SDK_PACKAGE_VERSION = "3.4.10-dev.202608230430",
109
+ NAMI_SDK_PACKAGE_VERSION: exports.NAMI_SDK_PACKAGE_VERSION = "3.4.10-dev.202608232056",
110
110
  // environments
111
111
  PRODUCTION: exports.PRODUCTION = "production", DEVELOPMENT: exports.DEVELOPMENT = "development",
112
112
  // error messages
@@ -59057,6 +59057,7 @@ const getProductDetailHandlers = (product, referenceId, skus = []) => ({
59057
59057
  periodInMonthsAbbrev: () => getDurationInMonths(product, false, true, false, true),
59058
59058
  periodly: () => convertISO8601PeriodToPeriodly(getStandardBillingPeriod(product)),
59059
59059
  iso_subscription_duration: () => getISOSubscriptionDuration(product),
59060
+ bill_term: () => getBillTerm(product),
59060
59061
  invalid: () => false,
59061
59062
  unavailable: () => false,
59062
59063
  });
@@ -59216,6 +59217,21 @@ const getIntroductoryPhase = (product, referenceId) => {
59216
59217
  }
59217
59218
  return convertOfferToPricingPhase(offer, product.subscription_period);
59218
59219
  };
59220
+ // Analytics-stable bill-term names keyed by normalized ISO 8601 duration.
59221
+ // Deliberately NOT localized (unlike `periodly`) — these land in analytics
59222
+ // payloads via `{{ sku.bill_term }}`, where values must be stable per NAM-3429.
59223
+ const BILL_TERM_BY_ISO_DURATION = {
59224
+ P1D: "daily",
59225
+ P1W: "weekly",
59226
+ P1M: "monthly",
59227
+ P3M: "quarterly",
59228
+ P6M: "semiannual",
59229
+ P1Y: "annual",
59230
+ };
59231
+ const getBillTerm = (product) => {
59232
+ const iso = getISOSubscriptionDuration(product);
59233
+ return (iso && BILL_TERM_BY_ISO_DURATION[iso]) || null;
59234
+ };
59219
59235
  const getISOSubscriptionDuration = (product) => {
59220
59236
  // ["P1W", "P4W", "P1M", "P2M", "P3M", "P4M", "P6M", "P8M", "P1Y"]
59221
59237
  // P7D => P1W
@@ -60812,6 +60828,22 @@ class PaywallState extends SimpleEventTarget {
60812
60828
  selectedProducts() {
60813
60829
  return this.state.selectedProducts;
60814
60830
  }
60831
+ /**
60832
+ * NAM-3606: removes the group's focus-driven selection so no product condition
60833
+ * matches — Apple parity with `PaywallState.setFocusedSku(nil)` clearing when the
60834
+ * group toggle gains focus on television. Routes through `setSelectedProducts` so
60835
+ * `state.sku` recomputes and subscribers re-render. No-op when the group has no
60836
+ * selection, keeping repeated toggle focusin events from emitting redundant state.
60837
+ */
60838
+ clearSelectedProduct(groupId) {
60839
+ const current = this.state.selectedProducts || {};
60840
+ if (!groupId || current[groupId] == null) {
60841
+ return;
60842
+ }
60843
+ const next = { ...current };
60844
+ delete next[groupId];
60845
+ this.setSelectedProducts(next);
60846
+ }
60815
60847
  setPaywall(paywall, context, campaign) {
60816
60848
  let state = {
60817
60849
  ...this.state,
@@ -62594,6 +62626,34 @@ class FlowLiquidResolver {
62594
62626
  const value = PaywallState.currentProvider?.state.formStates[fieldId];
62595
62627
  return formTagValue(value);
62596
62628
  }
62629
+ // Purchased-SKU context (NAM-3965): {{ sku.* }} resolves against the SKU
62630
+ // captured by the raise's `__buy_sku__` trigger — deliberately NOT the
62631
+ // selected SKU, so cart-add analytics report what was actually bought.
62632
+ // Unset until a buy action fires (token blanks, matching other platforms).
62633
+ if (key.startsWith("sku.")) {
62634
+ const purchased = flow?.purchasedSku;
62635
+ if (!purchased)
62636
+ return undefined;
62637
+ const leaf = key.substring("sku.".length);
62638
+ switch (leaf) {
62639
+ case "sku_ref_id":
62640
+ return purchased.skuRefId;
62641
+ case "name":
62642
+ // Control Center reference name; the store title stays reachable as
62643
+ // {{ sku.title }} via the product-detail table below.
62644
+ return purchased.name;
62645
+ // SKUs can grant several entitlements; the scalar contract takes the
62646
+ // first in payload order (NAM-3429).
62647
+ case "entitlement_ref_id":
62648
+ return purchased.entitlements[0]?.entitlement_ref_id ?? "";
62649
+ case "entitlement_name":
62650
+ return purchased.entitlements[0]?.name ?? "";
62651
+ default: {
62652
+ const value = getProductDetail(leaf, purchased.productDetails);
62653
+ return value != null ? String(value) : undefined;
62654
+ }
62655
+ }
62656
+ }
62597
62657
  switch (key) {
62598
62658
  case "campaignId":
62599
62659
  return screenState?.campaign?.rule;
@@ -64050,6 +64110,27 @@ function validateForm(validators, formStates) {
64050
64110
 
64051
64111
  /** Form field key convention for grouped handoffs: `<tag>::<payloadKey>`. */
64052
64112
  const HANDOFF_GROUP_KEY = /^([^:]+)::(.+)$/;
64113
+ /**
64114
+ * Web buttons hand the flow a public `NamiSKU` (`skuId`/`productDetails`) while
64115
+ * Expo hands the wire `PaywallSKU` (`sku_ref_id`/`product_details`); collapse
64116
+ * both onto one shape so `{{ sku.* }}` resolves identically on every platform.
64117
+ */
64118
+ function normalizePurchasedSku(sku) {
64119
+ if ('sku_ref_id' in sku) {
64120
+ return {
64121
+ skuRefId: sku.sku_ref_id,
64122
+ name: sku.name,
64123
+ entitlements: Array.isArray(sku.entitlements) ? sku.entitlements : [],
64124
+ productDetails: sku.product_details ?? null,
64125
+ };
64126
+ }
64127
+ return {
64128
+ skuRefId: sku.skuId,
64129
+ name: sku.name,
64130
+ entitlements: Array.isArray(sku.entitlements) ? sku.entitlements : [],
64131
+ productDetails: sku.productDetails ?? null,
64132
+ };
64133
+ }
64053
64134
  class BasicNamiFlow {
64054
64135
  constructor(flowObject = {}) {
64055
64136
  this.id = flowObject.id ?? '';
@@ -64418,7 +64499,19 @@ class NamiFlow extends BasicNamiFlow {
64418
64499
  ];
64419
64500
  return lifecycles.some(cycle => [cycle.before, cycle.action, cycle.after].some(actions => actions?.some(action => submitFns.includes(action.function))));
64420
64501
  }
64502
+ /**
64503
+ * Stash the SKU carried by a `__buy_sku__` trigger so `{{ sku.* }}` resolves
64504
+ * in this and later lifecycles of the raise. Runs before the lifecycle
64505
+ * lookup: a buy tap should update the context even when the current step
64506
+ * has no `__buy_sku__` lifecycles of its own.
64507
+ */
64508
+ captureBuySkuData(actionId, data) {
64509
+ if (actionId === NamiReservedActions.BUY_SKU && data?.sku) {
64510
+ this.purchasedSku = normalizePurchasedSku(data.sku);
64511
+ }
64512
+ }
64421
64513
  triggerActions(actionId, component, data) {
64514
+ this.captureBuySkuData(actionId, data);
64422
64515
  const lifecycles = this.lifecycles(actionId);
64423
64516
  if (!lifecycles)
64424
64517
  return;
@@ -64450,6 +64543,7 @@ class NamiFlow extends BasicNamiFlow {
64450
64543
  }
64451
64544
  }
64452
64545
  triggerBeforeActions(actionId, component, data) {
64546
+ this.captureBuySkuData(actionId, data);
64453
64547
  const lifecycles = this.lifecycles(actionId);
64454
64548
  if (!lifecycles)
64455
64549
  return;
@@ -64459,6 +64553,7 @@ class NamiFlow extends BasicNamiFlow {
64459
64553
  }
64460
64554
  }
64461
64555
  triggerAfterActions(actionId, component, data) {
64556
+ this.captureBuySkuData(actionId, data);
64462
64557
  const lifecycles = this.lifecycles(actionId);
64463
64558
  if (!lifecycles)
64464
64559
  return;
package/dist/index.d.ts CHANGED
@@ -2264,6 +2264,16 @@ declare class NamiFlowManager$2 {
2264
2264
  static getCurrentFlowStepAutoAdvanceDelay(): number | null;
2265
2265
  }
2266
2266
 
2267
+ /**
2268
+ * Normalized SKU captured when a `__buy_sku__` trigger hands the flow a SKU,
2269
+ * resolvable for the rest of the raise via `{{ sku.* }}` (NAM-3965).
2270
+ */
2271
+ interface PurchasedSkuContext {
2272
+ skuRefId?: string;
2273
+ name?: string;
2274
+ entitlements: IEntitlements$1[];
2275
+ productDetails?: NamiProductDetails | null;
2276
+ }
2267
2277
  declare class BasicNamiFlow implements NamiFlowObjectDTO {
2268
2278
  id: string;
2269
2279
  name: string;
@@ -2316,6 +2326,13 @@ declare class NamiFlow extends BasicNamiFlow {
2316
2326
  login?: Record<string, NamiHandoffAttributeValue>;
2317
2327
  permissions: Record<string, boolean>;
2318
2328
  };
2329
+ /**
2330
+ * SKU handed to the flow by the most recent `__buy_sku__` trigger, exposed
2331
+ * to flow-event Liquid as `{{ sku.* }}` (NAM-3965). Kept for the rest of the
2332
+ * raise so `__purchase_success__` lifecycles — which carry no data on any
2333
+ * platform — still resolve it; dies with the flow instance.
2334
+ */
2335
+ purchasedSku?: PurchasedSkuContext;
2319
2336
  /**
2320
2337
  * Per-raise flow-completion tracking (NAM-1545). Bound to the impression
2321
2338
  * of the current screen's raise; one completion result per impression
@@ -2387,6 +2404,13 @@ declare class NamiFlow extends BasicNamiFlow {
2387
2404
  * NamiFlowControl.isFormSubmitLifecycle / Android NamiFlow.isFormSubmitLifecycle.
2388
2405
  */
2389
2406
  isFormSubmitLifecycle(lifecycles: NamiFlowOn[]): boolean;
2407
+ /**
2408
+ * Stash the SKU carried by a `__buy_sku__` trigger so `{{ sku.* }}` resolves
2409
+ * in this and later lifecycles of the raise. Runs before the lifecycle
2410
+ * lookup: a buy tap should update the context even when the current step
2411
+ * has no `__buy_sku__` lifecycles of its own.
2412
+ */
2413
+ private captureBuySkuData;
2390
2414
  triggerActions(actionId: string, component?: any, data?: Record<string, any>): void;
2391
2415
  executeFullLifecycles(lifecycles: NamiFlowOn[], data?: Record<string, any>): void;
2392
2416
  triggerBeforeActions(actionId: string, component?: any, data?: Record<string, any>): void;
@@ -2503,6 +2527,14 @@ declare class PaywallState extends SimpleEventTarget {
2503
2527
  selectedProducts(): {
2504
2528
  [currentGroupId: string]: string | null;
2505
2529
  };
2530
+ /**
2531
+ * NAM-3606: removes the group's focus-driven selection so no product condition
2532
+ * matches — Apple parity with `PaywallState.setFocusedSku(nil)` clearing when the
2533
+ * group toggle gains focus on television. Routes through `setSelectedProducts` so
2534
+ * `state.sku` recomputes and subscribers re-render. No-op when the group has no
2535
+ * selection, keeping repeated toggle focusin events from emitting redundant state.
2536
+ */
2537
+ clearSelectedProduct(groupId: string): void;
2506
2538
  setPaywall(paywall: IPaywall, context: NamiPaywallLaunchContext, campaign: NamiCampaign): void;
2507
2539
  getSelectedPaywall(): IPaywall | undefined;
2508
2540
  getSelectedCampaign(): NamiCampaign | undefined;
package/dist/index.mjs CHANGED
@@ -104,7 +104,7 @@ const {
104
104
  // version — stamped by scripts/version.sh
105
105
  NAMI_SDK_VERSION = "3.4.10",
106
106
  // full package version including dev suffix — stamped by scripts/version.sh
107
- NAMI_SDK_PACKAGE_VERSION = "3.4.10-dev.202608230430",
107
+ NAMI_SDK_PACKAGE_VERSION = "3.4.10-dev.202608232056",
108
108
  // environments
109
109
  PRODUCTION = "production", DEVELOPMENT = "development",
110
110
  // error messages
@@ -59055,6 +59055,7 @@ const getProductDetailHandlers = (product, referenceId, skus = []) => ({
59055
59055
  periodInMonthsAbbrev: () => getDurationInMonths(product, false, true, false, true),
59056
59056
  periodly: () => convertISO8601PeriodToPeriodly(getStandardBillingPeriod(product)),
59057
59057
  iso_subscription_duration: () => getISOSubscriptionDuration(product),
59058
+ bill_term: () => getBillTerm(product),
59058
59059
  invalid: () => false,
59059
59060
  unavailable: () => false,
59060
59061
  });
@@ -59214,6 +59215,21 @@ const getIntroductoryPhase = (product, referenceId) => {
59214
59215
  }
59215
59216
  return convertOfferToPricingPhase(offer, product.subscription_period);
59216
59217
  };
59218
+ // Analytics-stable bill-term names keyed by normalized ISO 8601 duration.
59219
+ // Deliberately NOT localized (unlike `periodly`) — these land in analytics
59220
+ // payloads via `{{ sku.bill_term }}`, where values must be stable per NAM-3429.
59221
+ const BILL_TERM_BY_ISO_DURATION = {
59222
+ P1D: "daily",
59223
+ P1W: "weekly",
59224
+ P1M: "monthly",
59225
+ P3M: "quarterly",
59226
+ P6M: "semiannual",
59227
+ P1Y: "annual",
59228
+ };
59229
+ const getBillTerm = (product) => {
59230
+ const iso = getISOSubscriptionDuration(product);
59231
+ return (iso && BILL_TERM_BY_ISO_DURATION[iso]) || null;
59232
+ };
59217
59233
  const getISOSubscriptionDuration = (product) => {
59218
59234
  // ["P1W", "P4W", "P1M", "P2M", "P3M", "P4M", "P6M", "P8M", "P1Y"]
59219
59235
  // P7D => P1W
@@ -60810,6 +60826,22 @@ class PaywallState extends SimpleEventTarget {
60810
60826
  selectedProducts() {
60811
60827
  return this.state.selectedProducts;
60812
60828
  }
60829
+ /**
60830
+ * NAM-3606: removes the group's focus-driven selection so no product condition
60831
+ * matches — Apple parity with `PaywallState.setFocusedSku(nil)` clearing when the
60832
+ * group toggle gains focus on television. Routes through `setSelectedProducts` so
60833
+ * `state.sku` recomputes and subscribers re-render. No-op when the group has no
60834
+ * selection, keeping repeated toggle focusin events from emitting redundant state.
60835
+ */
60836
+ clearSelectedProduct(groupId) {
60837
+ const current = this.state.selectedProducts || {};
60838
+ if (!groupId || current[groupId] == null) {
60839
+ return;
60840
+ }
60841
+ const next = { ...current };
60842
+ delete next[groupId];
60843
+ this.setSelectedProducts(next);
60844
+ }
60813
60845
  setPaywall(paywall, context, campaign) {
60814
60846
  let state = {
60815
60847
  ...this.state,
@@ -62592,6 +62624,34 @@ class FlowLiquidResolver {
62592
62624
  const value = PaywallState.currentProvider?.state.formStates[fieldId];
62593
62625
  return formTagValue(value);
62594
62626
  }
62627
+ // Purchased-SKU context (NAM-3965): {{ sku.* }} resolves against the SKU
62628
+ // captured by the raise's `__buy_sku__` trigger — deliberately NOT the
62629
+ // selected SKU, so cart-add analytics report what was actually bought.
62630
+ // Unset until a buy action fires (token blanks, matching other platforms).
62631
+ if (key.startsWith("sku.")) {
62632
+ const purchased = flow?.purchasedSku;
62633
+ if (!purchased)
62634
+ return undefined;
62635
+ const leaf = key.substring("sku.".length);
62636
+ switch (leaf) {
62637
+ case "sku_ref_id":
62638
+ return purchased.skuRefId;
62639
+ case "name":
62640
+ // Control Center reference name; the store title stays reachable as
62641
+ // {{ sku.title }} via the product-detail table below.
62642
+ return purchased.name;
62643
+ // SKUs can grant several entitlements; the scalar contract takes the
62644
+ // first in payload order (NAM-3429).
62645
+ case "entitlement_ref_id":
62646
+ return purchased.entitlements[0]?.entitlement_ref_id ?? "";
62647
+ case "entitlement_name":
62648
+ return purchased.entitlements[0]?.name ?? "";
62649
+ default: {
62650
+ const value = getProductDetail(leaf, purchased.productDetails);
62651
+ return value != null ? String(value) : undefined;
62652
+ }
62653
+ }
62654
+ }
62595
62655
  switch (key) {
62596
62656
  case "campaignId":
62597
62657
  return screenState?.campaign?.rule;
@@ -64048,6 +64108,27 @@ function validateForm(validators, formStates) {
64048
64108
 
64049
64109
  /** Form field key convention for grouped handoffs: `<tag>::<payloadKey>`. */
64050
64110
  const HANDOFF_GROUP_KEY = /^([^:]+)::(.+)$/;
64111
+ /**
64112
+ * Web buttons hand the flow a public `NamiSKU` (`skuId`/`productDetails`) while
64113
+ * Expo hands the wire `PaywallSKU` (`sku_ref_id`/`product_details`); collapse
64114
+ * both onto one shape so `{{ sku.* }}` resolves identically on every platform.
64115
+ */
64116
+ function normalizePurchasedSku(sku) {
64117
+ if ('sku_ref_id' in sku) {
64118
+ return {
64119
+ skuRefId: sku.sku_ref_id,
64120
+ name: sku.name,
64121
+ entitlements: Array.isArray(sku.entitlements) ? sku.entitlements : [],
64122
+ productDetails: sku.product_details ?? null,
64123
+ };
64124
+ }
64125
+ return {
64126
+ skuRefId: sku.skuId,
64127
+ name: sku.name,
64128
+ entitlements: Array.isArray(sku.entitlements) ? sku.entitlements : [],
64129
+ productDetails: sku.productDetails ?? null,
64130
+ };
64131
+ }
64051
64132
  class BasicNamiFlow {
64052
64133
  constructor(flowObject = {}) {
64053
64134
  this.id = flowObject.id ?? '';
@@ -64416,7 +64497,19 @@ class NamiFlow extends BasicNamiFlow {
64416
64497
  ];
64417
64498
  return lifecycles.some(cycle => [cycle.before, cycle.action, cycle.after].some(actions => actions?.some(action => submitFns.includes(action.function))));
64418
64499
  }
64500
+ /**
64501
+ * Stash the SKU carried by a `__buy_sku__` trigger so `{{ sku.* }}` resolves
64502
+ * in this and later lifecycles of the raise. Runs before the lifecycle
64503
+ * lookup: a buy tap should update the context even when the current step
64504
+ * has no `__buy_sku__` lifecycles of its own.
64505
+ */
64506
+ captureBuySkuData(actionId, data) {
64507
+ if (actionId === NamiReservedActions.BUY_SKU && data?.sku) {
64508
+ this.purchasedSku = normalizePurchasedSku(data.sku);
64509
+ }
64510
+ }
64419
64511
  triggerActions(actionId, component, data) {
64512
+ this.captureBuySkuData(actionId, data);
64420
64513
  const lifecycles = this.lifecycles(actionId);
64421
64514
  if (!lifecycles)
64422
64515
  return;
@@ -64448,6 +64541,7 @@ class NamiFlow extends BasicNamiFlow {
64448
64541
  }
64449
64542
  }
64450
64543
  triggerBeforeActions(actionId, component, data) {
64544
+ this.captureBuySkuData(actionId, data);
64451
64545
  const lifecycles = this.lifecycles(actionId);
64452
64546
  if (!lifecycles)
64453
64547
  return;
@@ -64457,6 +64551,7 @@ class NamiFlow extends BasicNamiFlow {
64457
64551
  }
64458
64552
  }
64459
64553
  triggerAfterActions(actionId, component, data) {
64554
+ this.captureBuySkuData(actionId, data);
64460
64555
  const lifecycles = this.lifecycles(actionId);
64461
64556
  if (!lifecycles)
64462
64557
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namiml/sdk-core",
3
- "version": "3.4.10-dev.202608230430",
3
+ "version": "3.4.10-dev.202608232056",
4
4
  "description": "Platform-agnostic core for the Nami SDK — business logic, API, types, and state management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",