@funnelsgrove/payments 0.1.37 → 0.1.38

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
@@ -2,6 +2,11 @@
2
2
 
3
3
  Shared billing and checkout helpers for funnels.
4
4
 
5
+ Detailed implementation docs:
6
+
7
+ - [Shared Payments](../../docs/funnel-sdk/shared-payments.md)
8
+ - [Funnel API payment endpoints](../../docs/funnel-sdk/funnel-api.md#checkout-endpoints)
9
+
5
10
  ## Build And Publish
6
11
 
7
12
  - Build distributable output with `npm run build --workspace @funnelsgrove/payments`.
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from './services/planCatalog.service.js';
5
5
  export * from './services/runtimeBillingPlanCatalog.service.js';
6
6
  export * from './services/paywallOffer.service.js';
7
7
  export * from './services/stripe.service.js';
8
+ export * from './services/checkoutCompletionAnalytics.service.js';
8
9
  export * from './hooks/useResolvedPaywallPlans.js';
9
10
  export * from './providers/paymentProvider.types.js';
10
11
  export * from './providers/stripe/useStripeSubscriptionCheckoutSession.js';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export * from './services/planCatalog.service.js';
5
5
  export * from './services/runtimeBillingPlanCatalog.service.js';
6
6
  export * from './services/paywallOffer.service.js';
7
7
  export * from './services/stripe.service.js';
8
+ export * from './services/checkoutCompletionAnalytics.service.js';
8
9
  export * from './hooks/useResolvedPaywallPlans.js';
9
10
  export * from './providers/paymentProvider.types.js';
10
11
  export * from './providers/stripe/useStripeSubscriptionCheckoutSession.js';
@@ -0,0 +1,12 @@
1
+ import type { RuntimeMode } from '@funnelsgrove/runtime';
2
+ import { type StripeRuntimeConfigOverrides } from './stripe.service.js';
3
+ export type TrackPaidStripeSubscriptionCheckoutCompletedInput = {
4
+ checkoutSessionId?: string | null;
5
+ environment?: RuntimeMode;
6
+ metadata?: Record<string, unknown> | null;
7
+ runtimeConfig?: StripeRuntimeConfigOverrides;
8
+ stepId?: string;
9
+ stepName?: string;
10
+ };
11
+ export type TrackPaidStripeSubscriptionCheckoutCompletedResult = 'already_tracked' | 'missing_checkout_session' | 'tracked' | 'unpaid';
12
+ export declare function trackPaidStripeSubscriptionCheckoutCompleted(input: TrackPaidStripeSubscriptionCheckoutCompletedInput): Promise<TrackPaidStripeSubscriptionCheckoutCompletedResult>;
@@ -0,0 +1,74 @@
1
+ import { publicAnalyticsSdk, } from '@funnelsgrove/analytics';
2
+ import { verifyStripeSubscriptionCheckoutPayment, } from './stripe.service.js';
3
+ const asRecord = (value) => {
4
+ return value && typeof value === 'object' && !Array.isArray(value)
5
+ ? value
6
+ : {};
7
+ };
8
+ const activeCheckoutCompletionKeys = new Set();
9
+ const buildCheckoutCompletedStorageKey = (checkoutSessionId, runtimeConfig) => {
10
+ var _a;
11
+ const funnelId = ((_a = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelId) === null || _a === void 0 ? void 0 : _a.trim()) || 'unknown_funnel';
12
+ return `funnelsgrove:checkout_completed:${funnelId}:${checkoutSessionId}`;
13
+ };
14
+ const hasTrackedCheckoutSession = (checkoutSessionId, runtimeConfig) => {
15
+ var _a;
16
+ if (typeof window === 'undefined') {
17
+ return false;
18
+ }
19
+ try {
20
+ return Boolean((_a = window.sessionStorage) === null || _a === void 0 ? void 0 : _a.getItem(buildCheckoutCompletedStorageKey(checkoutSessionId, runtimeConfig)));
21
+ }
22
+ catch (_b) {
23
+ return false;
24
+ }
25
+ };
26
+ const markCheckoutSessionTracked = (checkoutSessionId, runtimeConfig) => {
27
+ var _a;
28
+ if (typeof window === 'undefined') {
29
+ return;
30
+ }
31
+ try {
32
+ (_a = window.sessionStorage) === null || _a === void 0 ? void 0 : _a.setItem(buildCheckoutCompletedStorageKey(checkoutSessionId, runtimeConfig), '1');
33
+ }
34
+ catch (_b) {
35
+ // A stable checkout-session event id still protects provider-side dedupe.
36
+ }
37
+ };
38
+ export async function trackPaidStripeSubscriptionCheckoutCompleted(input) {
39
+ var _a;
40
+ const checkoutSessionId = ((_a = input.checkoutSessionId) === null || _a === void 0 ? void 0 : _a.trim()) || '';
41
+ if (!checkoutSessionId) {
42
+ return 'missing_checkout_session';
43
+ }
44
+ const completionKey = buildCheckoutCompletedStorageKey(checkoutSessionId, input.runtimeConfig);
45
+ if (activeCheckoutCompletionKeys.has(completionKey) ||
46
+ hasTrackedCheckoutSession(checkoutSessionId, input.runtimeConfig)) {
47
+ return 'already_tracked';
48
+ }
49
+ activeCheckoutCompletionKeys.add(completionKey);
50
+ try {
51
+ const checkoutStatus = await verifyStripeSubscriptionCheckoutPayment({
52
+ checkoutSessionId,
53
+ environment: input.environment,
54
+ runtimeConfig: input.runtimeConfig,
55
+ });
56
+ if (!checkoutStatus.paid) {
57
+ return 'unpaid';
58
+ }
59
+ const analyticsMetadata = asRecord(checkoutStatus.analyticsMetadata);
60
+ const conversionInput = {
61
+ eventId: checkoutSessionId,
62
+ stepId: input.stepId,
63
+ stepName: input.stepName,
64
+ metadata: Object.assign(Object.assign(Object.assign({}, asRecord(input.metadata)), analyticsMetadata), { checkoutSessionId, checkout_session_id: checkoutSessionId, checkout_session_status: checkoutStatus.status || undefined, payment_status: checkoutStatus.paymentStatus || undefined, environment: checkoutStatus.environment || input.environment || analyticsMetadata.environment }),
65
+ };
66
+ publicAnalyticsSdk.trackCheckoutCompleted(conversionInput);
67
+ markCheckoutSessionTracked(checkoutSessionId, input.runtimeConfig);
68
+ await publicAnalyticsSdk.flush().catch(() => 0);
69
+ return 'tracked';
70
+ }
71
+ finally {
72
+ activeCheckoutCompletionKeys.delete(completionKey);
73
+ }
74
+ }
@@ -119,8 +119,22 @@ type UpdateSubscriptionCheckoutPlanResponse = {
119
119
  checkoutSessionId: string;
120
120
  environment?: 'test' | 'live';
121
121
  };
122
+ export type VerifySubscriptionCheckoutPaymentInput = {
123
+ checkoutSessionId: string;
124
+ environment?: RuntimeMode;
125
+ runtimeConfig?: StripeRuntimeConfigOverrides;
126
+ };
127
+ export type VerifySubscriptionCheckoutPaymentResponse = {
128
+ checkoutSessionId: string;
129
+ paid: boolean;
130
+ paymentStatus?: string | null;
131
+ status?: string | null;
132
+ environment?: 'test' | 'live';
133
+ analyticsMetadata?: Record<string, unknown>;
134
+ };
122
135
  export declare function createStripeSubscriptionCheckout(input: CreateSubscriptionCheckoutInput): Promise<CreateSubscriptionCheckoutResponse>;
123
136
  export declare function updateStripeSubscriptionCheckoutPlan(input: UpdateSubscriptionCheckoutPlanInput): Promise<UpdateSubscriptionCheckoutPlanResponse>;
137
+ export declare function verifyStripeSubscriptionCheckoutPayment(input: VerifySubscriptionCheckoutPaymentInput): Promise<VerifySubscriptionCheckoutPaymentResponse>;
124
138
  type CreateCheckoutSessionInput = {
125
139
  plan: CheckoutSessionPlanInput;
126
140
  successUrl: string;
@@ -714,6 +714,31 @@ export async function updateStripeSubscriptionCheckoutPlan(input) {
714
714
  }
715
715
  return (await response.json());
716
716
  }
717
+ export async function verifyStripeSubscriptionCheckoutPayment(input) {
718
+ const checkoutSessionId = input.checkoutSessionId.trim();
719
+ if (!checkoutSessionId) {
720
+ throw new Error('checkoutSessionId is required');
721
+ }
722
+ const runtimeConfig = resolveStripeRuntimeConfig(input.runtimeConfig);
723
+ const response = await fetch(buildStripeRuntimeApiUrl('/sdk/public/payments/subscriptions/checkout-session-status', runtimeConfig), {
724
+ method: 'POST',
725
+ headers: buildStripeRuntimeHeaders(runtimeConfig, {
726
+ 'Content-Type': 'application/json',
727
+ }),
728
+ body: JSON.stringify({
729
+ publishableKey: runtimeConfig.funnelSdkPublishableKey || undefined,
730
+ funnelId: runtimeConfig.funnelId || undefined,
731
+ funnelVersionId: runtimeConfig.funnelVersionId || undefined,
732
+ checkoutSessionId,
733
+ environment: input.environment || undefined,
734
+ }),
735
+ });
736
+ if (!response.ok) {
737
+ const errorMessage = await parseErrorMessage(response, 'Unable to verify subscription checkout');
738
+ throw new Error(errorMessage);
739
+ }
740
+ return (await response.json());
741
+ }
717
742
  export async function createStripeCheckoutSession(input) {
718
743
  var _a, _b, _c, _d, _e;
719
744
  const recurring = resolveCheckoutPlanRecurringConfig(input.plan);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",
@@ -27,6 +27,7 @@
27
27
  "test:run": "vitest run --passWithNoTests"
28
28
  },
29
29
  "dependencies": {
30
+ "@funnelsgrove/analytics": "^0.1.13",
30
31
  "@funnelsgrove/runtime": "^0.1.18",
31
32
  "@stripe/react-stripe-js": "^5.6.0",
32
33
  "@stripe/stripe-js": "^8.7.0",