@funnelsgrove/payments 0.11.2 → 0.11.4

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
@@ -108,11 +108,11 @@ Funnels should call `resolvePublishedBillingRuntime` once at their runtime-provi
108
108
  - Stripe owns real card fields and wallet controls. Local wallet buttons are placeholders until Stripe confirms availability.
109
109
  - Keep `planId` as the funnel plan key and send Stripe's price id only as `providerPlanId`; the API uses both fields to resolve the active test/live offer mapping.
110
110
  - Wallet unavailable state must fall back to card/manual checkout or disappear.
111
- - Count `checkout_started` only at shared UI boundaries: when a shared card dialog opens or when Stripe reports Apple Pay / Google Pay intent. Checkout Sessions use confirmation as a fallback because Stripe omits their click callback.
111
+ - Count `checkout_started` only at shared UI boundaries: when a shared card dialog opens or when Stripe reports Apple Pay / Google Pay intent. Checkout Sessions use confirmation or cancellation as a fallback because Stripe omits their click callback. Cancellation recovers the start with `checkout_start_source: wallet_cancel`; its timestamp is dismissal time, not wallet-open time. Closing the browser before either callback is still unobservable. Cancel has no payment-method field, so the method is included only when the element enables exactly one method.
112
112
  - Pass `checkoutAnalytics` to every current shared dialog and wallet surface. The package then owns `checkout_started`, `add_payment_info`, and verified `checkout_completed`, all deduplicated by the actual Checkout Session.
113
113
  - Call funnel lifecycle `completeStep` only from the shared checkout `onSuccess` callback. Payment-info submission is not step completion.
114
114
  - 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`.
115
- - Shared checkout analytics deduplicates `checkout_started` by Checkout Session id. Wallet confirmation may retry the start signal when the click callback is unavailable, without creating a duplicate.
115
+ - Shared checkout analytics deduplicates `checkout_started` by Checkout Session id. Wallet confirmation and cancellation may retry the start signal when the click callback is unavailable, without creating a duplicate. Cancellation never emits payment-info or purchase completion events.
116
116
  - Do not call `publicAnalyticsSdk.trackCheckoutStarted`, `trackPaymentInfoSubmitted`, or `trackCheckoutCompleted` directly from funnel checkout components.
117
117
  - 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.
118
118
  - Keep discount math reusable here, but keep offer activation timing in funnel code.
@@ -95,16 +95,18 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
95
95
  }
96
96
  await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess());
97
97
  };
98
- const queueCheckoutStarted = (paymentMethod) => {
98
+ const queueCheckoutStarted = (paymentMethod, checkoutStartSource = 'wallet_click') => {
99
99
  if (!checkoutAnalytics) {
100
100
  return;
101
101
  }
102
- queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, checkoutStartSource: 'wallet_click', paymentMethod }));
102
+ queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId,
103
+ checkoutStartSource,
104
+ paymentMethod }));
103
105
  };
104
106
  const handleConfirm = async (event) => {
105
107
  var _a, _b, _c;
106
108
  // Checkout Sessions omits the Express Checkout onClick callback. Confirmation
107
- // is therefore the first reliable wallet-intent boundary for this provider.
109
+ // and cancellation are the reliable wallet-intent boundaries for this provider.
108
110
  // Session-based analytics deduplication keeps this idempotent if onClick fires.
109
111
  queueCheckoutStarted(event.expressPaymentType);
110
112
  if (checkoutState.type !== 'success') {
@@ -176,13 +178,22 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
176
178
  queueCheckoutStarted(event.expressPaymentType);
177
179
  event.resolve();
178
180
  };
181
+ const handleCancel = () => {
182
+ var _a;
183
+ // Dismissal proves the wallet was opened, even when Stripe omitted onClick.
184
+ // Cancel has no method field; only single-method placements can identify it.
185
+ const enabledMethods = Object.entries((_a = resolvedOptions.paymentMethods) !== null && _a !== void 0 ? _a : {})
186
+ .filter(([, availability]) => availability !== 'never');
187
+ queueCheckoutStarted(enabledMethods.length === 1 ? enabledMethods[0][0] : undefined, 'wallet_cancel');
188
+ onCancel === null || onCancel === void 0 ? void 0 : onCancel();
189
+ };
179
190
  return (_jsxs(_Fragment, { children: [_jsx("div", { className: [
180
191
  'stripe-express-checkout-button',
181
192
  className !== null && className !== void 0 ? className : '',
182
193
  effectiveWalletAvailable === false ? 'is-hidden' : '',
183
194
  ]
184
195
  .filter(Boolean)
185
- .join(' '), children: _jsx(ExpressCheckoutElementWithCancel, { options: resolvedOptions, onCancel: onCancel, onClick: handleClick, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
196
+ .join(' '), children: _jsx(ExpressCheckoutElementWithCancel, { options: resolvedOptions, onCancel: handleCancel, onClick: handleClick, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
186
197
  setWalletAvailable(false);
187
198
  onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(false);
188
199
  onError === null || onError === void 0 ? void 0 : onError(error.message || 'Unable to load wallet checkout.');
@@ -12,7 +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
+ checkoutStartSource?: 'card_form_open' | 'wallet_click' | 'wallet_cancel';
16
16
  paymentMethod?: unknown;
17
17
  };
18
18
  export type TrackPaidStripeSubscriptionCheckoutCompletedInput = StripeSubscriptionCheckoutAnalyticsContext & {
@@ -284,10 +284,44 @@ const currentCheckoutUrl = () => {
284
284
  }
285
285
  return sanitizeCheckoutUrl((_a = window.location) === null || _a === void 0 ? void 0 : _a.href);
286
286
  };
287
+ const readMetaCookieValue = (name) => {
288
+ var _a;
289
+ if (typeof document === 'undefined') {
290
+ return undefined;
291
+ }
292
+ let cookieHeader;
293
+ try {
294
+ cookieHeader = document.cookie;
295
+ }
296
+ catch (_b) {
297
+ return undefined;
298
+ }
299
+ const encodedValue = (_a = cookieHeader
300
+ .split(';')
301
+ .map((part) => part.trim())
302
+ .find((part) => part.startsWith(`${name}=`))) === null || _a === void 0 ? void 0 : _a.slice(name.length + 1);
303
+ if (!encodedValue) {
304
+ return undefined;
305
+ }
306
+ try {
307
+ const value = decodeURIComponent(encodedValue).trim();
308
+ return value && value.length <= 2048 ? value : undefined;
309
+ }
310
+ catch (_c) {
311
+ const value = encodedValue.trim();
312
+ return value && value.length <= 2048 ? value : undefined;
313
+ }
314
+ };
287
315
  const normalizeAnalyticsMetadata = (metadata) => {
288
316
  const normalized = Object.fromEntries(Object.entries(isRecord(metadata) ? metadata : {}).filter(([, value]) => value !== undefined && value !== null && value !== ''));
317
+ const fbc = asTrimmedStringOrNull(normalized.fbc)
318
+ || asTrimmedStringOrNull(normalized._fbc)
319
+ || readMetaCookieValue('_fbc');
320
+ const fbp = asTrimmedStringOrNull(normalized.fbp)
321
+ || asTrimmedStringOrNull(normalized._fbp)
322
+ || readMetaCookieValue('_fbp');
289
323
  const checkoutUrl = sanitizeCheckoutUrl(normalized.url) || currentCheckoutUrl();
290
- const withCheckoutUrl = checkoutUrl ? Object.assign(Object.assign({}, normalized), { url: checkoutUrl }) : normalized;
324
+ const withCheckoutUrl = Object.assign(Object.assign(Object.assign(Object.assign({}, normalized), (fbc ? { fbc } : {})), (fbp ? { fbp } : {})), (checkoutUrl ? { url: checkoutUrl } : {}));
291
325
  return Object.keys(withCheckoutUrl).length > 0 ? withCheckoutUrl : undefined;
292
326
  };
293
327
  const normalizeCheckoutAnalyticsMetadata = (metadata, plan) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
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.85",
36
- "@funnelsgrove/runtime": "^0.11.1",
35
+ "@funnelsgrove/analytics": "^0.1.86",
36
+ "@funnelsgrove/runtime": "^0.11.2",
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",