@funnelsgrove/runtime 0.7.10 → 0.7.12

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.
@@ -1,6 +1,7 @@
1
1
  import type { RuntimeMode } from '../services/runtime-mode.service.js';
2
2
  export type GeneratedOfferSetItemKind = 'plan' | 'upsell' | 'downsell';
3
3
  export type GeneratedOfferSetPlanModeConfig = {
4
+ readonly provider?: 'stripe' | 'solidgate' | null;
4
5
  readonly providerPlanId?: string | null;
5
6
  readonly paymentMode?: string | null;
6
7
  readonly priceLabel?: string | null;
@@ -19,6 +20,8 @@ export type GeneratedOfferSetPlanItem = {
19
20
  readonly funnelPlanKey: string;
20
21
  readonly runtimePlanKey?: string | null;
21
22
  readonly projectBillingPlanId?: string | null;
23
+ readonly testProvider?: 'stripe' | 'solidgate' | null;
24
+ readonly liveProvider?: 'stripe' | 'solidgate' | null;
22
25
  readonly testProviderPlanId?: string | null;
23
26
  readonly liveProviderPlanId?: string | null;
24
27
  readonly followUpTestProviderPlanId?: string | null;
@@ -57,6 +60,7 @@ export type GeneratedOfferSetDiscount = {
57
60
  export type GeneratedOfferSet = {
58
61
  readonly id?: string | null;
59
62
  readonly key: string;
63
+ readonly paymentProfileId?: string | null;
60
64
  readonly isActive?: boolean | null;
61
65
  readonly metadata?: Record<string, unknown> | null;
62
66
  readonly items: readonly GeneratedOfferSetPlanItem[];
@@ -109,6 +113,7 @@ export type OfferSetRuntimeInput<TPlan extends Record<string, unknown>> = {
109
113
  };
110
114
  export type OfferSetRuntimeResult<TPlan extends Record<string, unknown>> = {
111
115
  offerSet: GeneratedOfferSet | null;
116
+ paymentProvidersByMode: Record<RuntimeMode, 'stripe' | 'solidgate'>;
112
117
  plansByMode: Record<RuntimeMode, Record<string, TPlan & OfferSetRuntimePlanOverrides>>;
113
118
  planKeys: string[];
114
119
  upsellPlanKeys: string[];
@@ -116,6 +121,7 @@ export type OfferSetRuntimeResult<TPlan extends Record<string, unknown>> = {
116
121
  defaultPlanKey: string;
117
122
  discountListsByMode: Record<RuntimeMode, readonly OfferSetRuntimeDiscount[]>;
118
123
  getPlanCatalog: (mode: RuntimeMode) => Record<string, TPlan & OfferSetRuntimePlanOverrides>;
124
+ getPaymentProvider: (mode: RuntimeMode) => 'stripe' | 'solidgate';
119
125
  getDiscountList: (mode: RuntimeMode) => readonly OfferSetRuntimeDiscount[];
120
126
  };
121
127
  export declare const findGeneratedOfferSet: (offerSets: readonly GeneratedOfferSet[], offerSetKey?: string, offerSetId?: string) => GeneratedOfferSet | null;
@@ -99,6 +99,22 @@ const readProviderPlanId = (item, mode) => {
99
99
  ? readString(item.testProviderPlanId)
100
100
  : readString(item.liveProviderPlanId)));
101
101
  };
102
+ const readPaymentProvider = (item, mode) => {
103
+ var _a;
104
+ const modeConfig = readGeneratedModeConfig(item, mode);
105
+ const value = (_a = readString(modeConfig.provider)) !== null && _a !== void 0 ? _a : (mode === 'test' ? readString(item.testProvider) : readString(item.liveProvider));
106
+ return value === 'stripe' || value === 'solidgate' ? value : null;
107
+ };
108
+ const resolveOfferSetPaymentProvider = (offerSet, mode) => {
109
+ var _a, _b;
110
+ const providers = new Set(((_a = offerSet === null || offerSet === void 0 ? void 0 : offerSet.items) !== null && _a !== void 0 ? _a : [])
111
+ .map((item) => readPaymentProvider(item, mode))
112
+ .filter((provider) => Boolean(provider)));
113
+ if (providers.size > 1) {
114
+ throw new Error(`Offer set "${(offerSet === null || offerSet === void 0 ? void 0 : offerSet.key) || 'unknown'}" mixes payment providers in ${mode} mode`);
115
+ }
116
+ return (_b = providers.values().next().value) !== null && _b !== void 0 ? _b : 'stripe';
117
+ };
102
118
  const readFollowUpProviderPlanId = (item, mode) => {
103
119
  var _a, _b, _c;
104
120
  const modeConfig = readGeneratedModeConfig(item, mode);
@@ -279,12 +295,17 @@ export const resolveGeneratedOfferSetRuntime = (input) => {
279
295
  test: applyGeneratedOfferSetToPlanCatalog(baseCatalogsByMode.test, 'test', offerSet),
280
296
  live: applyGeneratedOfferSetToPlanCatalog(baseCatalogsByMode.live, 'live', offerSet),
281
297
  };
298
+ const paymentProvidersByMode = {
299
+ test: resolveOfferSetPaymentProvider(offerSet, 'test'),
300
+ live: resolveOfferSetPaymentProvider(offerSet, 'live'),
301
+ };
282
302
  const discountListsByMode = {
283
- test: buildGeneratedOfferSetDiscountList(offerSet, 'test', input.fallbackDiscounts),
284
- live: buildGeneratedOfferSetDiscountList(offerSet, 'live', input.fallbackDiscounts),
303
+ test: buildGeneratedOfferSetDiscountList(offerSet, 'test', paymentProvidersByMode.test === 'stripe' ? input.fallbackDiscounts : undefined),
304
+ live: buildGeneratedOfferSetDiscountList(offerSet, 'live', paymentProvidersByMode.live === 'stripe' ? input.fallbackDiscounts : undefined),
285
305
  };
286
306
  return {
287
307
  offerSet,
308
+ paymentProvidersByMode,
288
309
  plansByMode,
289
310
  planKeys,
290
311
  upsellPlanKeys,
@@ -292,6 +313,7 @@ export const resolveGeneratedOfferSetRuntime = (input) => {
292
313
  defaultPlanKey,
293
314
  discountListsByMode,
294
315
  getPlanCatalog: (mode) => plansByMode[mode],
316
+ getPaymentProvider: (mode) => paymentProvidersByMode[mode],
295
317
  getDiscountList: (mode) => discountListsByMode[mode],
296
318
  };
297
319
  };
@@ -270,7 +270,7 @@ export function resolveRenderedExperimentStepId(input) {
270
270
  return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
271
271
  }
272
272
  export function useFunnelFlowController({ api = apiService, analytics, stepContractVersion, initialStepId, initialAttributes, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
273
- var _a, _b;
273
+ var _a, _b, _c;
274
274
  const [runtimeMode] = useRuntimeMode();
275
275
  const [editorModeEnabled, setEditorModeEnabled] = useState(() => isEditorEnabled());
276
276
  const [editorPanelExpanded, setEditorPanelExpanded] = useState(false);
@@ -278,7 +278,6 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
278
278
  const [editorVariantOverrides, setEditorVariantOverrides] = useState({});
279
279
  const shouldLockToInitialStep = lockToInitialStep || isPreviewStepLockRequested();
280
280
  const isPreviewRuntime = useMemo(() => isPreviewFrameRuntime(), []);
281
- const lifecycleEventsSuppressed = isPreviewRuntime || editorModeEnabled;
282
281
  const resolveRenderableStepId = useCallback((stepId) => {
283
282
  if (!stepId || !isFunnelStepId(stepId)) {
284
283
  return null;
@@ -308,10 +307,14 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
308
307
  });
309
308
  return resolveFallbackStepId(requestedStepId);
310
309
  }, [initialStepId, resolveFallbackStepId, resolveRuntimeInitialStepId]);
310
+ const isSubscriptionManagementRuntime = ((_a = stepById[safeInitialStepId]) === null || _a === void 0 ? void 0 : _a.kind) === 'manage-subscription';
311
+ const lifecycleEventsSuppressed = isPreviewRuntime || editorModeEnabled || isSubscriptionManagementRuntime;
311
312
  const [activeStepId, setActiveStepId] = useState(safeInitialStepId);
312
313
  const [attributes, setAttributes] = useState(() => (Object.assign({}, (initialAttributes || {}))));
313
314
  const [user, setUser] = useState(() => ({
314
- id: api.getOrCreateClientUserId(),
315
+ id: isSubscriptionManagementRuntime
316
+ ? resolveUrlUserAttributes().userId || ''
317
+ : api.getOrCreateClientUserId(),
315
318
  name: '',
316
319
  email: '',
317
320
  attributes: {},
@@ -503,8 +506,16 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
503
506
  safeInitialStepId,
504
507
  ]);
505
508
  useEffect(() => {
506
- const localUserId = api.getOrCreateClientUserId();
507
509
  const urlUserAttributes = resolveUrlUserAttributes();
510
+ if (isSubscriptionManagementRuntime) {
511
+ initialAttributionRef.current = collectCurrentFunnelAttribution();
512
+ setUser((prev) => (Object.assign(Object.assign({}, prev), { id: urlUserAttributes.userId || '' })));
513
+ setUserBootstrapped(true);
514
+ setAttributionReady(true);
515
+ setPostHogReady(true);
516
+ return;
517
+ }
518
+ const localUserId = api.getOrCreateClientUserId();
508
519
  const bootstrapCandidateUserId = api.getBootstrapCandidateUserId(localUserId);
509
520
  const initialAttribution = collectCurrentFunnelAttribution();
510
521
  initialAttributionRef.current = initialAttribution;
@@ -578,7 +589,7 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
578
589
  logger.error('Failed to identify PostHog user:', error);
579
590
  });
580
591
  return cancelAttributionFailOpen;
581
- }, [api, isPreviewRuntime]);
592
+ }, [api, isPreviewRuntime, isSubscriptionManagementRuntime]);
582
593
  const recordStepCompletion = useCallback((intent, options = {}) => {
583
594
  var _a;
584
595
  const meta = stepById[intent.visit.stepId];
@@ -1198,10 +1209,10 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
1198
1209
  safeInitialStepId,
1199
1210
  ]);
1200
1211
  const choiceTargets = getChoiceTargetsForStep(renderedStepId);
1201
- const actionBar = (_a = renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.actionBar) !== null && _a !== void 0 ? _a : activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.actionBar;
1212
+ const actionBar = (_b = renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.actionBar) !== null && _b !== void 0 ? _b : activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.actionBar;
1202
1213
  const isAutoAdvanceStep = typeof (actionBar === null || actionBar === void 0 ? void 0 : actionBar.autoAdvanceMs) === 'number';
1203
1214
  const autoAdvanceMs = actionBar === null || actionBar === void 0 ? void 0 : actionBar.autoAdvanceMs;
1204
- const buttonVariant = (_b = actionBar === null || actionBar === void 0 ? void 0 : actionBar.buttonVariant) !== null && _b !== void 0 ? _b : null;
1215
+ const buttonVariant = (_c = actionBar === null || actionBar === void 0 ? void 0 : actionBar.buttonVariant) !== null && _c !== void 0 ? _c : null;
1205
1216
  const buttonText = (actionBar === null || actionBar === void 0 ? void 0 : actionBar.buttonText) || 'Continue';
1206
1217
  const hideActionBar = (actionBar === null || actionBar === void 0 ? void 0 : actionBar.hidden) === true;
1207
1218
  useEffect(() => {
@@ -512,9 +512,6 @@ class ApiService {
512
512
  if (!payload.user) {
513
513
  throw new Error(SUBSCRIPTION_MANAGEMENT_LINK_ERROR);
514
514
  }
515
- if (userId) {
516
- persistUserId(payload.user.user_id || userId, FUNNEL_ID);
517
- }
518
515
  return payload;
519
516
  }
520
517
  async updateSubscription(input) {
@@ -3,6 +3,17 @@ const isRecord = (value) => (value !== null && typeof value === 'object' && !Arr
3
3
  const readString = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
4
4
  const readNumber = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : null);
5
5
  const readBoolean = (value) => (typeof value === 'boolean' ? value : null);
6
+ const readPaymentProvider = (value) => (value === 'stripe' || value === 'solidgate' ? value : null);
7
+ const parsePaymentProvider = (value, context) => {
8
+ if (value === undefined || value === null || value === '') {
9
+ return null;
10
+ }
11
+ const provider = readPaymentProvider(value);
12
+ if (!provider) {
13
+ throw new Error(`${context} has an unsupported payment provider`);
14
+ }
15
+ return provider;
16
+ };
6
17
  const readRecordArray = (value) => (Array.isArray(value) ? value.filter(isRecord) : []);
7
18
  const pick = (record, ...keys) => {
8
19
  for (const key of keys) {
@@ -26,6 +37,7 @@ const deepFreeze = (value) => {
26
37
  return value;
27
38
  };
28
39
  const buildPlanModeConfig = (item, mode) => {
40
+ var _a;
29
41
  const metadata = isRecord(item.metadata) ? item.metadata : {};
30
42
  const directMappings = isRecord(item.providerMappingsByMode)
31
43
  ? item.providerMappingsByMode
@@ -39,6 +51,7 @@ const buildPlanModeConfig = (item, mode) => {
39
51
  ? metadataMappings[mode]
40
52
  : {};
41
53
  const prefix = mode === 'test' ? 'test' : 'live';
54
+ const provider = (_a = parsePaymentProvider(pick(item, `${prefix}Provider`, `${prefix}_provider`), `Published runtime ${mode} plan mapping`)) !== null && _a !== void 0 ? _a : parsePaymentProvider(pick(modeMapping, 'provider'), `Published runtime ${mode} plan mapping`);
42
55
  const providerPlanId = (pickString(item, `${prefix}ProviderPlanId`, `${prefix}_provider_plan_id`)
43
56
  || pickString(modeMapping, 'providerPlanId'));
44
57
  if (!providerPlanId) {
@@ -57,7 +70,7 @@ const buildPlanModeConfig = (item, mode) => {
57
70
  minimumFractionDigits: 2,
58
71
  maximumFractionDigits: 2,
59
72
  }).format(amountMajor) + (interval ? `/${interval}` : ''));
60
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ providerPlanId }, (priceLabel ? { priceLabel } : {})), (pickString(modeMapping, 'perDayAmount')
73
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (provider ? { provider } : {})), { providerPlanId }), (priceLabel ? { priceLabel } : {})), (pickString(modeMapping, 'perDayAmount')
61
74
  ? { perDayAmount: pickString(modeMapping, 'perDayAmount') }
62
75
  : {})), (pickString(modeMapping, 'perDayLabel')
63
76
  ? { perDayLabel: pickString(modeMapping, 'perDayLabel') }
@@ -87,15 +100,17 @@ const normalizePublishedOfferSets = (config) => {
87
100
  throw new Error(`Published runtime offer set "${key}" has an item without a plan key`);
88
101
  }
89
102
  const projectBillingPlanId = pickString(item, 'projectBillingPlanId', 'project_billing_plan_id');
103
+ const testProvider = parsePaymentProvider(pick(item, 'testProvider', 'test_provider'), `Published runtime offer set "${key}"`);
104
+ const liveProvider = parsePaymentProvider(pick(item, 'liveProvider', 'live_provider'), `Published runtime offer set "${key}"`);
90
105
  const testMapping = buildPlanModeConfig(item, 'test');
91
106
  const liveMapping = buildPlanModeConfig(item, 'live');
92
107
  const title = pickString(item, 'title', 'displayNameOverride', 'display_name_override')
93
108
  || pickString(metadata, 'title', 'publicPlanName', 'public_plan_name');
94
109
  const description = pickString(item, 'description')
95
110
  || pickString(metadata, 'description');
96
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ funnelPlanKey }, (pickString(metadata, 'runtimePlanKey', 'planKey', 'billingPlanKey')
111
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ funnelPlanKey }, (pickString(metadata, 'runtimePlanKey', 'planKey', 'billingPlanKey')
97
112
  ? { runtimePlanKey: pickString(metadata, 'runtimePlanKey', 'planKey', 'billingPlanKey') }
98
- : {})), (projectBillingPlanId ? { projectBillingPlanId } : {})), (pickString(item, 'testProviderPlanId', 'test_provider_plan_id')
113
+ : {})), (projectBillingPlanId ? { projectBillingPlanId } : {})), (testProvider ? { testProvider } : {})), (liveProvider ? { liveProvider } : {})), (pickString(item, 'testProviderPlanId', 'test_provider_plan_id')
99
114
  ? { testProviderPlanId: pickString(item, 'testProviderPlanId', 'test_provider_plan_id') }
100
115
  : {})), (pickString(item, 'liveProviderPlanId', 'live_provider_plan_id')
101
116
  ? { liveProviderPlanId: pickString(item, 'liveProviderPlanId', 'live_provider_plan_id') }
@@ -119,7 +134,9 @@ const normalizePublishedOfferSets = (config) => {
119
134
  ? { oldPriceLabel: pickString(item, 'oldPriceLabel', 'old_price_label') }
120
135
  : {})), { isDefault: (_a = pickBoolean(item, 'isDefault', 'is_default')) !== null && _a !== void 0 ? _a : false, providerMappingsByMode: Object.assign(Object.assign({}, (testMapping ? { test: testMapping } : {})), (liveMapping ? { live: liveMapping } : {})), metadata });
121
136
  });
122
- return Object.assign(Object.assign({}, (pickString(offerSet, 'id') ? { id: pickString(offerSet, 'id') } : {})), { key, isActive: (_a = pickBoolean(offerSet, 'isActive', 'is_active')) !== null && _a !== void 0 ? _a : true, metadata: isRecord(offerSet.metadata) ? offerSet.metadata : {}, items });
137
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, (pickString(offerSet, 'id') ? { id: pickString(offerSet, 'id') } : {})), { key }), (pickString(offerSet, 'paymentProfileId', 'payment_profile_id')
138
+ ? { paymentProfileId: pickString(offerSet, 'paymentProfileId', 'payment_profile_id') }
139
+ : {})), { isActive: (_a = pickBoolean(offerSet, 'isActive', 'is_active')) !== null && _a !== void 0 ? _a : true, metadata: isRecord(offerSet.metadata) ? offerSet.metadata : {}, items });
123
140
  });
124
141
  };
125
142
  const normalizePublishedExperiments = (config, publishedAt, offerSets) => readRecordArray(config.experiments).map((experiment) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.7.10",
3
+ "version": "0.7.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",