@funnelsgrove/runtime 0.1.36 → 0.1.38
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.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runtime/offer-set-runtime.d.ts +71 -0
- package/dist/runtime/offer-set-runtime.js +193 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export * from './runtime/posthog-flags.js';
|
|
|
15
15
|
export * from './runtime/use-funnel-flow-controller.js';
|
|
16
16
|
export * from './runtime/preview-bridge.js';
|
|
17
17
|
export * from './runtime/preview-definition-overrides.js';
|
|
18
|
+
export * from './runtime/offer-set-runtime.js';
|
|
18
19
|
export * from './runtime/route-resolver.js';
|
|
19
20
|
export * from './runtime/subscription-handoff.js';
|
|
20
21
|
export * from './runtime/url-user-attributes.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export * from './runtime/posthog-flags.js';
|
|
|
15
15
|
export * from './runtime/use-funnel-flow-controller.js';
|
|
16
16
|
export * from './runtime/preview-bridge.js';
|
|
17
17
|
export * from './runtime/preview-definition-overrides.js';
|
|
18
|
+
export * from './runtime/offer-set-runtime.js';
|
|
18
19
|
export * from './runtime/route-resolver.js';
|
|
19
20
|
export * from './runtime/subscription-handoff.js';
|
|
20
21
|
export * from './runtime/url-user-attributes.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { RuntimeMode } from '../services/runtime-mode.service.js';
|
|
2
|
+
export type GeneratedOfferSetItemKind = 'plan' | 'upsell' | 'downsell';
|
|
3
|
+
export type GeneratedOfferSetPlanItem = {
|
|
4
|
+
readonly funnelPlanKey: string;
|
|
5
|
+
readonly projectBillingPlanId?: string | null;
|
|
6
|
+
readonly testProviderPlanId?: string | null;
|
|
7
|
+
readonly liveProviderPlanId?: string | null;
|
|
8
|
+
readonly analyticsPurchaseValue?: number | null;
|
|
9
|
+
readonly position?: number | null;
|
|
10
|
+
readonly isDefault?: boolean | null;
|
|
11
|
+
readonly displayNameOverride?: string | null;
|
|
12
|
+
readonly featuredTag?: string | null;
|
|
13
|
+
readonly oldPriceLabel?: string | null;
|
|
14
|
+
readonly metadata?: Record<string, unknown> | null;
|
|
15
|
+
};
|
|
16
|
+
export type GeneratedOfferSetDiscount = {
|
|
17
|
+
readonly key?: string | null;
|
|
18
|
+
readonly testProviderCouponId?: string | null;
|
|
19
|
+
readonly liveProviderCouponId?: string | null;
|
|
20
|
+
readonly position?: number | null;
|
|
21
|
+
readonly metadata?: Record<string, unknown> | null;
|
|
22
|
+
};
|
|
23
|
+
export type GeneratedOfferSet = {
|
|
24
|
+
readonly key: string;
|
|
25
|
+
readonly isActive?: boolean | null;
|
|
26
|
+
readonly metadata?: Record<string, unknown> | null;
|
|
27
|
+
readonly items: readonly GeneratedOfferSetPlanItem[];
|
|
28
|
+
};
|
|
29
|
+
export type OfferSetRuntimeDiscount = {
|
|
30
|
+
readonly stage: 'first';
|
|
31
|
+
readonly couponId: string;
|
|
32
|
+
readonly discountPercent: number;
|
|
33
|
+
readonly durationSeconds: number;
|
|
34
|
+
readonly previousDiscountPercent?: null;
|
|
35
|
+
} | {
|
|
36
|
+
readonly stage: 'second';
|
|
37
|
+
readonly couponId: string;
|
|
38
|
+
readonly discountPercent: number;
|
|
39
|
+
readonly durationSeconds: number;
|
|
40
|
+
readonly previousDiscountPercent: number;
|
|
41
|
+
};
|
|
42
|
+
export type OfferSetRuntimePlanOverrides = {
|
|
43
|
+
projectPlanId?: string;
|
|
44
|
+
providerPlanId?: string;
|
|
45
|
+
analyticsPurchaseValue?: number;
|
|
46
|
+
title?: string;
|
|
47
|
+
featuredTag?: string;
|
|
48
|
+
oldPriceLabel?: string;
|
|
49
|
+
isDefault?: boolean;
|
|
50
|
+
};
|
|
51
|
+
export type OfferSetRuntimeInput<TPlan extends Record<string, unknown>> = {
|
|
52
|
+
offerSets: readonly GeneratedOfferSet[];
|
|
53
|
+
offerSetKey?: string;
|
|
54
|
+
baseCatalogsByMode: Record<RuntimeMode, Record<string, TPlan>>;
|
|
55
|
+
fallbackDiscounts?: readonly OfferSetRuntimeDiscount[];
|
|
56
|
+
};
|
|
57
|
+
export type OfferSetRuntimeResult<TPlan extends Record<string, unknown>> = {
|
|
58
|
+
offerSet: GeneratedOfferSet | null;
|
|
59
|
+
plansByMode: Record<RuntimeMode, Record<string, TPlan & OfferSetRuntimePlanOverrides>>;
|
|
60
|
+
planKeys: string[];
|
|
61
|
+
upsellPlanKeys: string[];
|
|
62
|
+
downsellPlanKeys: string[];
|
|
63
|
+
defaultPlanKey: string;
|
|
64
|
+
discountListsByMode: Record<RuntimeMode, readonly OfferSetRuntimeDiscount[]>;
|
|
65
|
+
getPlanCatalog: (mode: RuntimeMode) => Record<string, TPlan & OfferSetRuntimePlanOverrides>;
|
|
66
|
+
getDiscountList: (mode: RuntimeMode) => readonly OfferSetRuntimeDiscount[];
|
|
67
|
+
};
|
|
68
|
+
export declare const findGeneratedOfferSet: (offerSets: readonly GeneratedOfferSet[], offerSetKey?: string) => GeneratedOfferSet | null;
|
|
69
|
+
export declare const applyGeneratedOfferSetToPlanCatalog: <TPlan extends Record<string, unknown>>(baseCatalog: Record<string, TPlan>, mode: RuntimeMode, offerSet: GeneratedOfferSet | null) => Record<string, TPlan & OfferSetRuntimePlanOverrides>;
|
|
70
|
+
export declare const buildGeneratedOfferSetDiscountList: (offerSet: GeneratedOfferSet | null, mode: RuntimeMode, fallbackDiscounts?: readonly OfferSetRuntimeDiscount[]) => readonly OfferSetRuntimeDiscount[];
|
|
71
|
+
export declare const resolveGeneratedOfferSetRuntime: <TPlan extends Record<string, unknown>>(input: OfferSetRuntimeInput<TPlan>) => OfferSetRuntimeResult<TPlan>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const isRecord = (value) => (typeof value === 'object' && value !== null && !Array.isArray(value));
|
|
2
|
+
const readString = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
|
|
3
|
+
const normalizeCatalogLookupKey = (value) => (value
|
|
4
|
+
.trim()
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[^a-z0-9]+/g, ''));
|
|
7
|
+
const readFiniteNumber = (value) => {
|
|
8
|
+
const numeric = typeof value === 'number'
|
|
9
|
+
? value
|
|
10
|
+
: typeof value === 'string' && value.trim()
|
|
11
|
+
? Number(value.trim())
|
|
12
|
+
: null;
|
|
13
|
+
return numeric !== null && Number.isFinite(numeric) ? numeric : null;
|
|
14
|
+
};
|
|
15
|
+
const readPositiveInteger = (value) => {
|
|
16
|
+
const numeric = readFiniteNumber(value);
|
|
17
|
+
return numeric !== null && numeric > 0 ? Math.round(numeric) : null;
|
|
18
|
+
};
|
|
19
|
+
const getOfferSetItemKind = (item) => {
|
|
20
|
+
var _a;
|
|
21
|
+
const metadata = isRecord(item.metadata) ? item.metadata : {};
|
|
22
|
+
const value = (_a = metadata.offerItemKind) !== null && _a !== void 0 ? _a : metadata.kind;
|
|
23
|
+
return value === 'upsell' || value === 'downsell' ? value : 'plan';
|
|
24
|
+
};
|
|
25
|
+
const getSortedItemsByKind = (offerSet, kind) => {
|
|
26
|
+
var _a;
|
|
27
|
+
return ((_a = offerSet === null || offerSet === void 0 ? void 0 : offerSet.items.filter((item) => getOfferSetItemKind(item) === kind).slice().sort((left, right) => {
|
|
28
|
+
var _a, _b;
|
|
29
|
+
const leftPosition = (_a = readFiniteNumber(left.position)) !== null && _a !== void 0 ? _a : 0;
|
|
30
|
+
const rightPosition = (_b = readFiniteNumber(right.position)) !== null && _b !== void 0 ? _b : 0;
|
|
31
|
+
return leftPosition - rightPosition;
|
|
32
|
+
})) !== null && _a !== void 0 ? _a : []);
|
|
33
|
+
};
|
|
34
|
+
export const findGeneratedOfferSet = (offerSets, offerSetKey) => {
|
|
35
|
+
var _a, _b;
|
|
36
|
+
const activeOfferSets = offerSets.filter((offerSet) => offerSet.isActive !== false);
|
|
37
|
+
return ((_b = (_a = activeOfferSets.find((offerSet) => offerSet.key === offerSetKey)) !== null && _a !== void 0 ? _a : activeOfferSets[0]) !== null && _b !== void 0 ? _b : null);
|
|
38
|
+
};
|
|
39
|
+
const readProviderPlanId = (item, mode) => (mode === 'test'
|
|
40
|
+
? readString(item.testProviderPlanId)
|
|
41
|
+
: readString(item.liveProviderPlanId));
|
|
42
|
+
const resolveCatalogKey = (baseCatalog, funnelPlanKey) => {
|
|
43
|
+
var _a;
|
|
44
|
+
const key = funnelPlanKey.trim();
|
|
45
|
+
if (!key) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (Object.prototype.hasOwnProperty.call(baseCatalog, key)) {
|
|
49
|
+
return key;
|
|
50
|
+
}
|
|
51
|
+
const normalizedKey = normalizeCatalogLookupKey(key);
|
|
52
|
+
return (_a = Object.keys(baseCatalog).find((catalogKey) => (normalizeCatalogLookupKey(catalogKey) === normalizedKey))) !== null && _a !== void 0 ? _a : null;
|
|
53
|
+
};
|
|
54
|
+
const resolveOfferItemRuntimeKey = (item, baseCatalogsByMode) => {
|
|
55
|
+
var _a, _b;
|
|
56
|
+
return (item
|
|
57
|
+
? (_b = (_a = resolveCatalogKey(baseCatalogsByMode.test, item.funnelPlanKey)) !== null && _a !== void 0 ? _a : resolveCatalogKey(baseCatalogsByMode.live, item.funnelPlanKey)) !== null && _b !== void 0 ? _b : readString(item.funnelPlanKey)
|
|
58
|
+
: null);
|
|
59
|
+
};
|
|
60
|
+
export const applyGeneratedOfferSetToPlanCatalog = (baseCatalog, mode, offerSet) => {
|
|
61
|
+
var _a, _b, _c;
|
|
62
|
+
if (!offerSet) {
|
|
63
|
+
return Object.assign({}, baseCatalog);
|
|
64
|
+
}
|
|
65
|
+
const planItems = getSortedItemsByKind(offerSet, 'plan');
|
|
66
|
+
const catalogItems = [
|
|
67
|
+
...planItems,
|
|
68
|
+
...getSortedItemsByKind(offerSet, 'upsell'),
|
|
69
|
+
...getSortedItemsByKind(offerSet, 'downsell'),
|
|
70
|
+
];
|
|
71
|
+
const nextCatalog = Object.assign({}, baseCatalog);
|
|
72
|
+
const defaultPlanItem = (_a = planItems.find((item) => item.isDefault === true)) !== null && _a !== void 0 ? _a : planItems[0];
|
|
73
|
+
const defaultPlanKey = defaultPlanItem
|
|
74
|
+
? (_b = resolveCatalogKey(nextCatalog, defaultPlanItem.funnelPlanKey)) !== null && _b !== void 0 ? _b : defaultPlanItem.funnelPlanKey
|
|
75
|
+
: '';
|
|
76
|
+
for (const item of catalogItems) {
|
|
77
|
+
const key = resolveCatalogKey(nextCatalog, item.funnelPlanKey);
|
|
78
|
+
if (!key) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const basePlan = nextCatalog[key];
|
|
82
|
+
if (!basePlan) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const providerPlanId = readProviderPlanId(item, mode);
|
|
86
|
+
const displayNameOverride = readString(item.displayNameOverride);
|
|
87
|
+
const featuredTag = readString(item.featuredTag);
|
|
88
|
+
const oldPriceLabel = readString(item.oldPriceLabel);
|
|
89
|
+
nextCatalog[key] = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, basePlan), (readString(item.projectBillingPlanId) ? { projectPlanId: (_c = readString(item.projectBillingPlanId)) !== null && _c !== void 0 ? _c : undefined } : {})), (providerPlanId ? { providerPlanId } : {})), (typeof item.analyticsPurchaseValue === 'number'
|
|
90
|
+
? { analyticsPurchaseValue: item.analyticsPurchaseValue }
|
|
91
|
+
: {})), (displayNameOverride ? { title: displayNameOverride } : {})), (featuredTag ? { featuredTag } : {})), (oldPriceLabel ? { oldPriceLabel } : {})), { isDefault: getOfferSetItemKind(item) === 'plan' && key === defaultPlanKey || undefined });
|
|
92
|
+
}
|
|
93
|
+
return nextCatalog;
|
|
94
|
+
};
|
|
95
|
+
const getOfferSetDiscounts = (offerSet) => {
|
|
96
|
+
const metadata = isRecord(offerSet === null || offerSet === void 0 ? void 0 : offerSet.metadata) ? offerSet.metadata : {};
|
|
97
|
+
const discounts = metadata.discounts;
|
|
98
|
+
return Array.isArray(discounts)
|
|
99
|
+
? discounts.filter((discount) => isRecord(discount))
|
|
100
|
+
: [];
|
|
101
|
+
};
|
|
102
|
+
const readDiscountCouponId = (discount, mode) => (mode === 'test'
|
|
103
|
+
? readString(discount.testProviderCouponId)
|
|
104
|
+
: readString(discount.liveProviderCouponId));
|
|
105
|
+
const readDiscountPercent = (discount, fallback) => {
|
|
106
|
+
var _a, _b, _c;
|
|
107
|
+
const metadata = isRecord(discount.metadata) ? discount.metadata : {};
|
|
108
|
+
return (_c = (_b = (_a = readPositiveInteger(metadata.discountPercent)) !== null && _a !== void 0 ? _a : readPositiveInteger(metadata.percentOff)) !== null && _b !== void 0 ? _b : fallback === null || fallback === void 0 ? void 0 : fallback.discountPercent) !== null && _c !== void 0 ? _c : 0;
|
|
109
|
+
};
|
|
110
|
+
const readDiscountDurationSeconds = (discount, fallback) => {
|
|
111
|
+
var _a, _b, _c;
|
|
112
|
+
const metadata = isRecord(discount.metadata) ? discount.metadata : {};
|
|
113
|
+
return (_c = (_b = (_a = readPositiveInteger(metadata.durationSeconds)) !== null && _a !== void 0 ? _a : readPositiveInteger(metadata.duration_seconds)) !== null && _b !== void 0 ? _b : fallback === null || fallback === void 0 ? void 0 : fallback.durationSeconds) !== null && _c !== void 0 ? _c : 0;
|
|
114
|
+
};
|
|
115
|
+
const isSecondDiscount = (discount, index) => {
|
|
116
|
+
var _a, _b;
|
|
117
|
+
const metadata = isRecord(discount.metadata) ? discount.metadata : {};
|
|
118
|
+
const stage = (_b = (_a = readString(metadata.stage)) !== null && _a !== void 0 ? _a : readString(metadata.offerStage)) !== null && _b !== void 0 ? _b : readString(discount.key);
|
|
119
|
+
return stage === 'second' || stage === 'downsell' || stage === 'upgraded' || index === 1;
|
|
120
|
+
};
|
|
121
|
+
export const buildGeneratedOfferSetDiscountList = (offerSet, mode, fallbackDiscounts = []) => {
|
|
122
|
+
var _a, _b, _c, _d, _e, _f;
|
|
123
|
+
const offerSetDiscounts = getOfferSetDiscounts(offerSet);
|
|
124
|
+
if (offerSetDiscounts.length === 0) {
|
|
125
|
+
return fallbackDiscounts;
|
|
126
|
+
}
|
|
127
|
+
const sortedDiscounts = offerSetDiscounts
|
|
128
|
+
.slice()
|
|
129
|
+
.sort((left, right) => { var _a, _b; return ((_a = readFiniteNumber(left.position)) !== null && _a !== void 0 ? _a : 0) - ((_b = readFiniteNumber(right.position)) !== null && _b !== void 0 ? _b : 0); });
|
|
130
|
+
const firstFallback = (_a = fallbackDiscounts.find((discount) => discount.stage === 'first')) !== null && _a !== void 0 ? _a : null;
|
|
131
|
+
const secondFallback = (_b = fallbackDiscounts.find((discount) => discount.stage === 'second')) !== null && _b !== void 0 ? _b : null;
|
|
132
|
+
const firstDiscount = (_c = sortedDiscounts.find((discount, index) => !isSecondDiscount(discount, index))) !== null && _c !== void 0 ? _c : sortedDiscounts[0];
|
|
133
|
+
const secondDiscount = (_e = (_d = sortedDiscounts.find((discount, index) => isSecondDiscount(discount, index))) !== null && _d !== void 0 ? _d : sortedDiscounts[1]) !== null && _e !== void 0 ? _e : sortedDiscounts[0];
|
|
134
|
+
const firstCouponId = firstDiscount ? readDiscountCouponId(firstDiscount, mode) : null;
|
|
135
|
+
const secondCouponId = secondDiscount ? readDiscountCouponId(secondDiscount, mode) : null;
|
|
136
|
+
if (!firstDiscount || !secondDiscount || !firstCouponId || !secondCouponId) {
|
|
137
|
+
return fallbackDiscounts;
|
|
138
|
+
}
|
|
139
|
+
const firstDiscountPercent = readDiscountPercent(firstDiscount, firstFallback);
|
|
140
|
+
const secondMetadata = isRecord(secondDiscount.metadata) ? secondDiscount.metadata : {};
|
|
141
|
+
return [
|
|
142
|
+
{
|
|
143
|
+
stage: 'first',
|
|
144
|
+
couponId: firstCouponId,
|
|
145
|
+
discountPercent: firstDiscountPercent,
|
|
146
|
+
durationSeconds: readDiscountDurationSeconds(firstDiscount, firstFallback),
|
|
147
|
+
previousDiscountPercent: null,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
stage: 'second',
|
|
151
|
+
couponId: secondCouponId,
|
|
152
|
+
discountPercent: readDiscountPercent(secondDiscount, secondFallback),
|
|
153
|
+
durationSeconds: readDiscountDurationSeconds(secondDiscount, secondFallback),
|
|
154
|
+
previousDiscountPercent: (_f = readPositiveInteger(secondMetadata.previousDiscountPercent)) !== null && _f !== void 0 ? _f : firstDiscountPercent,
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
};
|
|
158
|
+
export const resolveGeneratedOfferSetRuntime = (input) => {
|
|
159
|
+
var _a, _b;
|
|
160
|
+
const offerSet = findGeneratedOfferSet(input.offerSets, input.offerSetKey);
|
|
161
|
+
const planItems = getSortedItemsByKind(offerSet, 'plan');
|
|
162
|
+
const upsellItems = getSortedItemsByKind(offerSet, 'upsell');
|
|
163
|
+
const downsellItems = getSortedItemsByKind(offerSet, 'downsell');
|
|
164
|
+
const planKeys = planItems
|
|
165
|
+
.map((item) => resolveOfferItemRuntimeKey(item, input.baseCatalogsByMode))
|
|
166
|
+
.filter((key) => Boolean(key));
|
|
167
|
+
const upsellPlanKeys = upsellItems
|
|
168
|
+
.map((item) => resolveOfferItemRuntimeKey(item, input.baseCatalogsByMode))
|
|
169
|
+
.filter((key) => Boolean(key));
|
|
170
|
+
const downsellPlanKeys = (downsellItems.length > 0 ? downsellItems : planItems)
|
|
171
|
+
.map((item) => resolveOfferItemRuntimeKey(item, input.baseCatalogsByMode))
|
|
172
|
+
.filter((key) => Boolean(key));
|
|
173
|
+
const defaultPlanKey = (_b = resolveOfferItemRuntimeKey((_a = planItems.find((item) => item.isDefault === true)) !== null && _a !== void 0 ? _a : planItems[0], input.baseCatalogsByMode)) !== null && _b !== void 0 ? _b : '';
|
|
174
|
+
const plansByMode = {
|
|
175
|
+
test: applyGeneratedOfferSetToPlanCatalog(input.baseCatalogsByMode.test, 'test', offerSet),
|
|
176
|
+
live: applyGeneratedOfferSetToPlanCatalog(input.baseCatalogsByMode.live, 'live', offerSet),
|
|
177
|
+
};
|
|
178
|
+
const discountListsByMode = {
|
|
179
|
+
test: buildGeneratedOfferSetDiscountList(offerSet, 'test', input.fallbackDiscounts),
|
|
180
|
+
live: buildGeneratedOfferSetDiscountList(offerSet, 'live', input.fallbackDiscounts),
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
offerSet,
|
|
184
|
+
plansByMode,
|
|
185
|
+
planKeys,
|
|
186
|
+
upsellPlanKeys,
|
|
187
|
+
downsellPlanKeys,
|
|
188
|
+
defaultPlanKey,
|
|
189
|
+
discountListsByMode,
|
|
190
|
+
getPlanCatalog: (mode) => plansByMode[mode],
|
|
191
|
+
getDiscountList: (mode) => discountListsByMode[mode],
|
|
192
|
+
};
|
|
193
|
+
};
|