@funnelsgrove/payments 0.7.4 → 0.7.5

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.
@@ -1,3 +1,4 @@
1
+ import type { FunnelSdkRuntimeConfigOverrides, FunnelStepType, RuntimeMode } from '@funnelsgrove/runtime';
1
2
  import { type PrepareSolidgateCheckoutInput, type SolidgateCheckoutPreparation } from '../services/solidgate.service.js';
2
3
  export type SolidgateCheckoutDialogState = 'loading' | 'ready' | 'submitting' | 'verifying' | 'success' | 'recoverable_error' | 'expired';
3
4
  export type SolidgateCheckoutDialogContent = {
@@ -15,6 +16,7 @@ export type SolidgateCheckoutDialogContent = {
15
16
  export type SolidgateCheckoutDialogProps = {
16
17
  open: boolean;
17
18
  request: Omit<PrepareSolidgateCheckoutInput, 'signal'>;
19
+ checkoutAnalytics?: SolidgateCheckoutAnalyticsContext | null;
18
20
  content?: Partial<SolidgateCheckoutDialogContent>;
19
21
  onClose: () => void;
20
22
  onSuccess?: (checkout: SolidgateCheckoutPreparation) => void | Promise<void>;
@@ -22,4 +24,14 @@ export type SolidgateCheckoutDialogProps = {
22
24
  onError?: (message: string) => void;
23
25
  onStateChange?: (state: SolidgateCheckoutDialogState) => void;
24
26
  };
25
- export declare function SolidgateCheckoutDialog({ open, request, content, onClose, onSuccess, onRetry, onError, onStateChange, }: SolidgateCheckoutDialogProps): import("react/jsx-runtime").JSX.Element | null;
27
+ export type SolidgateCheckoutAnalyticsContext = {
28
+ environment?: RuntimeMode;
29
+ featureFlags?: Record<string, string | null | undefined>;
30
+ metadata?: Record<string, unknown> | null;
31
+ runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
32
+ stepId: string;
33
+ stepName: string;
34
+ stepType: Extract<FunnelStepType, 'checkout' | 'paywall_offer' | 'upsell_offer'>;
35
+ stepContractVersion: number;
36
+ };
37
+ export declare function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, content, onClose, onSuccess, onRetry, onError, onStateChange, }: SolidgateCheckoutDialogProps): import("react/jsx-runtime").JSX.Element | null;
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { publicAnalyticsSdk } from '@funnelsgrove/analytics';
3
4
  import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
4
5
  import { pollSolidgateCheckoutStatus, prepareSolidgateCheckout, } from '../services/solidgate.service.js';
5
6
  const SolidgatePayment = lazy(async () => ({
@@ -22,6 +23,32 @@ const initialView = {
22
23
  checkout: null,
23
24
  canResumeVerification: false,
24
25
  };
26
+ const trackSolidgateCheckoutEvent = async (eventName, attemptId, context) => {
27
+ var _a;
28
+ if (!context) {
29
+ return;
30
+ }
31
+ const input = {
32
+ eventId: eventName === 'checkout_started' ? attemptId : `add_payment_info:${attemptId}`,
33
+ featureFlags: context.featureFlags,
34
+ stepId: context.stepId,
35
+ stepName: context.stepName,
36
+ stepType: context.stepType,
37
+ stepContractVersion: context.stepContractVersion,
38
+ metadata: Object.assign(Object.assign({}, (context.metadata || {})), { checkoutSessionId: attemptId, checkout_session_id: attemptId, environment: context.environment || ((_a = context.metadata) === null || _a === void 0 ? void 0 : _a.environment), payment_provider: 'solidgate' }),
39
+ };
40
+ try {
41
+ const trackedEventId = eventName === 'checkout_started'
42
+ ? publicAnalyticsSdk.trackCheckoutStarted(input)
43
+ : publicAnalyticsSdk.trackPaymentInfoSubmitted(input);
44
+ if (trackedEventId) {
45
+ await publicAnalyticsSdk.flush().catch(() => 0);
46
+ }
47
+ }
48
+ catch (_b) {
49
+ // Analytics must never prevent payment.
50
+ }
51
+ };
25
52
  const isAbortError = (error) => (error instanceof DOMException && error.name === 'AbortError');
26
53
  const safeDestroyPaymentForm = (instance) => {
27
54
  var _a;
@@ -40,39 +67,44 @@ const safeDestroyPaymentForm = (instance) => {
40
67
  };
41
68
  function SolidgatePaymentForm({ checkout, hidden, loadingLabel, onError, onMounted, onSubmit, onVerify, }) {
42
69
  const instanceRef = useRef(null);
70
+ const applePayContainerRef = useRef(null);
71
+ const googlePayContainerRef = useRef(null);
43
72
  useEffect(() => () => {
44
73
  safeDestroyPaymentForm(instanceRef.current);
45
74
  instanceRef.current = null;
46
75
  }, [checkout.attemptId]);
47
- return (_jsx("div", { className: hidden ? 'hidden' : 'block', "data-solidgate-attempt-id": checkout.attemptId, "aria-hidden": hidden || undefined, children: _jsx(Suspense, { fallback: (_jsx("p", { role: 'status', className: 'text-sm text-muted-foreground', children: loadingLabel })), children: _jsx(SolidgatePayment, { merchantData: checkout.initialization.merchantData, width: '100%', formParams: { isSolidLogoVisible: true }, onReadyPaymentInstance: (instance) => {
48
- if (instanceRef.current && instanceRef.current !== instance) {
49
- try {
50
- instanceRef.current.unsubscribeAll();
76
+ return (_jsxs("div", { className: hidden ? 'hidden' : 'block', "data-solidgate-attempt-id": checkout.attemptId, "aria-hidden": hidden || undefined, children: [_jsxs("div", { className: 'mb-3 grid gap-3', children: [_jsx("div", { ref: applePayContainerRef, "data-solidgate-wallet": 'apple-pay' }), _jsx("div", { ref: googlePayContainerRef, "data-solidgate-wallet": 'google-pay' })] }), _jsx(Suspense, { fallback: (_jsx("p", { role: 'status', className: 'text-sm text-muted-foreground', children: loadingLabel })), children: _jsx(SolidgatePayment, { merchantData: checkout.initialization.merchantData, width: '100%', formParams: { isSolidLogoVisible: true }, applePayContainerRef: applePayContainerRef, googlePayContainerRef: googlePayContainerRef, onReadyPaymentInstance: (instance) => {
77
+ if (instanceRef.current && instanceRef.current !== instance) {
78
+ try {
79
+ instanceRef.current.unsubscribeAll();
80
+ }
81
+ catch (_a) {
82
+ // The active instance remains usable even if stale-listener cleanup fails.
83
+ }
51
84
  }
52
- catch (_a) {
53
- // The active instance remains usable even if stale-listener cleanup fails.
54
- }
55
- }
56
- instanceRef.current = instance;
57
- }, onMounted: onMounted, onSubmit: onSubmit, onVerify: onVerify, onSuccess: onVerify, onFail: onVerify, onError: onError }, checkout.attemptId) }) }));
85
+ instanceRef.current = instance;
86
+ }, onMounted: onMounted, onSubmit: onSubmit, onVerify: onVerify, onSuccess: onVerify, onFail: onVerify, onError: onError }, checkout.attemptId) })] }));
58
87
  }
59
- export function SolidgateCheckoutDialog({ open, request, content, onClose, onSuccess, onRetry, onError, onStateChange, }) {
88
+ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, content, onClose, onSuccess, onRetry, onError, onStateChange, }) {
60
89
  const copy = useMemo(() => (Object.assign(Object.assign({}, defaultContent), content)), [content]);
61
90
  const [view, setView] = useState(initialView);
62
91
  const prepareAbortRef = useRef(null);
63
92
  const statusAbortRef = useRef(null);
64
93
  const successAttemptRef = useRef(null);
94
+ const paymentInfoAttemptRef = useRef(null);
95
+ const checkoutAnalyticsRef = useRef(checkoutAnalytics);
65
96
  const closeButtonRef = useRef(null);
66
97
  const copyRef = useRef(copy);
67
98
  const onErrorRef = useRef(onError);
68
99
  const onStateChangeRef = useRef(onStateChange);
69
100
  const onSuccessRef = useRef(onSuccess);
70
101
  useEffect(() => {
102
+ checkoutAnalyticsRef.current = checkoutAnalytics;
71
103
  copyRef.current = copy;
72
104
  onErrorRef.current = onError;
73
105
  onStateChangeRef.current = onStateChange;
74
106
  onSuccessRef.current = onSuccess;
75
- }, [copy, onError, onStateChange, onSuccess]);
107
+ }, [checkoutAnalytics, copy, onError, onStateChange, onSuccess]);
76
108
  const { discountKeyOrProviderId, failUrl, funnelEndUserId, funnelId, idempotencyKey, offerSetId, projectBillingPlanId, returnUrl, runtimeConfig, successUrl, } = request;
77
109
  const { apiBaseUrl, funnelId: runtimeFunnelId, funnelSdkPublishableKey, funnelVersionId, } = runtimeConfig || {};
78
110
  const hasRuntimeConfig = runtimeConfig !== undefined;
@@ -125,6 +157,7 @@ export function SolidgateCheckoutDialog({ open, request, content, onClose, onSuc
125
157
  (_d = statusAbortRef.current) === null || _d === void 0 ? void 0 : _d.abort();
126
158
  prepareAbortRef.current = controller;
127
159
  successAttemptRef.current = null;
160
+ paymentInfoAttemptRef.current = null;
128
161
  queueMicrotask(() => {
129
162
  if (!controller.signal.aborted) {
130
163
  setState(initialView);
@@ -134,6 +167,7 @@ export function SolidgateCheckoutDialog({ open, request, content, onClose, onSuc
134
167
  .then((checkout) => {
135
168
  if (!controller.signal.aborted) {
136
169
  setState({ state: 'ready', checkout, canResumeVerification: false });
170
+ void trackSolidgateCheckoutEvent('checkout_started', checkout.attemptId, checkoutAnalyticsRef.current);
137
171
  }
138
172
  })
139
173
  .catch((error) => {
@@ -242,7 +276,14 @@ export function SolidgateCheckoutDialog({ open, request, content, onClose, onSuc
242
276
  if (view.state === 'loading') {
243
277
  setState(Object.assign(Object.assign({}, view), { state: 'ready' }));
244
278
  }
245
- }, onSubmit: () => setState(Object.assign(Object.assign({}, view), { state: 'submitting' })), onVerify: () => void verifyCanonicalStatus(), onError: () => {
279
+ }, onSubmit: () => {
280
+ var _a, _b, _c;
281
+ if (paymentInfoAttemptRef.current !== ((_a = view.checkout) === null || _a === void 0 ? void 0 : _a.attemptId)) {
282
+ paymentInfoAttemptRef.current = ((_b = view.checkout) === null || _b === void 0 ? void 0 : _b.attemptId) || null;
283
+ void trackSolidgateCheckoutEvent('add_payment_info', ((_c = view.checkout) === null || _c === void 0 ? void 0 : _c.attemptId) || '', checkoutAnalyticsRef.current);
284
+ }
285
+ setState(Object.assign(Object.assign({}, view), { state: 'submitting' }));
286
+ }, onVerify: () => void verifyCanonicalStatus(), onError: () => {
246
287
  var _a;
247
288
  setState(Object.assign(Object.assign({}, view), { state: 'recoverable_error', canResumeVerification: false }));
248
289
  (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, copyRef.current.error);
@@ -5,6 +5,7 @@ import type { RuntimeMode } from '@funnelsgrove/runtime';
5
5
  import { type RemoteProjectBillingPlan } from '../../../services/planCatalog.service.js';
6
6
  export type PaywallPlan = {
7
7
  id: string;
8
+ projectPlanId?: string;
8
9
  offerSetId?: string;
9
10
  offerSetKey?: string;
10
11
  title: string;
@@ -6,6 +6,8 @@ export type ResolvedPublishedBillingRuntime = {
6
6
  revisionId: string | null;
7
7
  mode: RuntimeMode;
8
8
  offerSetId: string;
9
+ paymentProfileId: string | null;
10
+ provider: 'stripe' | 'solidgate';
9
11
  experimentId: string | null;
10
12
  variantKey: string | null;
11
13
  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;
5
+ var _a, _b, _c, _d;
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,
@@ -26,6 +26,8 @@ export const resolvePublishedBillingRuntime = ({ config, featureFlags, mode, sea
26
26
  revisionId: (_b = config === null || config === void 0 ? void 0 : config.revisionId) !== null && _b !== void 0 ? _b : null,
27
27
  mode,
28
28
  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),
29
31
  experimentId: selection.experimentId,
30
32
  variantKey: selection.variantKey,
31
33
  experiments: resolvedExperiments.experiments,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
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.52",
36
- "@funnelsgrove/runtime": "^0.7.3",
35
+ "@funnelsgrove/analytics": "^0.1.61",
36
+ "@funnelsgrove/runtime": "^0.7.12",
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",