@funnelsgrove/payments 0.7.21 → 0.8.0

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
@@ -98,7 +98,7 @@ When a live funnel still uses one of these, migrate the funnel deliberately with
98
98
  - `providers/solidgate/components/*`: the official-SDK checkout dialog and canonical verification lifecycle.
99
99
  - `components/ManageSubscriptionScreen`: provider-neutral subscription management UI.
100
100
 
101
- Funnels should call `resolvePublishedBillingRuntime` once at their runtime-provider boundary. Screens consume its resolved view and should not parse offer sets or pricing-experiment variants themselves.
101
+ Funnels should call `resolvePublishedBillingRuntime` once at their runtime-provider boundary. Screens consume its resolved view and should not parse offer sets or pricing-experiment variants themselves. Pass the resolved `stripePublishableKey` into shared Stripe checkout hooks so Stripe.js can load while the Checkout Session request is still in flight; the key returned by that request remains the compatibility fallback.
102
102
 
103
103
  ## Engineering Rules
104
104
 
@@ -1,3 +1,2 @@
1
- export * from './useResolvedPaywallPlans.js';
2
1
  export * from './useStripeOneTimeCheckoutSession.js';
3
2
  export * from './useStripeSubscriptionCheckoutSession.js';
@@ -1,3 +1,2 @@
1
- export * from './useResolvedPaywallPlans.js';
2
1
  export * from './useStripeOneTimeCheckoutSession.js';
3
2
  export * from './useStripeSubscriptionCheckoutSession.js';
@@ -13,6 +13,7 @@ export type StripeOneTimeCheckoutSessionInput = {
13
13
  plan?: StripeOneTimeCheckoutPlanInput | null;
14
14
  returnUrl: string;
15
15
  runtimeConfig?: StripeRuntimeConfigOverrides;
16
+ stripePublishableKey?: string | null;
16
17
  userId?: string | null;
17
18
  };
18
19
  export type StripeOneTimeCheckoutPreparationOptions = {
@@ -39,10 +40,10 @@ export type StripeOneTimeCheckoutSession = {
39
40
  stripePromise: Promise<Stripe | null> | null;
40
41
  };
41
42
  export declare const buildStripeOneTimeCheckoutPreparationAnalyticsMetadata: (analyticsMetadata: Record<string, unknown> | null | undefined, options?: StripeOneTimeCheckoutPreparationOptions) => Record<string, unknown> | null | undefined;
42
- export type StripeOneTimeCheckoutIntentKeyInput = Pick<StripeOneTimeCheckoutSessionInput, 'checkoutMode' | 'couponId' | 'customerEmail' | 'plan' | 'returnUrl' | 'runtimeConfig' | 'userId'> & {
43
+ export type StripeOneTimeCheckoutIntentKeyInput = Pick<StripeOneTimeCheckoutSessionInput, 'checkoutMode' | 'couponId' | 'customerEmail' | 'plan' | 'returnUrl' | 'runtimeConfig' | 'stripePublishableKey' | 'userId'> & {
43
44
  analyticsMetadata?: Record<string, unknown> | null;
44
45
  };
45
- export declare function buildStripeOneTimeCheckoutIntentKey({ checkoutMode, couponId, customerEmail, plan, returnUrl, runtimeConfig, userId, }: StripeOneTimeCheckoutIntentKeyInput): string;
46
+ export declare function buildStripeOneTimeCheckoutIntentKey({ checkoutMode, couponId, customerEmail, plan, returnUrl, runtimeConfig, stripePublishableKey, userId, }: StripeOneTimeCheckoutIntentKeyInput): string;
46
47
  export declare function shouldResetStripeOneTimeCheckoutSessionForIntentKey(previousIntentKey: string | null, nextIntentKey: string): boolean;
47
- export declare function useStripeOneTimeCheckoutSession({ analyticsMetadata, checkoutMode, couponId, customerEmail, enabled, onError, plan, returnUrl, runtimeConfig, userId, }: StripeOneTimeCheckoutSessionInput): StripeOneTimeCheckoutSession;
48
+ export declare function useStripeOneTimeCheckoutSession({ analyticsMetadata, checkoutMode, couponId, customerEmail, enabled, onError, plan, returnUrl, runtimeConfig, stripePublishableKey, userId, }: StripeOneTimeCheckoutSessionInput): StripeOneTimeCheckoutSession;
48
49
  export {};
@@ -36,13 +36,14 @@ export const buildStripeOneTimeCheckoutPreparationAnalyticsMetadata = (analytics
36
36
  const _b = analyticsMetadata !== null && analyticsMetadata !== void 0 ? analyticsMetadata : {}, { payment_method: _paymentMethod, paymentMethod: _camelCasePaymentMethod } = _b, metadata = __rest(_b, ["payment_method", "paymentMethod"]);
37
37
  return Object.assign(Object.assign(Object.assign({}, metadata), (checkoutStartSource ? { checkoutStartSource } : {})), (paymentMethod ? { payment_method: paymentMethod } : {}));
38
38
  };
39
- export function buildStripeOneTimeCheckoutIntentKey({ checkoutMode, couponId, customerEmail, plan, returnUrl, runtimeConfig, userId, }) {
39
+ export function buildStripeOneTimeCheckoutIntentKey({ checkoutMode, couponId, customerEmail, plan, returnUrl, runtimeConfig, stripePublishableKey, userId, }) {
40
40
  var _a;
41
41
  const runtimeConfigSignature = [
42
42
  normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.apiBaseUrl),
43
43
  normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelId),
44
44
  normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelVersionId),
45
45
  normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey),
46
+ normalizeRuntimeConfigValue(stripePublishableKey),
46
47
  ].join('|');
47
48
  const planSignature = [
48
49
  normalizeRuntimeConfigValue(plan === null || plan === void 0 ? void 0 : plan.id),
@@ -65,7 +66,7 @@ export function buildStripeOneTimeCheckoutIntentKey({ checkoutMode, couponId, cu
65
66
  export function shouldResetStripeOneTimeCheckoutSessionForIntentKey(previousIntentKey, nextIntentKey) {
66
67
  return previousIntentKey !== null && previousIntentKey !== nextIntentKey;
67
68
  }
68
- export function useStripeOneTimeCheckoutSession({ analyticsMetadata, checkoutMode, couponId, customerEmail, enabled = true, onError, plan, returnUrl, runtimeConfig, userId, }) {
69
+ export function useStripeOneTimeCheckoutSession({ analyticsMetadata, checkoutMode, couponId, customerEmail, enabled = true, onError, plan, returnUrl, runtimeConfig, stripePublishableKey, userId, }) {
69
70
  var _a, _b, _c;
70
71
  const [clientSecret, setClientSecret] = useState(null);
71
72
  const [clientSecretIntentKey, setClientSecretIntentKey] = useState(null);
@@ -77,7 +78,8 @@ export function useStripeOneTimeCheckoutSession({ analyticsMetadata, checkoutMod
77
78
  const requestIdRef = useRef(0);
78
79
  const configured = enabled &&
79
80
  (isStripeConfigured(checkoutMode) ||
80
- Boolean(normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey)));
81
+ Boolean(normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey)) ||
82
+ Boolean(normalizeRuntimeConfigValue(stripePublishableKey)));
81
83
  const intentKey = buildStripeOneTimeCheckoutIntentKey({
82
84
  checkoutMode,
83
85
  couponId,
@@ -85,6 +87,7 @@ export function useStripeOneTimeCheckoutSession({ analyticsMetadata, checkoutMod
85
87
  plan,
86
88
  returnUrl,
87
89
  runtimeConfig,
90
+ stripePublishableKey,
88
91
  userId,
89
92
  });
90
93
  const planKey = [
@@ -94,7 +97,10 @@ export function useStripeOneTimeCheckoutSession({ analyticsMetadata, checkoutMod
94
97
  ].join('|');
95
98
  const activeClientSecret = clientSecretIntentKey === intentKey ? clientSecret : null;
96
99
  const activeCheckoutSessionId = clientSecretIntentKey === intentKey ? checkoutSessionId : null;
97
- const stripePromise = useMemo(() => getStripePromise(checkoutMode, runtimeStripePublishableKey !== null && runtimeStripePublishableKey !== void 0 ? runtimeStripePublishableKey : runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey), [checkoutMode, runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey, runtimeStripePublishableKey]);
100
+ const stripePromise = useMemo(() => {
101
+ var _a;
102
+ return getStripePromise(checkoutMode, (_a = runtimeStripePublishableKey !== null && runtimeStripePublishableKey !== void 0 ? runtimeStripePublishableKey : stripePublishableKey) !== null && _a !== void 0 ? _a : runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey);
103
+ }, [checkoutMode, runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey, runtimeStripePublishableKey, stripePublishableKey]);
98
104
  const previousIntentKeyRef = useRef(intentKey);
99
105
  const analyticsMetadataRef = useRef(analyticsMetadata);
100
106
  useEffect(() => {
@@ -14,6 +14,7 @@ export type StripeSubscriptionCheckoutSessionInput = {
14
14
  plan?: PaywallPlan | null;
15
15
  returnUrl: string;
16
16
  runtimeConfig?: StripeRuntimeConfigOverrides;
17
+ stripePublishableKey?: string | null;
17
18
  userId?: string | null;
18
19
  };
19
20
  export type StripeSubscriptionCheckoutPreparationOptions = {
@@ -42,10 +43,10 @@ export type StripeSubscriptionCheckoutSession = {
42
43
  updateCheckoutSessionPlan: () => Promise<boolean>;
43
44
  };
44
45
  export declare const buildStripeSubscriptionCheckoutPreparationAnalyticsMetadata: (analyticsMetadata: Record<string, unknown> | null | undefined, options?: StripeSubscriptionCheckoutPreparationOptions) => Record<string, unknown> | null | undefined;
45
- export type StripeSubscriptionCheckoutIntentKeyInput = Pick<StripeSubscriptionCheckoutSessionInput, 'checkoutMode' | 'couponId' | 'customerEmail' | 'returnUrl' | 'runtimeConfig' | 'userId'> & {
46
+ export type StripeSubscriptionCheckoutIntentKeyInput = Pick<StripeSubscriptionCheckoutSessionInput, 'checkoutMode' | 'couponId' | 'customerEmail' | 'returnUrl' | 'runtimeConfig' | 'stripePublishableKey' | 'userId'> & {
46
47
  analyticsMetadata?: Record<string, unknown> | null;
47
48
  };
48
- export declare function buildStripeSubscriptionCheckoutIntentKey({ checkoutMode, couponId, customerEmail, returnUrl, runtimeConfig, userId, }: StripeSubscriptionCheckoutIntentKeyInput): string;
49
+ export declare function buildStripeSubscriptionCheckoutIntentKey({ checkoutMode, couponId, customerEmail, returnUrl, runtimeConfig, stripePublishableKey, userId, }: StripeSubscriptionCheckoutIntentKeyInput): string;
49
50
  export declare function shouldResetStripeSubscriptionCheckoutSessionForIntentKey(previousIntentKey: string | null, nextIntentKey: string): boolean;
50
51
  export declare function isInactiveCheckoutSessionError(message?: string | null): boolean;
51
- export declare function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, couponId, customerEmail, displayPlan, enabled, onError, plan, returnUrl, runtimeConfig, userId, }: StripeSubscriptionCheckoutSessionInput): StripeSubscriptionCheckoutSession;
52
+ export declare function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, couponId, customerEmail, displayPlan, enabled, onError, plan, returnUrl, runtimeConfig, stripePublishableKey, userId, }: StripeSubscriptionCheckoutSessionInput): StripeSubscriptionCheckoutSession;
@@ -35,12 +35,13 @@ export const buildStripeSubscriptionCheckoutPreparationAnalyticsMetadata = (anal
35
35
  const _b = analyticsMetadata !== null && analyticsMetadata !== void 0 ? analyticsMetadata : {}, { payment_method: _paymentMethod, paymentMethod: _camelCasePaymentMethod } = _b, metadata = __rest(_b, ["payment_method", "paymentMethod"]);
36
36
  return Object.assign(Object.assign(Object.assign({}, metadata), (checkoutStartSource ? { checkoutStartSource } : {})), (paymentMethod ? { payment_method: paymentMethod } : {}));
37
37
  };
38
- export function buildStripeSubscriptionCheckoutIntentKey({ checkoutMode, couponId, customerEmail, returnUrl, runtimeConfig, userId, }) {
38
+ export function buildStripeSubscriptionCheckoutIntentKey({ checkoutMode, couponId, customerEmail, returnUrl, runtimeConfig, stripePublishableKey, userId, }) {
39
39
  const runtimeConfigSignature = [
40
40
  normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.apiBaseUrl),
41
41
  normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelId),
42
42
  normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelVersionId),
43
43
  normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey),
44
+ normalizeRuntimeConfigValue(stripePublishableKey),
44
45
  ].join('|');
45
46
  const customerEmailSignature = normalizeRuntimeConfigValue(customerEmail);
46
47
  const userIdSignature = normalizeRuntimeConfigValue(userId);
@@ -61,7 +62,7 @@ export function isInactiveCheckoutSessionError(message) {
61
62
  var _a;
62
63
  return /\bcheckout session\b.*\bno longer active\b/i.test((_a = message === null || message === void 0 ? void 0 : message.trim()) !== null && _a !== void 0 ? _a : '');
63
64
  }
64
- export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, couponId, customerEmail, displayPlan, enabled = true, onError, plan, returnUrl, runtimeConfig, userId, }) {
65
+ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, couponId, customerEmail, displayPlan, enabled = true, onError, plan, returnUrl, runtimeConfig, stripePublishableKey, userId, }) {
65
66
  var _a, _b, _c, _d, _e;
66
67
  const [clientSecret, setClientSecret] = useState(null);
67
68
  const [clientSecretIntentKey, setClientSecretIntentKey] = useState(null);
@@ -74,13 +75,15 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
74
75
  const requestIdRef = useRef(0);
75
76
  const configured = enabled &&
76
77
  (isStripeConfigured(checkoutMode) ||
77
- Boolean(normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey)));
78
+ Boolean(normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey)) ||
79
+ Boolean(normalizeRuntimeConfigValue(stripePublishableKey)));
78
80
  const intentKey = buildStripeSubscriptionCheckoutIntentKey({
79
81
  checkoutMode,
80
82
  couponId,
81
83
  customerEmail,
82
84
  returnUrl,
83
85
  runtimeConfig,
86
+ stripePublishableKey,
84
87
  userId,
85
88
  });
86
89
  const planKey = [
@@ -93,7 +96,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
93
96
  const activeClientSecret = clientSecretIntentKey === intentKey ? clientSecret : null;
94
97
  const activeCheckoutSessionId = clientSecretIntentKey === intentKey ? checkoutSessionId : null;
95
98
  const activePlanKey = clientSecretIntentKey === intentKey ? clientSecretPlanKey : null;
96
- const stripePromise = useMemo(() => getStripePromise(checkoutMode, runtimeStripePublishableKey), [checkoutMode, runtimeStripePublishableKey]);
99
+ const stripePromise = useMemo(() => getStripePromise(checkoutMode, runtimeStripePublishableKey !== null && runtimeStripePublishableKey !== void 0 ? runtimeStripePublishableKey : stripePublishableKey), [checkoutMode, runtimeStripePublishableKey, stripePublishableKey]);
97
100
  const previousIntentKeyRef = useRef(intentKey);
98
101
  const analyticsMetadataRef = useRef(analyticsMetadata);
99
102
  useEffect(() => {
@@ -2,7 +2,6 @@ import { type Stripe } from '@stripe/stripe-js';
2
2
  import { type BillingFallbackPlanConfig, type BillingPlanCatalog, type BillingPlanInterval, type BillingPlanInputCatalog } from '../../../config/billing.config.js';
3
3
  import { type FunnelSdkRuntimeConfigOverrides, type ResolvedCountryPricing } from '@funnelsgrove/runtime';
4
4
  import type { RuntimeMode } from '@funnelsgrove/runtime';
5
- import { type RemoteProjectBillingPlan } from '../../../services/planCatalog.service.js';
6
5
  export type PaywallPlan = {
7
6
  id: string;
8
7
  funnelPlanKey?: string;
@@ -36,7 +35,6 @@ export type PaywallPlanResolutionOptions = {
36
35
  locale?: string | null;
37
36
  pricing?: ResolvedCountryPricing<string> | null;
38
37
  };
39
- export declare const buildPaywallPlanResolutionSignature: (options?: PaywallPlanResolutionOptions) => string;
40
38
  export declare function buildConfigPaywallPlans(planCatalog?: BillingPlanCatalog | readonly BillingFallbackPlanConfig[], orderedPlanIds?: readonly string[]): readonly PaywallPlan[];
41
39
  export declare function getFallbackPlans(planCatalog?: BillingPlanInputCatalog | readonly BillingFallbackPlanConfig[], options?: PaywallPlanResolutionOptions): readonly PaywallPlan[];
42
40
  export declare function getOrderedPaywallPlans(plans: readonly PaywallPlan[], orderedPlanIds: readonly string[]): readonly PaywallPlan[];
@@ -44,9 +42,6 @@ export declare function getDefaultPlanId(plans: readonly PaywallPlan[], pricing?
44
42
  export declare function getPaywallPlanSelectionValue(plan: Pick<PaywallPlan, 'id' | 'providerPlanId'>): string;
45
43
  export declare function findPaywallPlan(plans: readonly PaywallPlan[], selectionValue: string | null | undefined): PaywallPlan | null;
46
44
  export declare function getDefaultPlanSelectionValue(plans: readonly PaywallPlan[], pricing?: ResolvedCountryPricing<string> | null): string | null;
47
- export declare function getStripeSyncedPlans(mode: RuntimeMode, planCatalog?: BillingPlanInputCatalog | readonly BillingFallbackPlanConfig[], options?: PaywallPlanResolutionOptions): Promise<readonly PaywallPlan[]>;
48
- export declare function resolvePaywallPlansFromProjectPlans(projectPlans: readonly RemoteProjectBillingPlan[], planCatalog?: BillingPlanInputCatalog | readonly BillingFallbackPlanConfig[], options?: PaywallPlanResolutionOptions): readonly PaywallPlan[];
49
- export declare function getPaywallPlans(mode: RuntimeMode, planCatalog?: BillingPlanInputCatalog | readonly BillingFallbackPlanConfig[], options?: PaywallPlanResolutionOptions): Promise<readonly PaywallPlan[]>;
50
45
  export declare function isStripeConfigured(mode: RuntimeMode): boolean;
51
46
  export declare function getStripePromise(_mode: RuntimeMode, runtimePublishableKey?: string | null): Promise<Stripe | null> | null;
52
47
  type CheckoutSessionPlanInput = Pick<PaywallPlan, 'amountCents' | 'analyticsPurchaseValue' | 'billingInterval' | 'billingIntervalCount' | 'description' | 'id' | 'providerPlanId' | 'title' | 'followUpProviderPlanId' | 'followUpPlanTitle' | 'followUpBillingInterval' | 'followUpBillingIntervalCount' | 'introIterations' | 'offerSetId' | 'offerSetKey'>;
@@ -1,18 +1,8 @@
1
1
  import { loadStripe } from '@stripe/stripe-js';
2
2
  import { billingPlanList, } from '../../../config/billing.config.js';
3
- import { FUNNEL_ID, FUNNEL_SDK_PUBLISHABLE_KEY, funnelSdkService, } from '@funnelsgrove/runtime';
4
- import { isBillingPlanMappingCatalog, resolveMappedProjectPlans, } from '../../../services/planCatalog.service.js';
3
+ import { FUNNEL_SDK_PUBLISHABLE_KEY, funnelSdkService, } from '@funnelsgrove/runtime';
4
+ import { isBillingPlanMappingCatalog } from '../../../services/planCatalog.service.js';
5
5
  const stripePromiseByKey = new Map();
6
- const syncedPlansPromiseByMode = new Map();
7
- export const buildPaywallPlanResolutionSignature = (options) => {
8
- var _a, _b, _c, _d;
9
- const locale = ((_a = options === null || options === void 0 ? void 0 : options.locale) === null || _a === void 0 ? void 0 : _a.trim()) || '';
10
- const pricing = options === null || options === void 0 ? void 0 : options.pricing;
11
- const countryCode = ((_b = pricing === null || pricing === void 0 ? void 0 : pricing.countryCode) === null || _b === void 0 ? void 0 : _b.trim()) || '';
12
- const defaultOfferKey = ((_c = pricing === null || pricing === void 0 ? void 0 : pricing.defaultOfferKey) === null || _c === void 0 ? void 0 : _c.trim()) || '';
13
- const offerOrder = ((_d = pricing === null || pricing === void 0 ? void 0 : pricing.offerOrder) === null || _d === void 0 ? void 0 : _d.map((offerKey) => offerKey.trim()).join('|')) || '';
14
- return [locale, countryCode, defaultOfferKey, offerOrder].join('::');
15
- };
16
6
  const isRecord = (value) => {
17
7
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
18
8
  };
@@ -97,74 +87,6 @@ const toConfigPaywallPlan = (key, plan) => {
97
87
  return Object.assign(Object.assign({}, plan), { id: ((_a = plan.id) === null || _a === void 0 ? void 0 : _a.trim()) || key.trim() || ((_b = plan.providerPlanId) === null || _b === void 0 ? void 0 : _b.trim()) || plan.projectPlanId.trim(), perDayLabel: ((_c = plan.perDayLabel) === null || _c === void 0 ? void 0 : _c.trim()) || 'per day', billingInterval: interval.interval, billingIntervalCount: interval.intervalCount, source: 'config' });
98
88
  };
99
89
  const fallbackPaywallPlans = buildConfigPaywallPlans(billingPlanList);
100
- const toMinorAmount = (value) => {
101
- const numeric = asFiniteNumberOrNull(value);
102
- if (numeric === null || numeric <= 0) {
103
- return null;
104
- }
105
- return Math.round(numeric);
106
- };
107
- const toPlainObject = (value) => {
108
- return isRecord(value) ? value : {};
109
- };
110
- const toCurrencyCode = (value) => {
111
- const raw = asTrimmedStringOrNull(value);
112
- if (!raw) {
113
- return 'USD';
114
- }
115
- return raw.toUpperCase();
116
- };
117
- const resolveFormattingLocale = (locale) => {
118
- const trimmedLocale = typeof locale === 'string' ? locale.trim() : '';
119
- return trimmedLocale || 'en-US';
120
- };
121
- const formatMoneyFromMinor = (amountMinor, currencyCode, locale) => {
122
- return new Intl.NumberFormat(resolveFormattingLocale(locale), {
123
- style: 'currency',
124
- currency: currencyCode,
125
- minimumFractionDigits: 2,
126
- maximumFractionDigits: 2,
127
- }).format(amountMinor / 100);
128
- };
129
- const formatMoneyFromMajor = (amountMajor, currencyCode, locale) => {
130
- return new Intl.NumberFormat(resolveFormattingLocale(locale), {
131
- style: 'currency',
132
- currency: currencyCode,
133
- minimumFractionDigits: 2,
134
- maximumFractionDigits: 2,
135
- }).format(amountMajor);
136
- };
137
- const getIntervalLabel = (interval, intervalCount) => {
138
- if (!interval) {
139
- return '';
140
- }
141
- const normalized = interval.toLowerCase();
142
- const count = intervalCount && intervalCount > 0 ? intervalCount : 1;
143
- if (count === 1) {
144
- return normalized;
145
- }
146
- return `${count} ${normalized}s`;
147
- };
148
- const getIntervalDays = (interval, intervalCount) => {
149
- if (!interval) {
150
- return null;
151
- }
152
- const normalized = interval.toLowerCase();
153
- const count = intervalCount && intervalCount > 0 ? intervalCount : 1;
154
- if (normalized === 'day') {
155
- return count;
156
- }
157
- if (normalized === 'week') {
158
- return count * 7;
159
- }
160
- if (normalized === 'month') {
161
- return count * 30;
162
- }
163
- if (normalized === 'year') {
164
- return count * 365;
165
- }
166
- return null;
167
- };
168
90
  const reorderPlans = (plans, pricing) => {
169
91
  if (!pricing || pricing.offerOrder.length === 0) {
170
92
  return plans;
@@ -185,108 +107,6 @@ const reorderPlans = (plans, pricing) => {
185
107
  return leftIndex - rightIndex;
186
108
  });
187
109
  };
188
- const toRemoteProjectBillingPlan = (rawPlan) => {
189
- var _a, _b;
190
- const providerPlanId = asTrimmedStringOrNull(rawPlan.providerPlanId);
191
- if (!providerPlanId) {
192
- return null;
193
- }
194
- return {
195
- id: asTrimmedStringOrNull(rawPlan.id) || providerPlanId,
196
- key: asTrimmedStringOrNull(rawPlan.key) || providerPlanId,
197
- offerSetId: asTrimmedStringOrNull(rawPlan.offerSetId) || undefined,
198
- offerSetKey: asTrimmedStringOrNull(rawPlan.offerSetKey) || undefined,
199
- projectPlanId: asTrimmedStringOrNull(rawPlan.projectPlanId),
200
- providerPlanId,
201
- displayName: asTrimmedStringOrNull(rawPlan.displayName),
202
- description: asTrimmedStringOrNull(rawPlan.description),
203
- interval: (_b = (_a = asTrimmedStringOrNull(rawPlan.interval)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : null,
204
- intervalCount: asPositiveIntOrNull(rawPlan.intervalCount),
205
- trialPeriodDays: asPositiveIntOrNull(rawPlan.trialPeriodDays),
206
- currency: asTrimmedStringOrNull(rawPlan.currency),
207
- amountMinor: toMinorAmount(rawPlan.amountMinor),
208
- amountDecimal: asTrimmedStringOrNull(rawPlan.amountDecimal),
209
- metadata: toPlainObject(rawPlan.metadata),
210
- isActive: rawPlan.isActive !== false,
211
- };
212
- };
213
- const toPlanFromRemoteProjectPlan = (rawPlan, options, planIdOverride) => {
214
- var _a;
215
- const planId = (planIdOverride === null || planIdOverride === void 0 ? void 0 : planIdOverride.trim()) || rawPlan.id || rawPlan.key || rawPlan.providerPlanId;
216
- if (!planId) {
217
- return null;
218
- }
219
- if (!rawPlan.isActive) {
220
- return null;
221
- }
222
- const amountMinor = rawPlan.amountMinor;
223
- if (!amountMinor) {
224
- return null;
225
- }
226
- const metadata = rawPlan.metadata;
227
- const currencyCode = toCurrencyCode(rawPlan.currency);
228
- const interval = rawPlan.interval;
229
- const intervalCount = rawPlan.intervalCount;
230
- const intervalLabel = getIntervalLabel(interval, intervalCount);
231
- const intervalDays = getIntervalDays(interval, intervalCount);
232
- const locale = options === null || options === void 0 ? void 0 : options.locale;
233
- const title = rawPlan.displayName || planId;
234
- const priceLabel = intervalLabel
235
- ? `${formatMoneyFromMinor(amountMinor, currencyCode, locale)}/${intervalLabel}`
236
- : formatMoneyFromMinor(amountMinor, currencyCode, locale);
237
- const perDayAmount = intervalDays
238
- ? formatMoneyFromMajor(amountMinor / 100 / intervalDays, currencyCode, locale)
239
- : formatMoneyFromMinor(amountMinor, currencyCode, locale);
240
- return {
241
- id: planId,
242
- offerSetId: rawPlan.offerSetId,
243
- offerSetKey: rawPlan.offerSetKey,
244
- title,
245
- description: rawPlan.description || asTrimmedStringOrNull(metadata.description) || undefined,
246
- providerPlanId: rawPlan.providerPlanId,
247
- priceLabel,
248
- oldPriceLabel: asTrimmedStringOrNull(metadata.oldPriceLabel) || undefined,
249
- featuredTag: asTrimmedStringOrNull(metadata.featuredTag) || undefined,
250
- perDayAmount,
251
- perDayLabel: 'per day',
252
- amountCents: amountMinor,
253
- analyticsPurchaseValue: (_a = asNonNegativeNumberOrNull(metadata.analyticsPurchaseValue)) !== null && _a !== void 0 ? _a : undefined,
254
- comparison: intervalDays
255
- ? {
256
- perDay: `${perDayAmount}/day`,
257
- perWeek: `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 7, currencyCode, locale)}/week`,
258
- perMonth: interval === 'month' || interval === 'year'
259
- ? `${formatMoneyFromMajor(interval === 'year'
260
- ? amountMinor / 100 / ((intervalCount || 1) * 12)
261
- : amountMinor / 100 / (intervalCount || 1), currencyCode, locale)}/month`
262
- : `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 30, currencyCode, locale)}/month`,
263
- perYear: interval === 'year'
264
- ? `${formatMoneyFromMinor(amountMinor / (intervalCount || 1), currencyCode, locale)}/year`
265
- : `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 365, currencyCode, locale)}/year`,
266
- }
267
- : undefined,
268
- checkoutOriginalAmountCents: asPositiveIntOrNull(metadata.checkoutOriginalAmountCents) || undefined,
269
- checkoutSummaryLabel: asTrimmedStringOrNull(metadata.checkoutSummaryLabel) || undefined,
270
- billingInterval: asBillingPlanIntervalOrNull(interval) || undefined,
271
- billingIntervalCount: intervalCount || undefined,
272
- followUpProviderPlanId: asTrimmedStringOrNull(metadata.followUpProviderPlanId) || undefined,
273
- followUpPlanTitle: asTrimmedStringOrNull(metadata.followUpPlanTitle) || undefined,
274
- followUpBillingInterval: asBillingPlanIntervalOrNull(metadata.followUpBillingInterval) || undefined,
275
- followUpBillingIntervalCount: asPositiveIntOrNull(metadata.followUpBillingIntervalCount) || undefined,
276
- introIterations: asPositiveIntOrNull(metadata.introIterations) || undefined,
277
- source: 'stripe',
278
- };
279
- };
280
- const addFeaturedTagFallback = (plans) => {
281
- if (plans.length === 0) {
282
- return plans;
283
- }
284
- if (plans.some((plan) => Boolean(plan.featuredTag))) {
285
- return plans;
286
- }
287
- return plans.map((plan, index) => index === 0
288
- ? Object.assign(Object.assign({}, plan), { featuredTag: '🔥 Most Popular Choice' }) : plan);
289
- };
290
110
  export function buildConfigPaywallPlans(planCatalog, orderedPlanIds = []) {
291
111
  const plans = Array.isArray(planCatalog)
292
112
  ? planCatalog.map((plan, index) => toConfigPaywallPlan(String(index), plan))
@@ -334,12 +154,6 @@ export function getOrderedPaywallPlans(plans, orderedPlanIds) {
334
154
  }
335
155
  return selectedPlans;
336
156
  }
337
- const shouldLoadStripePlans = (planCatalog) => {
338
- if (!planCatalog) {
339
- return true;
340
- }
341
- return !Array.isArray(planCatalog) && isBillingPlanMappingCatalog(planCatalog);
342
- };
343
157
  export function getDefaultPlanId(plans, pricing) {
344
158
  var _a, _b;
345
159
  if (plans.length === 0) {
@@ -376,74 +190,6 @@ export function getDefaultPlanSelectionValue(plans, pricing) {
376
190
  }
377
191
  return getPaywallPlanSelectionValue(plans.find((plan) => plan.id === defaultPlanId) || plans[0]);
378
192
  }
379
- const getStripeSyncedPlanRecords = async (mode, offerSet) => {
380
- const cacheKey = `${mode}:${FUNNEL_ID || 'no-funnel'}:${FUNNEL_SDK_PUBLISHABLE_KEY}:${offerSet.offerSetId || ''}:${offerSet.offerSetKey || ''}`;
381
- const cachedPromise = syncedPlansPromiseByMode.get(cacheKey);
382
- if (cachedPromise) {
383
- return cachedPromise;
384
- }
385
- const nextPromise = (async () => {
386
- const payload = await funnelSdkService.listPaymentPlans({
387
- environment: mode,
388
- offerSetId: offerSet.offerSetId,
389
- offerSetKey: offerSet.offerSetKey,
390
- });
391
- const rawPlans = Array.isArray(payload.plans) ? payload.plans : [];
392
- return rawPlans
393
- .filter((rawPlan) => isRecord(rawPlan))
394
- .map((rawPlan) => toRemoteProjectBillingPlan(rawPlan))
395
- .filter((rawPlan) => Boolean(rawPlan));
396
- })();
397
- syncedPlansPromiseByMode.set(cacheKey, nextPromise);
398
- return nextPromise;
399
- };
400
- const resolvePlanCatalogOfferSet = (planCatalog) => {
401
- const configuredPlans = planCatalog
402
- ? (Array.isArray(planCatalog) ? planCatalog : Object.values(planCatalog))
403
- : [];
404
- const offerSetIds = [...new Set(configuredPlans
405
- .map((plan) => { var _a; return (_a = plan.offerSetId) === null || _a === void 0 ? void 0 : _a.trim(); })
406
- .filter((value) => Boolean(value)))];
407
- const offerSetKeys = [...new Set(configuredPlans
408
- .map((plan) => { var _a; return (_a = plan.offerSetKey) === null || _a === void 0 ? void 0 : _a.trim(); })
409
- .filter((value) => Boolean(value)))];
410
- return {
411
- offerSetId: offerSetIds.length === 1 ? offerSetIds[0] : undefined,
412
- offerSetKey: offerSetKeys.length === 1 ? offerSetKeys[0] : undefined,
413
- };
414
- };
415
- export async function getStripeSyncedPlans(mode, planCatalog, options) {
416
- const rawPlans = await getStripeSyncedPlanRecords(mode, resolvePlanCatalogOfferSet(planCatalog));
417
- const mappedPlans = (planCatalog && !Array.isArray(planCatalog) && isBillingPlanMappingCatalog(planCatalog)
418
- ? resolveMappedProjectPlans(planCatalog, rawPlans).map((rawPlan) => toPlanFromRemoteProjectPlan(rawPlan, options, rawPlan.funnelKey))
419
- : rawPlans.map((rawPlan) => toPlanFromRemoteProjectPlan(rawPlan, options)))
420
- .filter((plan) => Boolean(plan))
421
- .sort((left, right) => left.amountCents - right.amountCents);
422
- return reorderPlans(addFeaturedTagFallback(mappedPlans), options === null || options === void 0 ? void 0 : options.pricing);
423
- }
424
- export function resolvePaywallPlansFromProjectPlans(projectPlans, planCatalog, options) {
425
- const mappedPlans = (planCatalog && !Array.isArray(planCatalog) && isBillingPlanMappingCatalog(planCatalog)
426
- ? resolveMappedProjectPlans(planCatalog, projectPlans).map((rawPlan) => toPlanFromRemoteProjectPlan(rawPlan, options, rawPlan.funnelKey))
427
- : projectPlans.map((rawPlan) => toPlanFromRemoteProjectPlan(rawPlan, options)))
428
- .filter((plan) => Boolean(plan))
429
- .sort((left, right) => left.amountCents - right.amountCents);
430
- return reorderPlans(addFeaturedTagFallback(mappedPlans), options === null || options === void 0 ? void 0 : options.pricing);
431
- }
432
- export async function getPaywallPlans(mode, planCatalog, options) {
433
- if (!shouldLoadStripePlans(planCatalog)) {
434
- return getFallbackPlans(planCatalog, options);
435
- }
436
- try {
437
- const syncedPlans = resolvePaywallPlansFromProjectPlans(await getStripeSyncedPlanRecords(mode, resolvePlanCatalogOfferSet(planCatalog)), planCatalog, options);
438
- if (syncedPlans.length > 0) {
439
- return syncedPlans;
440
- }
441
- }
442
- catch (_a) {
443
- // fall through to config fallback plans
444
- }
445
- return getFallbackPlans(planCatalog, options);
446
- }
447
193
  export function isStripeConfigured(mode) {
448
194
  void mode;
449
195
  return FUNNEL_SDK_PUBLISHABLE_KEY.length > 0;
@@ -1,25 +1,4 @@
1
1
  import type { BillingFallbackPlanConfig, BillingPlanCatalog, BillingPlanMappingCatalog } from '../config/billing.config.js';
2
- export type RemoteProjectBillingPlan = {
3
- id: string;
4
- key: string;
5
- offerSetId?: string;
6
- offerSetKey?: string;
7
- projectPlanId: string | null;
8
- providerPlanId: string;
9
- displayName: string | null;
10
- description: string | null;
11
- interval: string | null;
12
- intervalCount: number | null;
13
- trialPeriodDays: number | null;
14
- currency: string | null;
15
- amountMinor: number | null;
16
- amountDecimal: string | null;
17
- metadata: Record<string, unknown>;
18
- isActive: boolean;
19
- };
20
- export type ResolvedMappedProjectBillingPlan = RemoteProjectBillingPlan & {
21
- funnelKey: string;
22
- };
23
2
  export type BillingPlanListItem = BillingFallbackPlanConfig & {
24
3
  key: string;
25
4
  };
@@ -27,4 +6,3 @@ export declare const isBillingPlanMappingCatalog: (value: unknown) => value is B
27
6
  export declare const buildBillingPlanMappingCatalog: (planCatalog: BillingPlanCatalog) => BillingPlanMappingCatalog;
28
7
  export declare const buildBillingPlanList: (planCatalog: BillingPlanCatalog) => BillingPlanListItem[];
29
8
  export declare const getFallbackBillingPlans: (planCatalog?: BillingPlanCatalog | readonly BillingFallbackPlanConfig[]) => BillingFallbackPlanConfig[];
30
- export declare const resolveMappedProjectPlans: (mappingCatalog: BillingPlanMappingCatalog, projectPlans: readonly RemoteProjectBillingPlan[]) => ResolvedMappedProjectBillingPlan[];
@@ -46,18 +46,3 @@ export const getFallbackBillingPlans = (planCatalog) => {
46
46
  }
47
47
  return Object.values(planCatalog);
48
48
  };
49
- export const resolveMappedProjectPlans = (mappingCatalog, projectPlans) => {
50
- const projectPlanById = new Map(projectPlans.flatMap((plan) => {
51
- const keys = [plan.projectPlanId, plan.id].filter((key) => typeof key === 'string' && key.length > 0);
52
- return keys.map((key) => [key, plan]);
53
- }));
54
- return Object.entries(mappingCatalog).flatMap(([funnelKey, mapping]) => {
55
- const projectPlan = projectPlanById.get(mapping.projectPlanId) || null;
56
- if (!projectPlan) {
57
- return [];
58
- }
59
- return [
60
- Object.assign(Object.assign({}, projectPlan), { funnelKey }),
61
- ];
62
- });
63
- };
@@ -8,6 +8,7 @@ export type ResolvedPublishedBillingRuntime = {
8
8
  offerSetId: string;
9
9
  paymentProfileId: string | null;
10
10
  provider: 'stripe' | 'solidgate';
11
+ stripePublishableKey: string | null;
11
12
  experimentId: string | null;
12
13
  variantKey: string | null;
13
14
  experiments: readonly FunnelExperimentDefinition[];
@@ -2,7 +2,7 @@ import { resolveGeneratedOfferSetRuntime, resolvePublishedOfferSetSelection, res
2
2
  import { buildBillingDiscountCatalog } from './paywallOffer.service.js';
3
3
  import { buildConfigPaywallPlans, } from '../providers/stripe/services/stripe.service.js';
4
4
  export const resolvePublishedBillingRuntime = ({ config, featureFlags, mode, search = '', defaultOfferSetId, baseCatalogsByMode, fallbackDiscounts, resolvedExperiments = resolvePublishedRuntimeExperiments(config), }) => {
5
- var _a, _b, _c, _d;
5
+ var _a, _b, _c, _d, _e, _f, _g;
6
6
  const offerSets = (_a = config === null || config === void 0 ? void 0 : config.offerSets) !== null && _a !== void 0 ? _a : [];
7
7
  const selection = resolvePublishedOfferSetSelection({
8
8
  offerSets,
@@ -22,12 +22,17 @@ export const resolvePublishedBillingRuntime = ({ config, featureFlags, mode, sea
22
22
  .map((key) => planCatalog[key])
23
23
  .filter((plan) => Boolean(plan));
24
24
  const discountList = runtime.getDiscountList(mode);
25
+ const provider = runtime.getPaymentProvider(mode);
26
+ const paymentProviderConfig = (_c = (_b = runtime.offerSet) === null || _b === void 0 ? void 0 : _b.paymentProviderConfigByMode) === null || _c === void 0 ? void 0 : _c[mode];
25
27
  return {
26
- revisionId: (_b = config === null || config === void 0 ? void 0 : config.revisionId) !== null && _b !== void 0 ? _b : null,
28
+ revisionId: (_d = config === null || config === void 0 ? void 0 : config.revisionId) !== null && _d !== void 0 ? _d : null,
27
29
  mode,
28
30
  offerSetId: selection.offerSetId,
29
- paymentProfileId: ((_d = (_c = runtime.offerSet) === null || _c === void 0 ? void 0 : _c.paymentProfileId) === null || _d === void 0 ? void 0 : _d.trim()) || null,
30
- provider: runtime.getPaymentProvider(mode),
31
+ paymentProfileId: ((_f = (_e = runtime.offerSet) === null || _e === void 0 ? void 0 : _e.paymentProfileId) === null || _f === void 0 ? void 0 : _f.trim()) || null,
32
+ provider,
33
+ stripePublishableKey: provider === 'stripe' && (paymentProviderConfig === null || paymentProviderConfig === void 0 ? void 0 : paymentProviderConfig.provider) === 'stripe'
34
+ ? ((_g = paymentProviderConfig === null || paymentProviderConfig === void 0 ? void 0 : paymentProviderConfig.stripePublishableKey) === null || _g === void 0 ? void 0 : _g.trim()) || null
35
+ : null,
31
36
  experimentId: selection.experimentId,
32
37
  variantKey: selection.variantKey,
33
38
  experiments: resolvedExperiments.experiments,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.7.21",
3
+ "version": "0.8.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
@@ -32,8 +32,8 @@
32
32
  "test:run": "vitest run --passWithNoTests"
33
33
  },
34
34
  "dependencies": {
35
- "@funnelsgrove/analytics": "^0.1.78",
36
- "@funnelsgrove/runtime": "^0.7.29",
35
+ "@funnelsgrove/analytics": "^0.1.81",
36
+ "@funnelsgrove/runtime": "^0.8.0",
37
37
  "@solidgate/react-sdk": "1.34.0",
38
38
  "@stripe/react-stripe-js": "^5.6.0",
39
39
  "@stripe/stripe-js": "^8.7.0",
@@ -1,15 +0,0 @@
1
- import { type RuntimeMode } from '@funnelsgrove/runtime';
2
- import type { BillingFallbackPlanConfig, BillingPlanInputCatalog } from '../../../config/billing.config.js';
3
- import { type PaywallPlan, type PaywallPlanResolutionOptions } from '../services/stripe.service.js';
4
- type UseResolvedPaywallPlansInput = {
5
- runtimeMode: RuntimeMode;
6
- planCatalog?: BillingPlanInputCatalog | readonly BillingFallbackPlanConfig[];
7
- resolutionOptions?: PaywallPlanResolutionOptions;
8
- previewPlans?: readonly PaywallPlan[] | null;
9
- };
10
- export declare const useResolvedPaywallPlans: ({ runtimeMode, planCatalog, resolutionOptions, previewPlans, }: UseResolvedPaywallPlansInput) => {
11
- plans: readonly PaywallPlan[];
12
- loading: boolean;
13
- error: string | null;
14
- };
15
- export {};
@@ -1,68 +0,0 @@
1
- 'use client';
2
- import { useEffect, useMemo, useState } from 'react';
3
- import { isPreviewFrameRuntime } from '@funnelsgrove/runtime';
4
- import { getFallbackPlans, getPaywallPlans, } from '../services/stripe.service.js';
5
- import { isBillingPlanMappingCatalog } from '../../../services/planCatalog.service.js';
6
- export const useResolvedPaywallPlans = ({ runtimeMode, planCatalog, resolutionOptions, previewPlans, }) => {
7
- const fallbackPlans = useMemo(() => getFallbackPlans(planCatalog, resolutionOptions), [planCatalog, resolutionOptions]);
8
- const isMappingCatalog = Boolean(planCatalog) && !Array.isArray(planCatalog) && isBillingPlanMappingCatalog(planCatalog);
9
- const shouldLoadRemotePlans = !planCatalog || isMappingCatalog;
10
- const [plans, setPlans] = useState(() => (previewPlans === null || previewPlans === void 0 ? void 0 : previewPlans.length) ? previewPlans : fallbackPlans);
11
- const [loading, setLoading] = useState(shouldLoadRemotePlans);
12
- const [error, setError] = useState(null);
13
- useEffect(() => {
14
- if (previewPlans === null || previewPlans === void 0 ? void 0 : previewPlans.length) {
15
- setPlans(previewPlans);
16
- setLoading(false);
17
- setError(null);
18
- return;
19
- }
20
- if (!shouldLoadRemotePlans || (isPreviewFrameRuntime() && !isMappingCatalog)) {
21
- setPlans(fallbackPlans);
22
- setLoading(false);
23
- setError(null);
24
- return;
25
- }
26
- let isActive = true;
27
- const load = async () => {
28
- setLoading(true);
29
- setError(null);
30
- try {
31
- const nextPlans = await getPaywallPlans(runtimeMode, planCatalog, resolutionOptions);
32
- if (!isActive) {
33
- return;
34
- }
35
- setPlans(nextPlans);
36
- }
37
- catch (nextError) {
38
- if (!isActive) {
39
- return;
40
- }
41
- setPlans(fallbackPlans);
42
- setError(nextError instanceof Error ? nextError.message : 'Unable to load plans');
43
- }
44
- finally {
45
- if (isActive) {
46
- setLoading(false);
47
- }
48
- }
49
- };
50
- void load();
51
- return () => {
52
- isActive = false;
53
- };
54
- }, [
55
- fallbackPlans,
56
- isMappingCatalog,
57
- planCatalog,
58
- previewPlans,
59
- resolutionOptions,
60
- runtimeMode,
61
- shouldLoadRemotePlans,
62
- ]);
63
- return {
64
- plans,
65
- loading,
66
- error,
67
- };
68
- };