@funnelsgrove/payments 0.15.5 → 0.15.7

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
@@ -39,7 +39,7 @@ Use these for new one-time paywall plans:
39
39
  `variant='compact'` for a centered desktop modal with the same Stripe-owned wallet/card flow;
40
40
  mobile remains full-screen and all checkout analytics and verified-success behavior are unchanged.
41
41
 
42
- Use `completeStripeOneClickPayment` for normal post-checkout one-click upsells. It delegates the charge to `funnelSdkService.chargeOneClickPayment` and owns any required Stripe confirmation. `chargeStripeOneClickPayment` remains the lower-level charge-only primitive.
42
+ Use `completeStripeOneClickPayment` for normal post-checkout one-click upsells. It delegates the charge to `funnelSdkService.chargeOneClickPayment` and owns any required Stripe confirmation. Pass its optional `checkoutAnalytics` context to let the shared package emit `checkout_completed` only after `succeeded`, including after required confirmation. The context uses `StripeSubscriptionCheckoutAnalyticsContext`; completion is deduplicated by PaymentIntent ID, and analytics failures do not fail the payment. Funnel code supplies the context and completes its step; the shared package owns the analytics callback. `chargeStripeOneClickPayment` remains the lower-level charge-only primitive.
43
43
 
44
44
  Solidgate browser checkout is available for controlled provider-routed funnels through
45
45
  `SolidgateCheckoutDialog`, `prepareSolidgateCheckout`, and `pollSolidgateCheckoutStatus`. The dialog
@@ -136,6 +136,7 @@ conversion event is introduced.
136
136
  - Wallet unavailable state must fall back to card/manual checkout or disappear.
137
137
  - 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.
138
138
  - 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.
139
+ - Payment submission triggers, validation, declines, and retry identity follow [AE-BR-001–004](../../docs/product-specs/analytics-events.md#payment-information-submission). Keep that behavior inside shared checkout surfaces.
139
140
  - Call funnel lifecycle `completeStep` only from the shared checkout `onSuccess` callback. Payment-info submission is not step completion.
140
141
  - 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`.
141
142
  - 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.
@@ -72,6 +72,7 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
72
72
  const [submitting, setSubmitting] = useState(false);
73
73
  const submitInFlightRef = useRef(false);
74
74
  const [paymentElementReady, setPaymentElementReady] = useState(false);
75
+ const [paymentElementComplete, setPaymentElementComplete] = useState(false);
75
76
  const [error, setError] = useState(null);
76
77
  const [billingCountry, setBillingCountry] = useState(null);
77
78
  const initialCustomerEmail = (customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || '';
@@ -194,6 +195,7 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
194
195
  });
195
196
  const handlePaymentElementChange = (event) => {
196
197
  var _a, _b;
198
+ setPaymentElementComplete(event.complete);
197
199
  setBillingCountry((_b = (_a = event.value.billingDetails) === null || _a === void 0 ? void 0 : _a.address.country) !== null && _b !== void 0 ? _b : fixedBillingCountry);
198
200
  };
199
201
  const trackCheckoutSuccess = async () => {
@@ -282,7 +284,9 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
282
284
  finishSubmitting();
283
285
  return;
284
286
  }
285
- await trackPaymentInfoSubmitted();
287
+ if (paymentElementComplete) {
288
+ await trackPaymentInfoSubmitted();
289
+ }
286
290
  const result = await checkoutState.checkout.confirm({
287
291
  redirect: 'if_required',
288
292
  });
@@ -1,5 +1,6 @@
1
1
  import type { FunnelStepType, RuntimeMode } from '@funnelsgrove/runtime';
2
- import { type StripeRuntimeConfigOverrides } from './stripe.service.js';
2
+ import type { StripeRuntimeConfigOverrides } from './shared/types.js';
3
+ import type { ChargeOneClickPaymentResponse, CompleteStripeOneClickPaymentInput } from './oneClick/types.js';
3
4
  export type StripeSubscriptionCheckoutAnalyticsContext = {
4
5
  environment?: RuntimeMode;
5
6
  featureFlags?: Record<string, string | null | undefined>;
@@ -28,3 +29,4 @@ export declare function trackStripeSubscriptionCheckoutStarted(input: TrackStrip
28
29
  export declare function queueStripeSubscriptionCheckoutStarted(input: TrackStripeSubscriptionCheckoutStartedInput): void;
29
30
  export declare function trackStripeSubscriptionPaymentInfoSubmitted(input: TrackStripeSubscriptionPaymentInfoSubmittedInput): Promise<TrackStripeSubscriptionPaymentInfoSubmittedResult>;
30
31
  export declare function trackPaidStripeSubscriptionCheckoutCompleted(input: TrackPaidStripeSubscriptionCheckoutCompletedInput): Promise<TrackPaidStripeSubscriptionCheckoutCompletedResult>;
32
+ export declare function trackPaidStripeOneClickPaymentCompleted(input: CompleteStripeOneClickPaymentInput, payment: ChargeOneClickPaymentResponse): Promise<void>;
@@ -10,7 +10,7 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { publicAnalyticsSdk, } from '@funnelsgrove/analytics';
13
- import { verifyStripeSubscriptionCheckoutPayment, } from './stripe.service.js';
13
+ import { verifyStripeSubscriptionCheckoutPayment } from './checkout/verifySubscriptionCheckoutPayment.js';
14
14
  import { toAnalyticsPaymentMethod } from '../../../services/paymentMethodAnalytics.service.js';
15
15
  const asRecord = (value) => {
16
16
  return value && typeof value === 'object' && !Array.isArray(value)
@@ -175,3 +175,41 @@ export async function trackPaidStripeSubscriptionCheckoutCompleted(input) {
175
175
  activeCheckoutCompletionKeys.delete(completionKey);
176
176
  }
177
177
  }
178
+ export async function trackPaidStripeOneClickPaymentCompleted(input, payment) {
179
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
180
+ const context = input.checkoutAnalytics;
181
+ const paymentIntentId = (_a = payment.paymentIntentId) === null || _a === void 0 ? void 0 : _a.trim();
182
+ if (!context || payment.status !== 'succeeded' || !paymentIntentId) {
183
+ return;
184
+ }
185
+ const runtimeConfig = (_b = input.runtimeConfig) !== null && _b !== void 0 ? _b : context.runtimeConfig;
186
+ const completionKey = buildCheckoutStorageKey('checkout_completed', paymentIntentId, runtimeConfig);
187
+ if (activeCheckoutCompletionKeys.has(completionKey)
188
+ || hasTrackedCheckoutSession('checkout_completed', paymentIntentId, runtimeConfig)) {
189
+ return;
190
+ }
191
+ activeCheckoutCompletionKeys.add(completionKey);
192
+ try {
193
+ const userId = ((_c = input.userId) === null || _c === void 0 ? void 0 : _c.trim()) || ((_d = input.user_id) === null || _d === void 0 ? void 0 : _d.trim());
194
+ const environment = (_f = (_e = payment.environment) !== null && _e !== void 0 ? _e : input.environment) !== null && _f !== void 0 ? _f : context.environment;
195
+ const eventId = publicAnalyticsSdk.trackCheckoutCompleted({
196
+ eventId: paymentIntentId,
197
+ userId,
198
+ environment,
199
+ stepId: context.stepId,
200
+ stepName: context.stepName,
201
+ stepType: context.stepType,
202
+ stepContractVersion: context.stepContractVersion,
203
+ featureFlags: context.featureFlags,
204
+ metadata: Object.assign(Object.assign(Object.assign({}, asRecord(input.analyticsMetadata)), asRecord(context.metadata)), { userId,
205
+ environment, amountCents: (_g = payment.amountCents) !== null && _g !== void 0 ? _g : input.amountCents, currency: (_j = (_h = payment.currency) !== null && _h !== void 0 ? _h : input.currency) !== null && _j !== void 0 ? _j : 'usd', planId: input.planId, providerPlanId: input.providerPlanId, payment_provider: 'stripe' }),
206
+ });
207
+ if (eventId) {
208
+ markCheckoutSessionTracked('checkout_completed', paymentIntentId, runtimeConfig);
209
+ await publicAnalyticsSdk.flush().catch(() => 0);
210
+ }
211
+ }
212
+ finally {
213
+ activeCheckoutCompletionKeys.delete(completionKey);
214
+ }
215
+ }
@@ -1,13 +1,18 @@
1
1
  import { chargeStripeOneClickPayment } from './chargePayment.js';
2
2
  import { confirmOneClickPayment } from './confirmation.js';
3
+ import { trackPaidStripeOneClickPaymentCompleted } from '../checkoutCompletionAnalytics.service.js';
3
4
  export const requiresStripeOneClickConfirmation = (status) => {
4
5
  return status === 'requires_action' || status === 'requires_confirmation';
5
6
  };
6
7
  export async function completeStripeOneClickPayment(input) {
7
8
  const payment = await chargeStripeOneClickPayment(input);
8
- if (!requiresStripeOneClickConfirmation(payment.status)) {
9
- return Object.assign(Object.assign({}, payment), { confirmationAttempted: false });
9
+ const confirmationAttempted = requiresStripeOneClickConfirmation(payment.status);
10
+ const result = Object.assign(Object.assign({}, payment), { status: confirmationAttempted ? await confirmOneClickPayment(payment, input) : payment.status, confirmationAttempted });
11
+ try {
12
+ await trackPaidStripeOneClickPaymentCompleted(input, result);
10
13
  }
11
- const status = await confirmOneClickPayment(payment, input);
12
- return Object.assign(Object.assign({}, payment), { status, confirmationAttempted: true });
14
+ catch (_a) {
15
+ // Analytics cannot turn a successful charge into a payment retry.
16
+ }
17
+ return result;
13
18
  }
@@ -1,5 +1,6 @@
1
1
  import type { RuntimeMode } from '@funnelsgrove/runtime';
2
2
  import type { StripeRuntimeConfigOverrides } from '../shared/types.js';
3
+ import type { StripeSubscriptionCheckoutAnalyticsContext } from '../checkoutCompletionAnalytics.service.js';
3
4
  export type ChargeOneClickPaymentInput = {
4
5
  planId: string;
5
6
  planKey?: string | null;
@@ -34,6 +35,7 @@ export type ChargeOneClickPaymentResponse = {
34
35
  };
35
36
  export type CompleteStripeOneClickPaymentInput = ChargeOneClickPaymentInput & {
36
37
  returnUrl: string;
38
+ checkoutAnalytics?: StripeSubscriptionCheckoutAnalyticsContext;
37
39
  };
38
40
  export type CompleteStripeOneClickPaymentResponse = ChargeOneClickPaymentResponse & {
39
41
  confirmationAttempted: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.15.5",
3
+ "version": "0.15.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
@@ -32,7 +32,7 @@
32
32
  "test:run": "vitest run --passWithNoTests"
33
33
  },
34
34
  "dependencies": {
35
- "@funnelsgrove/analytics": "0.1.102",
35
+ "@funnelsgrove/analytics": "0.1.104",
36
36
  "@funnelsgrove/runtime": "0.14.2",
37
37
  "@solidgate/react-sdk": "1.34.0",
38
38
  "@stripe/react-stripe-js": "^5.6.0",