@funnelsgrove/payments 0.1.58 → 0.2.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
@@ -87,8 +87,9 @@ When a live funnel still uses one of these, migrate the funnel deliberately with
87
87
  - Keep provider-specific implementation under `src/providers/<provider>`.
88
88
  - Stripe owns real card fields and wallet controls. Local wallet buttons are placeholders until Stripe confirms availability.
89
89
  - Wallet unavailable state must fall back to card/manual checkout or disappear.
90
- - Do not count checkout intent until the visitor taps or confirms a visible checkout CTA.
91
- - Wallet slots may create render-only Checkout Sessions on mount so Apple Pay and Google Pay render before the visitor taps. These sessions must use `checkoutStartSource: 'wallet_render'`; trusted server `checkout_started` analytics ignores that source.
90
+ - Count `checkout_started` only at shared UI boundaries: when a shared card dialog opens or when Stripe reports an Apple Pay / Google Pay button click.
91
+ - Wallet slots may create render-only Checkout Sessions on mount so Apple Pay and Google Pay render before the visitor taps. Session creation is technical preparation and must never emit `checkout_started`.
92
+ - Shared checkout analytics deduplicates `checkout_started` by Checkout Session id. Card submit and wallet confirmation must not emit it again.
92
93
  - Funnel paywalls should style wallet buttons through the shared slot API: `className` for the Stripe button/placeholder, `slotClassName` for the outer slot, `appearance` for Stripe Elements appearance, and `options` for Stripe Express Checkout options.
93
94
  - Keep discount math reusable here, but keep offer activation timing in funnel code.
94
95
  - Checkout metadata must avoid sensitive raw query params and use `@funnelsgrove/analytics` metadata helpers.
@@ -191,9 +191,6 @@ function SharedStripeCheckoutForm({ amountCents, checkoutAnalytics, checkoutSess
191
191
  setSubmitting(false);
192
192
  return;
193
193
  }
194
- if (checkoutAnalytics) {
195
- queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, paymentMethod: 'card' }));
196
- }
197
194
  const result = await stripe.confirmCardPayment(clientSecret, {
198
195
  payment_method: {
199
196
  card: cardNumberElement,
@@ -249,6 +246,12 @@ function SharedStripeCheckoutForm({ amountCents, checkoutAnalytics, checkoutSess
249
246
  }
250
247
  export function SharedStripeCheckoutDialog(_a) {
251
248
  var { checkoutAnalytics, checkoutSessionId, clientSecret, onClose, stripePromise } = _a, props = __rest(_a, ["checkoutAnalytics", "checkoutSessionId", "clientSecret", "onClose", "stripePromise"]);
249
+ useEffect(() => {
250
+ if (!checkoutAnalytics) {
251
+ return;
252
+ }
253
+ queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, checkoutStartSource: 'card_form_open', paymentMethod: 'card' }));
254
+ }, [checkoutAnalytics, checkoutSessionId]);
252
255
  useEffect(() => {
253
256
  const handleKeyDown = (event) => {
254
257
  if (event.key === 'Escape') {
@@ -250,9 +250,6 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
250
250
  setSubmitting(false);
251
251
  return;
252
252
  }
253
- if (checkoutAnalytics) {
254
- queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, paymentMethod: 'card' }));
255
- }
256
253
  await (onPaymentInfoSubmitted === null || onPaymentInfoSubmitted === void 0 ? void 0 : onPaymentInfoSubmitted());
257
254
  const result = await checkoutState.checkout.confirm({
258
255
  redirect: 'if_required',
@@ -330,6 +327,12 @@ export function SharedStripeCheckoutV2Dialog(_a) {
330
327
  var { checkoutAnalytics, checkoutSessionId, clientSecret, onClose, stripePromise, supportedCountries } = _a, props = __rest(_a, ["checkoutAnalytics", "checkoutSessionId", "clientSecret", "onClose", "stripePromise", "supportedCountries"]);
331
328
  const normalizedSupportedCountries = useMemo(() => normalizeSupportedCheckoutCountries(supportedCountries), [supportedCountries]);
332
329
  const fixedBillingCountry = getFixedSupportedBillingCountry(normalizedSupportedCountries);
330
+ useEffect(() => {
331
+ if (!checkoutAnalytics) {
332
+ return;
333
+ }
334
+ queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, checkoutStartSource: 'card_form_open', paymentMethod: 'card' }));
335
+ }, [checkoutAnalytics, checkoutSessionId]);
333
336
  useEffect(() => {
334
337
  const handleKeyDown = (event) => {
335
338
  if (event.key === 'Escape') {
@@ -123,9 +123,6 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
123
123
  event.paymentFailed({ reason: 'fail', message });
124
124
  return;
125
125
  }
126
- if (checkoutAnalytics) {
127
- queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, paymentMethod: event.expressPaymentType }));
128
- }
129
126
  let preparedCustomerEmail = (customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || null;
130
127
  if (beforeConfirm) {
131
128
  try {
@@ -162,13 +159,19 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
162
159
  window.location.assign(returnUrl);
163
160
  }
164
161
  };
162
+ const handleClick = (event) => {
163
+ if (checkoutAnalytics) {
164
+ queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, checkoutStartSource: 'wallet_click', paymentMethod: event.expressPaymentType }));
165
+ }
166
+ event.resolve();
167
+ };
165
168
  return (_jsxs(_Fragment, { children: [_jsx("div", { className: [
166
169
  'stripe-express-checkout-button',
167
170
  className !== null && className !== void 0 ? className : '',
168
171
  effectiveWalletAvailable === false ? 'is-hidden' : '',
169
172
  ]
170
173
  .filter(Boolean)
171
- .join(' '), children: _jsx(ExpressCheckoutElementWithCancel, { options: resolvedOptions, onCancel: onCancel, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
174
+ .join(' '), children: _jsx(ExpressCheckoutElementWithCancel, { options: resolvedOptions, onCancel: onCancel, onClick: handleClick, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
172
175
  setWalletAvailable(false);
173
176
  onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(false);
174
177
  onError === null || onError === void 0 ? void 0 : onError(error.message || 'Unable to load wallet checkout.');
@@ -78,9 +78,6 @@ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, checko
78
78
  return;
79
79
  }
80
80
  onError === null || onError === void 0 ? void 0 : onError(null);
81
- if (checkoutAnalytics) {
82
- queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, paymentMethod: event.expressPaymentType }));
83
- }
84
81
  let preparedCustomerEmail = (customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || null;
85
82
  if (beforeConfirm) {
86
83
  try {
@@ -138,13 +135,19 @@ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, checko
138
135
  window.location.assign(returnUrl);
139
136
  }
140
137
  };
138
+ const handleClick = (event) => {
139
+ if (checkoutAnalytics) {
140
+ queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, checkoutStartSource: 'wallet_click', paymentMethod: event.expressPaymentType }));
141
+ }
142
+ event.resolve();
143
+ };
141
144
  return (_jsxs(_Fragment, { children: [_jsx("div", { className: [
142
145
  'stripe-express-checkout-button',
143
146
  className !== null && className !== void 0 ? className : '',
144
147
  effectiveWalletAvailable === false ? 'is-hidden' : '',
145
148
  ]
146
149
  .filter(Boolean)
147
- .join(' '), children: _jsx(ExpressCheckoutElement, { options: resolvedOptions, onCancel: onCancel, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
150
+ .join(' '), children: _jsx(ExpressCheckoutElement, { options: resolvedOptions, onCancel: onCancel, onClick: handleClick, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
148
151
  setWalletAvailable(false);
149
152
  onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(false);
150
153
  onError === null || onError === void 0 ? void 0 : onError(error.message || 'Unable to load wallet checkout.');
@@ -121,7 +121,7 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, appearance, availa
121
121
  return;
122
122
  }
123
123
  walletCheckoutStartedAfterClickSessionIdRef.current = session.checkoutSessionId;
124
- void trackStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId: session.checkoutSessionId, paymentMethod: analyticsPaymentMethod }));
124
+ void trackStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId: session.checkoutSessionId, checkoutStartSource: 'wallet_click', paymentMethod: analyticsPaymentMethod }));
125
125
  }, [
126
126
  analyticsPaymentMethod,
127
127
  checkoutAnalytics,
@@ -12,6 +12,7 @@ export type StripeSubscriptionCheckoutAnalyticsContext = {
12
12
  };
13
13
  export type TrackStripeSubscriptionCheckoutStartedInput = StripeSubscriptionCheckoutAnalyticsContext & {
14
14
  checkoutSessionId?: string | null;
15
+ checkoutStartSource?: 'card_form_open' | 'wallet_click';
15
16
  paymentMethod?: unknown;
16
17
  };
17
18
  export type TrackPaidStripeSubscriptionCheckoutCompletedInput = StripeSubscriptionCheckoutAnalyticsContext & {
@@ -70,7 +70,12 @@ export async function trackStripeSubscriptionCheckoutStarted(input) {
70
70
  stepName: input.stepName,
71
71
  stepType: input.stepType,
72
72
  stepContractVersion: input.stepContractVersion,
73
- metadata: Object.assign(Object.assign(Object.assign({}, metadata), { checkoutSessionId, checkout_session_id: checkoutSessionId, environment: input.environment || metadata.environment, payment_provider: 'stripe' }), (paymentMethod ? { payment_method: paymentMethod } : {})),
73
+ metadata: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, metadata), { checkoutSessionId, checkout_session_id: checkoutSessionId }), (input.checkoutStartSource
74
+ ? {
75
+ checkoutStartSource: input.checkoutStartSource,
76
+ checkout_start_source: input.checkoutStartSource,
77
+ }
78
+ : {})), { environment: input.environment || metadata.environment, payment_provider: 'stripe' }), (paymentMethod ? { payment_method: paymentMethod } : {})),
74
79
  };
75
80
  const eventId = publicAnalyticsSdk.trackCheckoutStarted(conversionInput);
76
81
  if (!eventId) {
@@ -1,10 +1,7 @@
1
- var _a, _b, _c, _d;
2
1
  import { loadStripe } from '@stripe/stripe-js';
3
2
  import { billingPlanList, } from '../config/billing.config.js';
4
- import { FUNNEL_ID, FUNNEL_SDK_PUBLISHABLE_KEY, funnelSdkService, runtimeEnvConfig, } from '@funnelsgrove/runtime';
3
+ import { FUNNEL_ID, FUNNEL_SDK_PUBLISHABLE_KEY, funnelSdkService, } from '@funnelsgrove/runtime';
5
4
  import { isBillingPlanMappingCatalog, resolveMappedProjectPlans, } from './planCatalog.service.js';
6
- const STRIPE_TEST_PUBLISHABLE_KEY = ((_b = (_a = runtimeEnvConfig.stripeTestPublishableKey) !== null && _a !== void 0 ? _a : runtimeEnvConfig.stripePublishableKey) !== null && _b !== void 0 ? _b : '').trim();
7
- const STRIPE_LIVE_PUBLISHABLE_KEY = ((_d = (_c = runtimeEnvConfig.stripeLivePublishableKey) !== null && _c !== void 0 ? _c : runtimeEnvConfig.stripePublishableKey) !== null && _d !== void 0 ? _d : '').trim();
8
5
  const stripePromiseByKey = new Map();
9
6
  const syncedPlansPromiseByMode = new Map();
10
7
  export const buildPaywallPlanResolutionSignature = (options) => {
@@ -292,12 +289,6 @@ export function buildConfigPaywallPlans(planCatalog, orderedPlanIds = []) {
292
289
  : Object.entries(planCatalog !== null && planCatalog !== void 0 ? planCatalog : {}).map(([key, plan]) => toConfigPaywallPlan(key, plan));
293
290
  return getOrderedPaywallPlans(plans, orderedPlanIds);
294
291
  }
295
- const getStripePublishableKeyFromEnv = (mode) => {
296
- if (mode === 'live') {
297
- return STRIPE_LIVE_PUBLISHABLE_KEY || STRIPE_TEST_PUBLISHABLE_KEY;
298
- }
299
- return STRIPE_TEST_PUBLISHABLE_KEY || STRIPE_LIVE_PUBLISHABLE_KEY;
300
- };
301
292
  const toFallbackPaywallPlans = (planCatalog, options) => {
302
293
  const sourcePlans = buildConfigPaywallPlans(planCatalog);
303
294
  return reorderPlans(sourcePlans.length > 0 ? sourcePlans : fallbackPaywallPlans, options === null || options === void 0 ? void 0 : options.pricing);
@@ -430,7 +421,8 @@ export async function getPaywallPlans(mode, planCatalog, options) {
430
421
  return getFallbackPlans(planCatalog, options);
431
422
  }
432
423
  export function isStripeConfigured(mode) {
433
- return getStripePublishableKeyFromEnv(mode).length > 0 || FUNNEL_SDK_PUBLISHABLE_KEY.length > 0;
424
+ void mode;
425
+ return FUNNEL_SDK_PUBLISHABLE_KEY.length > 0;
434
426
  }
435
427
  export function getStripePromise(_mode, runtimePublishableKey) {
436
428
  const resolvedPublishableKey = (runtimePublishableKey === null || runtimePublishableKey === void 0 ? void 0 : runtimePublishableKey.trim()) || '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.1.58",
3
+ "version": "0.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@funnelsgrove/analytics": "^0.1.28",
36
- "@funnelsgrove/runtime": "^0.1.54",
36
+ "@funnelsgrove/runtime": "^0.2.0",
37
37
  "@stripe/react-stripe-js": "^5.6.0",
38
38
  "@stripe/stripe-js": "^8.7.0",
39
39
  "react": "19.2.3",