@funnelsgrove/runtime 0.7.0 → 0.7.2

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/README.md CHANGED
@@ -37,6 +37,7 @@ documented in [`docs/product-specs/funnel-template-runtime.md`](../../docs/produ
37
37
 
38
38
  - `config/*`: manifest, theme, font, env, and builder-preview contracts.
39
39
  - `runtime/*`: flow, routing, attribution, feature flags, preview bridge, URL attributes, and manifest validation.
40
+ - `runtime/published-runtime-resolution`: running-experiment normalization and immutable offer-set selection from pricing assignments.
40
41
  - `services/*`: API client, runtime mode, public env, project env, logger, and funnel state helpers.
41
42
  - `components/*`: generic runtime context, subscription handoff/management, editor panel, dev info, and base controls.
42
43
  - `content/*` and `steps/types`: shared step content and taxonomy contracts.
package/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@ export type { FunnelStepChoices } from './runtime/use-step-choices.js';
23
23
  export * from './runtime/preview-bridge.js';
24
24
  export * from './runtime/preview-definition-overrides.js';
25
25
  export * from './runtime/offer-set-runtime.js';
26
+ export * from './runtime/published-runtime-resolution.js';
26
27
  export * from './runtime/route-resolver.js';
27
28
  export * from './runtime/submit-email-capture.js';
28
29
  export * from './runtime/subscription-handoff.js';
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ export { useStepChoices } from './runtime/use-step-choices.js';
24
24
  export * from './runtime/preview-bridge.js';
25
25
  export * from './runtime/preview-definition-overrides.js';
26
26
  export * from './runtime/offer-set-runtime.js';
27
+ export * from './runtime/published-runtime-resolution.js';
27
28
  export * from './runtime/route-resolver.js';
28
29
  export * from './runtime/submit-email-capture.js';
29
30
  export * from './runtime/subscription-handoff.js';
@@ -0,0 +1,23 @@
1
+ import type { FunnelExperimentDefinition, FunnelPricingExperimentDefinition } from '../config/funnel.experiments.types.js';
2
+ import type { FunnelManifestExperiment } from '../config/funnel.manifest.types.js';
3
+ import type { ResolvedFunnelRuntimeConfig } from '../services/funnel-runtime-config.js';
4
+ import type { GeneratedOfferSet } from './offer-set-runtime.js';
5
+ export type ResolvedPublishedExperiments = {
6
+ experiments: readonly FunnelExperimentDefinition[];
7
+ pricingExperiments: readonly FunnelPricingExperimentDefinition[];
8
+ flowExperiments: readonly FunnelManifestExperiment[];
9
+ };
10
+ export type PublishedOfferSetSelection = {
11
+ offerSetId: string;
12
+ offerSet: GeneratedOfferSet | null;
13
+ experimentId: string | null;
14
+ variantKey: string | null;
15
+ };
16
+ export declare const resolvePublishedRuntimeExperiments: (config: ResolvedFunnelRuntimeConfig | null) => ResolvedPublishedExperiments;
17
+ export declare const resolvePublishedOfferSetSelection: ({ offerSets, pricingExperiments, featureFlags, search, defaultOfferSetId, }: {
18
+ offerSets: readonly GeneratedOfferSet[];
19
+ pricingExperiments: readonly FunnelPricingExperimentDefinition[];
20
+ featureFlags: Readonly<Record<string, string>>;
21
+ search?: string;
22
+ defaultOfferSetId?: string;
23
+ }) => PublishedOfferSetSelection;
@@ -0,0 +1,72 @@
1
+ import { toManifestExperiments } from '../config/funnel.experiments.types.js';
2
+ const getPricingExperimentVariants = (experiment) => {
3
+ var _a;
4
+ return ((_a = experiment.variants) === null || _a === void 0 ? void 0 : _a.length)
5
+ ? experiment.variants
6
+ : [experiment.control, experiment.variant];
7
+ };
8
+ export const resolvePublishedRuntimeExperiments = (config) => {
9
+ var _a;
10
+ const experiments = (_a = config === null || config === void 0 ? void 0 : config.experiments.filter((experiment) => { var _a; return ((_a = experiment.status) !== null && _a !== void 0 ? _a : 'running') === 'running'; })) !== null && _a !== void 0 ? _a : [];
11
+ return {
12
+ experiments,
13
+ pricingExperiments: experiments.filter((experiment) => experiment.type === 'pricing'),
14
+ flowExperiments: toManifestExperiments(experiments),
15
+ };
16
+ };
17
+ export const resolvePublishedOfferSetSelection = ({ offerSets, pricingExperiments, featureFlags, search = '', defaultOfferSetId, }) => {
18
+ var _a, _b, _c, _d, _e;
19
+ const activeOfferSets = offerSets.filter((offerSet) => {
20
+ var _a;
21
+ return (offerSet.isActive !== false && Boolean((_a = offerSet.id) === null || _a === void 0 ? void 0 : _a.trim()));
22
+ });
23
+ const activeOfferSetsById = new Map(activeOfferSets.map((offerSet) => [offerSet.id.trim(), offerSet]));
24
+ const params = new URLSearchParams(search);
25
+ const forcedExperimentId = params.get('mode') === 'test'
26
+ ? ((_a = params.get('experiment')) === null || _a === void 0 ? void 0 : _a.trim()) || null
27
+ : null;
28
+ const forcedVariantKey = forcedExperimentId
29
+ ? ((_b = params.get('variant')) === null || _b === void 0 ? void 0 : _b.trim()) || null
30
+ : null;
31
+ const resolveExperimentSelection = (experiment, variantKey) => {
32
+ if (!variantKey) {
33
+ return null;
34
+ }
35
+ const variant = getPricingExperimentVariants(experiment)
36
+ .find((candidate) => candidate.variantKey === variantKey);
37
+ const offerSet = variant ? activeOfferSetsById.get(variant.offerSetId) : null;
38
+ return variant && offerSet
39
+ ? {
40
+ offerSetId: offerSet.id,
41
+ offerSet,
42
+ experimentId: experiment.id,
43
+ variantKey,
44
+ }
45
+ : null;
46
+ };
47
+ if (forcedExperimentId) {
48
+ const forcedExperiment = pricingExperiments.find((experiment) => experiment.id === forcedExperimentId);
49
+ const forcedSelection = forcedExperiment
50
+ ? resolveExperimentSelection(forcedExperiment, forcedVariantKey)
51
+ : null;
52
+ if (forcedSelection) {
53
+ return forcedSelection;
54
+ }
55
+ }
56
+ for (const experiment of pricingExperiments) {
57
+ const selection = resolveExperimentSelection(experiment, ((_c = featureFlags[experiment.id]) === null || _c === void 0 ? void 0 : _c.trim()) || null);
58
+ if (selection) {
59
+ return selection;
60
+ }
61
+ }
62
+ const defaultOfferSet = defaultOfferSetId
63
+ ? activeOfferSetsById.get(defaultOfferSetId.trim())
64
+ : null;
65
+ const offerSet = (_d = defaultOfferSet !== null && defaultOfferSet !== void 0 ? defaultOfferSet : activeOfferSets[0]) !== null && _d !== void 0 ? _d : null;
66
+ return {
67
+ offerSetId: (_e = offerSet === null || offerSet === void 0 ? void 0 : offerSet.id) !== null && _e !== void 0 ? _e : '',
68
+ offerSet,
69
+ experimentId: null,
70
+ variantKey: null,
71
+ };
72
+ };
@@ -40,26 +40,33 @@ const getLegacyProviderMapping = (plan, mode) => {
40
40
  const buildPlanModeConfig = (item, mode, legacyPlan = null) => {
41
41
  var _a, _b;
42
42
  const metadata = isRecord(item.metadata) ? item.metadata : {};
43
+ const directMappings = isRecord(item.providerMappingsByMode)
44
+ ? item.providerMappingsByMode
45
+ : {};
43
46
  const metadataMappings = isRecord(metadata.providerMappingsByMode)
44
47
  ? metadata.providerMappingsByMode
45
48
  : {};
46
- const metadataMapping = isRecord(metadataMappings[mode]) ? metadataMappings[mode] : {};
49
+ const modeMapping = isRecord(directMappings[mode])
50
+ ? directMappings[mode]
51
+ : isRecord(metadataMappings[mode])
52
+ ? metadataMappings[mode]
53
+ : {};
47
54
  const legacyMapping = getLegacyProviderMapping(legacyPlan, mode);
48
55
  const prefix = mode === 'test' ? 'test' : 'live';
49
56
  const providerPlanId = (pickString(item, `${prefix}ProviderPlanId`, `${prefix}_provider_plan_id`)
50
- || pickString(metadataMapping, 'providerPlanId')
57
+ || pickString(modeMapping, 'providerPlanId')
51
58
  || (legacyMapping ? pickString(legacyMapping, 'providerPlanId', 'provider_plan_id') : null));
52
59
  if (!providerPlanId) {
53
60
  return null;
54
61
  }
55
- const amountMinor = (_a = pickNumber(metadataMapping, 'amountCents')) !== null && _a !== void 0 ? _a : (legacyMapping ? pickNumber(legacyMapping, 'amountMinor', 'amount_minor') : null);
56
- const interval = pickString(metadataMapping, 'billingInterval')
62
+ const amountMinor = (_a = pickNumber(modeMapping, 'amountCents')) !== null && _a !== void 0 ? _a : (legacyMapping ? pickNumber(legacyMapping, 'amountMinor', 'amount_minor') : null);
63
+ const interval = pickString(modeMapping, 'billingInterval')
57
64
  || (legacyMapping ? pickString(legacyMapping, 'interval') : null);
58
- const intervalCount = (_b = pickNumber(metadataMapping, 'billingIntervalCount')) !== null && _b !== void 0 ? _b : (legacyMapping ? pickNumber(legacyMapping, 'intervalCount', 'interval_count') : null);
59
- const currency = pickString(metadataMapping, 'currency')
65
+ const intervalCount = (_b = pickNumber(modeMapping, 'billingIntervalCount')) !== null && _b !== void 0 ? _b : (legacyMapping ? pickNumber(legacyMapping, 'intervalCount', 'interval_count') : null);
66
+ const currency = pickString(modeMapping, 'currency')
60
67
  || (legacyMapping ? pickString(legacyMapping, 'currency') : null);
61
68
  const amountMajor = amountMinor === null ? null : amountMinor / 100;
62
- const priceLabel = pickString(metadataMapping, 'priceLabel') || (amountMajor === null
69
+ const priceLabel = pickString(modeMapping, 'priceLabel') || (amountMajor === null
63
70
  ? null
64
71
  : new Intl.NumberFormat('en-US', {
65
72
  style: 'currency',
@@ -67,12 +74,12 @@ const buildPlanModeConfig = (item, mode, legacyPlan = null) => {
67
74
  minimumFractionDigits: 2,
68
75
  maximumFractionDigits: 2,
69
76
  }).format(amountMajor) + (interval ? `/${interval}` : ''));
70
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ providerPlanId }, (priceLabel ? { priceLabel } : {})), (pickString(metadataMapping, 'perDayAmount')
71
- ? { perDayAmount: pickString(metadataMapping, 'perDayAmount') }
72
- : {})), (pickString(metadataMapping, 'perDayLabel')
73
- ? { perDayLabel: pickString(metadataMapping, 'perDayLabel') }
74
- : {})), (amountMinor === null ? {} : { amountCents: amountMinor })), (pickString(metadataMapping, 'checkoutSummaryLabel')
75
- ? { checkoutSummaryLabel: pickString(metadataMapping, 'checkoutSummaryLabel') }
77
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ providerPlanId }, (priceLabel ? { priceLabel } : {})), (pickString(modeMapping, 'perDayAmount')
78
+ ? { perDayAmount: pickString(modeMapping, 'perDayAmount') }
79
+ : {})), (pickString(modeMapping, 'perDayLabel')
80
+ ? { perDayLabel: pickString(modeMapping, 'perDayLabel') }
81
+ : {})), (amountMinor === null ? {} : { amountCents: amountMinor })), (pickString(modeMapping, 'checkoutSummaryLabel')
82
+ ? { checkoutSummaryLabel: pickString(modeMapping, 'checkoutSummaryLabel') }
76
83
  : amountMajor === null
77
84
  ? {}
78
85
  : { checkoutSummaryLabel: `${new Intl.NumberFormat('en-US', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",