@funnelsgrove/runtime 0.1.59 → 0.1.61

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.
@@ -10,6 +10,10 @@ export type GeneratedOfferSetPlanModeConfig = {
10
10
  readonly checkoutSummaryLabel?: string | null;
11
11
  readonly billingInterval?: string | null;
12
12
  readonly billingIntervalCount?: number | null;
13
+ readonly followUpProviderPlanId?: string | null;
14
+ readonly followUpBillingInterval?: string | null;
15
+ readonly followUpBillingIntervalCount?: number | null;
16
+ readonly introIterations?: number | null;
13
17
  };
14
18
  export type GeneratedOfferSetPlanItem = {
15
19
  readonly funnelPlanKey: string;
@@ -17,6 +21,14 @@ export type GeneratedOfferSetPlanItem = {
17
21
  readonly projectBillingPlanId?: string | null;
18
22
  readonly testProviderPlanId?: string | null;
19
23
  readonly liveProviderPlanId?: string | null;
24
+ readonly followUpTestProviderPlanId?: string | null;
25
+ readonly followUpLiveProviderPlanId?: string | null;
26
+ readonly followUpProviderPlanId?: string | null;
27
+ readonly followUpTitle?: string | null;
28
+ readonly followUpPlanTitle?: string | null;
29
+ readonly followUpBillingInterval?: string | null;
30
+ readonly followUpBillingIntervalCount?: number | null;
31
+ readonly introIterations?: number | null;
20
32
  readonly title?: string | null;
21
33
  readonly description?: string | null;
22
34
  readonly priceLabel?: string | null;
@@ -75,6 +87,11 @@ export type OfferSetRuntimePlanOverrides = {
75
87
  checkoutSummaryLabel?: string;
76
88
  billingInterval?: string;
77
89
  billingIntervalCount?: number;
90
+ followUpProviderPlanId?: string;
91
+ followUpPlanTitle?: string;
92
+ followUpBillingInterval?: string;
93
+ followUpBillingIntervalCount?: number;
94
+ introIterations?: number;
78
95
  featuredTag?: string;
79
96
  oldPriceLabel?: string;
80
97
  isDefault?: boolean;
@@ -99,6 +99,13 @@ const readProviderPlanId = (item, mode) => {
99
99
  ? readString(item.testProviderPlanId)
100
100
  : readString(item.liveProviderPlanId)));
101
101
  };
102
+ const readFollowUpProviderPlanId = (item, mode) => {
103
+ var _a, _b, _c;
104
+ const modeConfig = readGeneratedModeConfig(item, mode);
105
+ return ((_c = (_b = (_a = readString(modeConfig.followUpProviderPlanId)) !== null && _a !== void 0 ? _a : readString(modeConfig.follow_up_provider_plan_id)) !== null && _b !== void 0 ? _b : readString(item.followUpProviderPlanId)) !== null && _c !== void 0 ? _c : (mode === 'test'
106
+ ? readString(item.followUpTestProviderPlanId)
107
+ : readString(item.followUpLiveProviderPlanId)));
108
+ };
102
109
  const resolveOfferItemRuntimeKey = (item, baseCatalogsByMode) => {
103
110
  var _a, _b, _c, _d, _e;
104
111
  if (!item) {
@@ -126,9 +133,10 @@ const readGeneratedPlanNumberValue = (item, mode, itemKeys, metadataKeys = itemK
126
133
  return ((_b = (_a = readFiniteNumberField(modeConfig, metadataKeys)) !== null && _a !== void 0 ? _a : itemValue) !== null && _b !== void 0 ? _b : readFiniteNumberField(metadata, metadataKeys));
127
134
  };
128
135
  const buildGeneratedPlanOverrides = (item, mode, isDefault) => {
129
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
136
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
130
137
  const metadata = readOfferItemMetadata(item);
131
138
  const providerPlanId = readProviderPlanId(item, mode);
139
+ const followUpProviderPlanId = readFollowUpProviderPlanId(item, mode);
132
140
  const projectPlanId = (_a = readString(item.projectBillingPlanId)) !== null && _a !== void 0 ? _a : readStringField(metadata, ['projectBillingPlanId', 'projectPlanId', 'project_billing_plan_id']);
133
141
  const title = (_c = (_b = readString(item.title)) !== null && _b !== void 0 ? _b : readString(item.displayNameOverride)) !== null && _c !== void 0 ? _c : readStringField(metadata, ['title', 'displayName', 'publicPlanName', 'public_plan_name']);
134
142
  const description = readGeneratedPlanStringValue(item, mode, ['description']);
@@ -138,13 +146,17 @@ const buildGeneratedPlanOverrides = (item, mode, isDefault) => {
138
146
  const checkoutSummaryLabel = readGeneratedPlanStringValue(item, mode, ['checkoutSummaryLabel']);
139
147
  const billingInterval = readGeneratedPlanStringValue(item, mode, ['billingInterval']);
140
148
  const billingIntervalCount = readGeneratedPlanNumberValue(item, mode, ['billingIntervalCount']);
149
+ const followUpPlanTitle = (_e = (_d = readString(item.followUpPlanTitle)) !== null && _d !== void 0 ? _d : readString(item.followUpTitle)) !== null && _e !== void 0 ? _e : readStringField(metadata, ['followUpPlanTitle', 'followUpTitle', 'follow_up_plan_title']);
150
+ const followUpBillingInterval = readGeneratedPlanStringValue(item, mode, ['followUpBillingInterval'], ['followUpBillingInterval', 'follow_up_billing_interval']);
151
+ const followUpBillingIntervalCount = readGeneratedPlanNumberValue(item, mode, ['followUpBillingIntervalCount'], ['followUpBillingIntervalCount', 'follow_up_billing_interval_count']);
152
+ const introIterations = readGeneratedPlanNumberValue(item, mode, ['introIterations'], ['introIterations', 'intro_iterations']);
141
153
  const amountCents = readGeneratedPlanNumberValue(item, mode, ['amountCents']);
142
154
  const modeConfig = readGeneratedModeConfig(item, mode);
143
- const paymentMode = (_k = (_j = (_h = (_g = (_f = (_e = (_d = readOneTimePaymentMode(modeConfig.paymentMode)) !== null && _d !== void 0 ? _d : readOneTimePaymentMode(modeConfig.payment_mode)) !== null && _e !== void 0 ? _e : readOneTimePaymentMode(modeConfig.checkoutMode)) !== null && _f !== void 0 ? _f : readOneTimePaymentMode(modeConfig.checkout_mode)) !== null && _g !== void 0 ? _g : readOneTimePaymentMode(metadata.paymentMode)) !== null && _h !== void 0 ? _h : readOneTimePaymentMode(metadata.payment_mode)) !== null && _j !== void 0 ? _j : readOneTimePaymentMode(metadata.checkoutMode)) !== null && _k !== void 0 ? _k : readOneTimePaymentMode(metadata.checkout_mode);
144
- const analyticsPurchaseValue = (_l = readFiniteNumber(item.analyticsPurchaseValue)) !== null && _l !== void 0 ? _l : readFiniteNumberField(metadata, ['analyticsPurchaseValue']);
145
- const featuredTag = (_m = readString(item.featuredTag)) !== null && _m !== void 0 ? _m : readStringField(metadata, ['featuredTag']);
146
- const oldPriceLabel = (_o = readString(item.oldPriceLabel)) !== null && _o !== void 0 ? _o : readStringField(metadata, ['oldPriceLabel']);
147
- 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({}, (projectPlanId ? { projectPlanId } : {})), (providerPlanId ? { providerPlanId } : {})), (paymentMode ? { paymentMode } : {})), (analyticsPurchaseValue !== null ? { analyticsPurchaseValue } : {})), (title ? { title } : {})), (description ? { description } : {})), (priceLabel ? { priceLabel } : {})), (perDayAmount ? { perDayAmount } : {})), (perDayLabel ? { perDayLabel } : {})), (amountCents !== null ? { amountCents: Math.max(0, Math.round(amountCents)) } : {})), (checkoutSummaryLabel ? { checkoutSummaryLabel } : {})), (billingInterval ? { billingInterval } : {})), (billingIntervalCount !== null ? { billingIntervalCount: Math.max(1, Math.round(billingIntervalCount)) } : {})), (featuredTag ? { featuredTag } : {})), (oldPriceLabel ? { oldPriceLabel } : {})), (isDefault ? { isDefault: true } : {}));
155
+ const paymentMode = (_m = (_l = (_k = (_j = (_h = (_g = (_f = readOneTimePaymentMode(modeConfig.paymentMode)) !== null && _f !== void 0 ? _f : readOneTimePaymentMode(modeConfig.payment_mode)) !== null && _g !== void 0 ? _g : readOneTimePaymentMode(modeConfig.checkoutMode)) !== null && _h !== void 0 ? _h : readOneTimePaymentMode(modeConfig.checkout_mode)) !== null && _j !== void 0 ? _j : readOneTimePaymentMode(metadata.paymentMode)) !== null && _k !== void 0 ? _k : readOneTimePaymentMode(metadata.payment_mode)) !== null && _l !== void 0 ? _l : readOneTimePaymentMode(metadata.checkoutMode)) !== null && _m !== void 0 ? _m : readOneTimePaymentMode(metadata.checkout_mode);
156
+ const analyticsPurchaseValue = (_o = readFiniteNumber(item.analyticsPurchaseValue)) !== null && _o !== void 0 ? _o : readFiniteNumberField(metadata, ['analyticsPurchaseValue']);
157
+ const featuredTag = (_p = readString(item.featuredTag)) !== null && _p !== void 0 ? _p : readStringField(metadata, ['featuredTag']);
158
+ const oldPriceLabel = (_q = readString(item.oldPriceLabel)) !== null && _q !== void 0 ? _q : readStringField(metadata, ['oldPriceLabel']);
159
+ 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(Object.assign(Object.assign(Object.assign({}, (projectPlanId ? { projectPlanId } : {})), (providerPlanId ? { providerPlanId } : {})), (paymentMode ? { paymentMode } : {})), (analyticsPurchaseValue !== null ? { analyticsPurchaseValue } : {})), (title ? { title } : {})), (description ? { description } : {})), (priceLabel ? { priceLabel } : {})), (perDayAmount ? { perDayAmount } : {})), (perDayLabel ? { perDayLabel } : {})), (amountCents !== null ? { amountCents: Math.max(0, Math.round(amountCents)) } : {})), (checkoutSummaryLabel ? { checkoutSummaryLabel } : {})), (billingInterval ? { billingInterval } : {})), (billingIntervalCount !== null ? { billingIntervalCount: Math.max(1, Math.round(billingIntervalCount)) } : {})), (followUpProviderPlanId ? { followUpProviderPlanId } : {})), (followUpPlanTitle ? { followUpPlanTitle } : {})), (followUpBillingInterval ? { followUpBillingInterval } : {})), (followUpBillingIntervalCount !== null ? { followUpBillingIntervalCount: Math.max(1, Math.round(followUpBillingIntervalCount)) } : {})), (introIterations !== null ? { introIterations: Math.max(1, Math.round(introIterations)) } : {})), (featuredTag ? { featuredTag } : {})), (oldPriceLabel ? { oldPriceLabel } : {})), (isDefault ? { isDefault: true } : {}));
148
160
  };
149
161
  export const applyGeneratedOfferSetToPlanCatalog = (baseCatalog, mode, offerSet) => {
150
162
  var _a, _b, _c;
@@ -1,8 +1,9 @@
1
- import { buildSdkHeaders, FUNNEL_ID, getFunnelSdkPublishableKey, } from './runtime-api.config.js';
1
+ import { buildSdkHeaders, FUNNEL_ID, FUNNEL_VERSION_ID, getFunnelSdkPublishableKey, } from './runtime-api.config.js';
2
2
  import { funnelSdkService } from './funnel-sdk.service.js';
3
3
  import { mergeFunnelUserAttribution, } from '../runtime/funnel-attribution.js';
4
4
  import { applyUrlUserAttributesToUser, hasUrlUserProfileAttributes, resolveUrlUserAttributes, } from '../runtime/url-user-attributes.js';
5
5
  const DEFAULT_USER_ID_STORAGE_KEY = 'funnel:user-id';
6
+ const DEFAULT_BOOTSTRAP_IDEMPOTENCY_KEY_STORAGE_KEY = 'funnel:bootstrap-idempotency-key';
6
7
  const DEFAULT_FUNNEL_DOMAIN_SIGNAL = 'funnelsgrove';
7
8
  const canUseDom = () => {
8
9
  return typeof window !== 'undefined';
@@ -102,6 +103,100 @@ const buildUserIdStorageKey = (funnelId = FUNNEL_ID) => {
102
103
  }
103
104
  return `funnel:${normalizedFunnelId}:user-id`;
104
105
  };
106
+ const buildBootstrapIdempotencyKeyStorageKey = (funnelId = FUNNEL_ID) => {
107
+ const normalizedFunnelId = normalizeFunnelId(funnelId);
108
+ return normalizedFunnelId
109
+ ? `funnel:${normalizedFunnelId}:bootstrap-idempotency-key`
110
+ : DEFAULT_BOOTSTRAP_IDEMPOTENCY_KEY_STORAGE_KEY;
111
+ };
112
+ const generateBootstrapIdempotencyKey = () => {
113
+ var _a;
114
+ if (canUseDom() && typeof ((_a = window.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === 'function') {
115
+ return window.crypto.randomUUID();
116
+ }
117
+ return `${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
118
+ };
119
+ const parseBootstrapIdempotencyRecord = (value) => {
120
+ if (!value) {
121
+ return null;
122
+ }
123
+ try {
124
+ const parsed = JSON.parse(value);
125
+ const key = asString(parsed.key);
126
+ const fingerprint = asString(parsed.fingerprint);
127
+ return parsed.version === 1 && key && fingerprint
128
+ ? { version: 1, key, fingerprint }
129
+ : null;
130
+ }
131
+ catch (_a) {
132
+ return null;
133
+ }
134
+ };
135
+ const toStableJsonValue = (value) => {
136
+ if (Array.isArray(value)) {
137
+ return value.map(toStableJsonValue);
138
+ }
139
+ if (!isRecord(value)) {
140
+ return value;
141
+ }
142
+ return Object.keys(value).sort().reduce((result, key) => {
143
+ if (value[key] !== undefined) {
144
+ result[key] = toStableJsonValue(value[key]);
145
+ }
146
+ return result;
147
+ }, {});
148
+ };
149
+ const buildFallbackDigest = (value) => {
150
+ var _a;
151
+ const bytes = new TextEncoder().encode(value);
152
+ const hashes = Array.from({ length: 8 }, (_, index) => ((0x811c9dc5 ^ Math.imul(index + 1, 0x9e3779b1)) >>> 0));
153
+ for (const byte of bytes) {
154
+ for (let index = 0; index < hashes.length; index += 1) {
155
+ hashes[index] = Math.imul(((_a = hashes[index]) !== null && _a !== void 0 ? _a : 0) ^ byte, 0x01000193 + index * 2) >>> 0;
156
+ }
157
+ }
158
+ return hashes.map((hash) => hash.toString(16).padStart(8, '0')).join('');
159
+ };
160
+ const digestBootstrapRequest = async (value) => {
161
+ var _a;
162
+ const serialized = JSON.stringify(toStableJsonValue(value));
163
+ const bytes = new TextEncoder().encode(serialized);
164
+ const subtle = (_a = globalThis.crypto) === null || _a === void 0 ? void 0 : _a.subtle;
165
+ if (!subtle) {
166
+ return `fallback:${buildFallbackDigest(serialized)}`;
167
+ }
168
+ const digest = await subtle.digest('SHA-256', bytes);
169
+ const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
170
+ return `sha256:${hex}`;
171
+ };
172
+ const getOrCreateBootstrapIdempotencyRecord = (fingerprint) => {
173
+ const created = () => ({
174
+ version: 1,
175
+ key: generateBootstrapIdempotencyKey(),
176
+ fingerprint,
177
+ });
178
+ if (!canUseDom()) {
179
+ return created();
180
+ }
181
+ const storageKey = buildBootstrapIdempotencyKeyStorageKey();
182
+ const existing = parseBootstrapIdempotencyRecord(window.localStorage.getItem(storageKey));
183
+ if ((existing === null || existing === void 0 ? void 0 : existing.fingerprint) === fingerprint) {
184
+ return existing;
185
+ }
186
+ const next = created();
187
+ window.localStorage.setItem(storageKey, JSON.stringify(next));
188
+ return next;
189
+ };
190
+ const clearBootstrapIdempotencyRecord = (completed) => {
191
+ if (!canUseDom()) {
192
+ return;
193
+ }
194
+ const storageKey = buildBootstrapIdempotencyKeyStorageKey();
195
+ const current = parseBootstrapIdempotencyRecord(window.localStorage.getItem(storageKey));
196
+ if ((current === null || current === void 0 ? void 0 : current.key) === completed.key && current.fingerprint === completed.fingerprint) {
197
+ window.localStorage.removeItem(storageKey);
198
+ }
199
+ };
105
200
  const generateUserId = () => {
106
201
  var _a;
107
202
  if (canUseDom() && typeof ((_a = window.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === 'function') {
@@ -217,11 +312,12 @@ class ApiService {
217
312
  return persistUserId(resolvedCandidateUserId || fallbackUserId, FUNNEL_ID);
218
313
  }
219
314
  async bootstrapSession(input) {
220
- var _a;
315
+ var _a, _b, _c;
221
316
  const urlUserAttributes = resolveUrlUserAttributes();
222
317
  const inputEmail = (input === null || input === void 0 ? void 0 : input.email) || urlUserAttributes.email || undefined;
223
318
  const inputName = (input === null || input === void 0 ? void 0 : input.name) || urlUserAttributes.name || undefined;
224
- const userId = await this.resolveBootstrapUserId((input === null || input === void 0 ? void 0 : input.userId) || urlUserAttributes.userId);
319
+ const candidateUserId = (input === null || input === void 0 ? void 0 : input.userId) || urlUserAttributes.userId;
320
+ const userId = await this.resolveBootstrapUserId(candidateUserId);
225
321
  const publishableKey = getFunnelSdkPublishableKey();
226
322
  if (!publishableKey) {
227
323
  return toAppUser({
@@ -231,17 +327,29 @@ class ApiService {
231
327
  fallbackDocument: withMergedAttributionDocument({}, input === null || input === void 0 ? void 0 : input.attribution),
232
328
  });
233
329
  }
330
+ const metadata = {
331
+ source: 'funnel-session',
332
+ environment: resolveRuntimeAnalyticsEnvironment(),
333
+ };
334
+ const fingerprint = await digestBootstrapRequest({
335
+ funnelVersionId: asString(FUNNEL_VERSION_ID),
336
+ userId,
337
+ email: (_b = (_a = asString(inputEmail)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : null,
338
+ fullName: asString(inputName),
339
+ metadata,
340
+ attribution: mergeFunnelUserAttribution({}, input === null || input === void 0 ? void 0 : input.attribution),
341
+ });
342
+ const idempotencyRecord = getOrCreateBootstrapIdempotencyRecord(fingerprint);
234
343
  const payload = await funnelSdkService.bootstrapUser({
235
344
  userId,
345
+ idempotencyKey: idempotencyRecord.key,
236
346
  email: inputEmail,
237
347
  fullName: inputName,
238
- metadata: {
239
- source: 'funnel-session',
240
- environment: resolveRuntimeAnalyticsEnvironment(),
241
- },
348
+ metadata,
242
349
  attribution: input === null || input === void 0 ? void 0 : input.attribution,
243
350
  });
244
- const persistedUserId = persistUserId(asString((_a = payload.user) === null || _a === void 0 ? void 0 : _a.user_id) ||
351
+ clearBootstrapIdempotencyRecord(idempotencyRecord);
352
+ const persistedUserId = persistUserId(asString((_c = payload.user) === null || _c === void 0 ? void 0 : _c.user_id) ||
245
353
  asString(payload.user_id) ||
246
354
  userId, FUNNEL_ID) || userId;
247
355
  return toAppUser({
@@ -14,6 +14,7 @@ export type FunnelSdkListPaymentPlansResponse = {
14
14
  };
15
15
  export type FunnelSdkBootstrapUserInput = {
16
16
  userId?: string | null;
17
+ idempotencyKey?: string | null;
17
18
  email?: string | null;
18
19
  fullName?: string | null;
19
20
  metadata?: FunnelSdkJsonObject | null;
@@ -98,6 +99,11 @@ export type FunnelSdkCreateSubscriptionCheckoutInput = {
98
99
  analyticsMetadata?: Record<string, unknown> | null;
99
100
  billingInterval?: string | null;
100
101
  billingIntervalCount?: number | null;
102
+ followUpProviderPlanId?: string | null;
103
+ followUpPlanTitle?: string | null;
104
+ followUpBillingInterval?: string | null;
105
+ followUpBillingIntervalCount?: number | null;
106
+ introIterations?: number | null;
101
107
  couponId?: string | null;
102
108
  customerEmail?: string | null;
103
109
  description?: string | null;
@@ -141,6 +147,11 @@ export type FunnelSdkUpdateSubscriptionCheckoutPlanInput = {
141
147
  analyticsMetadata?: Record<string, unknown> | null;
142
148
  billingInterval?: string | null;
143
149
  billingIntervalCount?: number | null;
150
+ followUpProviderPlanId?: string | null;
151
+ followUpPlanTitle?: string | null;
152
+ followUpBillingInterval?: string | null;
153
+ followUpBillingIntervalCount?: number | null;
154
+ introIterations?: number | null;
144
155
  couponId?: string | null;
145
156
  customerEmail?: string | null;
146
157
  description?: string | null;
@@ -106,6 +106,7 @@ class FunnelSdkService {
106
106
  runtimeConfig: input.runtimeConfig,
107
107
  body: {
108
108
  user_id: input.userId,
109
+ idempotencyKey: input.idempotencyKey,
109
110
  email: input.email,
110
111
  fullName: input.fullName,
111
112
  metadata: input.metadata,
@@ -229,6 +230,11 @@ class FunnelSdkService {
229
230
  amountCents: input.amountCents,
230
231
  billingInterval: input.billingInterval,
231
232
  billingIntervalCount: input.billingIntervalCount,
233
+ followUpProviderPlanId: input.followUpProviderPlanId,
234
+ followUpPlanTitle: input.followUpPlanTitle,
235
+ followUpBillingInterval: input.followUpBillingInterval,
236
+ followUpBillingIntervalCount: input.followUpBillingIntervalCount,
237
+ introIterations: input.introIterations,
232
238
  couponId: input.couponId,
233
239
  analyticsMetadata: input.analyticsMetadata,
234
240
  customerEmail: input.customerEmail,
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
7
+ "directory": "apps/funnel-runtime"
8
+ },
4
9
  "type": "module",
5
10
  "private": false,
6
11
  "main": "./dist/index.js",