@funnelsgrove/payments 0.7.2 → 0.7.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 +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/providers/solidgate/components/SolidgateCheckoutDialog.d.ts +25 -0
- package/dist/providers/solidgate/components/SolidgateCheckoutDialog.js +250 -0
- package/dist/providers/solidgate/components/index.d.ts +1 -0
- package/dist/providers/solidgate/components/index.js +1 -0
- package/dist/providers/solidgate/index.d.ts +2 -0
- package/dist/providers/solidgate/index.js +2 -0
- package/dist/providers/solidgate/services/index.d.ts +1 -0
- package/dist/providers/solidgate/services/index.js +1 -0
- package/dist/providers/solidgate/services/solidgate.service.d.ts +59 -0
- package/dist/providers/solidgate/services/solidgate.service.js +196 -0
- package/dist/providers/stripe/services/stripe.service.js +7 -4
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -34,6 +34,12 @@ Use these for new one-time paywall plans:
|
|
|
34
34
|
|
|
35
35
|
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.
|
|
36
36
|
|
|
37
|
+
Solidgate browser checkout is available for controlled provider-routed funnels through
|
|
38
|
+
`SolidgateCheckoutDialog`, `prepareSolidgateCheckout`, and `pollSolidgateCheckoutStatus`. The dialog
|
|
39
|
+
mounts one official Solidgate form per attempt, destroys the remote form on replacement/unmount,
|
|
40
|
+
and never treats iframe `success` as funnel completion. Its `onSuccess` callback runs only after the
|
|
41
|
+
provider-neutral status endpoint finds canonical shared-ledger payment evidence.
|
|
42
|
+
|
|
37
43
|
## Advanced Wallet Primitives
|
|
38
44
|
|
|
39
45
|
`ApplePaySubscriptionCheckoutSlot`, `GooglePaySubscriptionCheckoutSlot`, and `WalletSubscriptionCheckoutSlot` stay exported and runtime-compatible. They are backward-compatible advanced low-level primitives: reach for them only when a placement needs wiring `StripeSubscriptionWalletSurface` does not cover. Never share one `useStripeSubscriptionCheckoutSession` result across placements; give each visible placement its own session.
|
|
@@ -84,6 +90,8 @@ When a live funnel still uses one of these, migrate the funnel deliberately with
|
|
|
84
90
|
- `providers/stripe/hooks/*`: Stripe-backed plan resolution and isolated card/wallet checkout-session state.
|
|
85
91
|
- `providers/stripe/components/*`: plan selector, checkout dialogs, Express Checkout wrappers, wallet slots, placeholders, and trust assets.
|
|
86
92
|
- `providers/stripe/testing/*`: Stripe wallet checkout smoke assertions.
|
|
93
|
+
- `providers/solidgate/services/*`: strict Solidgate checkout preparation, status, and bounded polling clients.
|
|
94
|
+
- `providers/solidgate/components/*`: the official-SDK checkout dialog and canonical verification lifecycle.
|
|
87
95
|
- `components/ManageSubscriptionScreen`: provider-neutral subscription management UI.
|
|
88
96
|
|
|
89
97
|
Funnels should call `resolvePublishedBillingRuntime` once at their runtime-provider boundary. Screens consume its resolved view and should not parse offer sets or pricing-experiment variants themselves.
|
|
@@ -92,6 +100,7 @@ Funnels should call `resolvePublishedBillingRuntime` once at their runtime-provi
|
|
|
92
100
|
|
|
93
101
|
- Keep every provider-specific component, hook, service, and test helper under `src/providers/<provider>`. Add future providers as sibling folders; do not place provider SDK imports in root `components`, `hooks`, or `services`.
|
|
94
102
|
- Stripe owns real card fields and wallet controls. Local wallet buttons are placeholders until Stripe confirms availability.
|
|
103
|
+
- 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.
|
|
95
104
|
- Wallet unavailable state must fall back to card/manual checkout or disappear.
|
|
96
105
|
- 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.
|
|
97
106
|
- 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.
|
package/dist/index.d.ts
CHANGED
|
@@ -8,4 +8,5 @@ export * from './services/paywallOffer.service.js';
|
|
|
8
8
|
export * from './services/paymentMethodAnalytics.service.js';
|
|
9
9
|
export * from './providers/paymentProvider.types.js';
|
|
10
10
|
export * from './providers/stripe/index.js';
|
|
11
|
+
export * from './providers/solidgate/index.js';
|
|
11
12
|
export { ManageSubscriptionScreen, type ManageSubscriptionContent, type ManageSubscriptionScreenProps, } from './components/ManageSubscriptionScreen.js';
|
package/dist/index.js
CHANGED
|
@@ -10,5 +10,6 @@ export * from './services/paymentMethodAnalytics.service.js';
|
|
|
10
10
|
export * from './providers/paymentProvider.types.js';
|
|
11
11
|
// Provider-specific integrations are grouped behind provider barrels.
|
|
12
12
|
export * from './providers/stripe/index.js';
|
|
13
|
+
export * from './providers/solidgate/index.js';
|
|
13
14
|
// Provider-neutral subscription management UI.
|
|
14
15
|
export { ManageSubscriptionScreen, } from './components/ManageSubscriptionScreen.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type PrepareSolidgateCheckoutInput, type SolidgateCheckoutPreparation } from '../services/solidgate.service.js';
|
|
2
|
+
export type SolidgateCheckoutDialogState = 'loading' | 'ready' | 'submitting' | 'verifying' | 'success' | 'recoverable_error' | 'expired';
|
|
3
|
+
export type SolidgateCheckoutDialogContent = {
|
|
4
|
+
title: string;
|
|
5
|
+
closeAriaLabel: string;
|
|
6
|
+
loading: string;
|
|
7
|
+
submitting: string;
|
|
8
|
+
verifying: string;
|
|
9
|
+
success: string;
|
|
10
|
+
error: string;
|
|
11
|
+
expired: string;
|
|
12
|
+
retry: string;
|
|
13
|
+
resumeVerification: string;
|
|
14
|
+
};
|
|
15
|
+
export type SolidgateCheckoutDialogProps = {
|
|
16
|
+
open: boolean;
|
|
17
|
+
request: Omit<PrepareSolidgateCheckoutInput, 'signal'>;
|
|
18
|
+
content?: Partial<SolidgateCheckoutDialogContent>;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
onSuccess?: (checkout: SolidgateCheckoutPreparation) => void | Promise<void>;
|
|
21
|
+
onRetry?: () => void;
|
|
22
|
+
onError?: (message: string) => void;
|
|
23
|
+
onStateChange?: (state: SolidgateCheckoutDialogState) => void;
|
|
24
|
+
};
|
|
25
|
+
export declare function SolidgateCheckoutDialog({ open, request, content, onClose, onSuccess, onRetry, onError, onStateChange, }: SolidgateCheckoutDialogProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
4
|
+
import { pollSolidgateCheckoutStatus, prepareSolidgateCheckout, } from '../services/solidgate.service.js';
|
|
5
|
+
const SolidgatePayment = lazy(async () => ({
|
|
6
|
+
default: (await import('@solidgate/react-sdk')).default,
|
|
7
|
+
}));
|
|
8
|
+
const defaultContent = {
|
|
9
|
+
title: 'Secure checkout',
|
|
10
|
+
closeAriaLabel: 'Close checkout',
|
|
11
|
+
loading: 'Preparing your secure checkout…',
|
|
12
|
+
submitting: 'Submitting payment…',
|
|
13
|
+
verifying: 'Verifying payment securely…',
|
|
14
|
+
success: 'Payment confirmed.',
|
|
15
|
+
error: 'Payment could not be completed. Please try again.',
|
|
16
|
+
expired: 'This checkout has expired. Start a new payment to continue.',
|
|
17
|
+
retry: 'Try again',
|
|
18
|
+
resumeVerification: 'Check payment status',
|
|
19
|
+
};
|
|
20
|
+
const initialView = {
|
|
21
|
+
state: 'loading',
|
|
22
|
+
checkout: null,
|
|
23
|
+
canResumeVerification: false,
|
|
24
|
+
};
|
|
25
|
+
const isAbortError = (error) => (error instanceof DOMException && error.name === 'AbortError');
|
|
26
|
+
const safeDestroyPaymentForm = (instance) => {
|
|
27
|
+
var _a;
|
|
28
|
+
try {
|
|
29
|
+
instance === null || instance === void 0 ? void 0 : instance.unsubscribeAll();
|
|
30
|
+
}
|
|
31
|
+
catch (_b) {
|
|
32
|
+
// The global destroy below is the final cleanup boundary.
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
(_a = window.PaymentFormSdk) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
36
|
+
}
|
|
37
|
+
catch (_c) {
|
|
38
|
+
// Cleanup must stay safe when the remote SDK is partially initialized.
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function SolidgatePaymentForm({ checkout, hidden, loadingLabel, onError, onMounted, onSubmit, onVerify, }) {
|
|
42
|
+
const instanceRef = useRef(null);
|
|
43
|
+
useEffect(() => () => {
|
|
44
|
+
safeDestroyPaymentForm(instanceRef.current);
|
|
45
|
+
instanceRef.current = null;
|
|
46
|
+
}, [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();
|
|
51
|
+
}
|
|
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) }) }));
|
|
58
|
+
}
|
|
59
|
+
export function SolidgateCheckoutDialog({ open, request, content, onClose, onSuccess, onRetry, onError, onStateChange, }) {
|
|
60
|
+
const copy = useMemo(() => (Object.assign(Object.assign({}, defaultContent), content)), [content]);
|
|
61
|
+
const [view, setView] = useState(initialView);
|
|
62
|
+
const prepareAbortRef = useRef(null);
|
|
63
|
+
const statusAbortRef = useRef(null);
|
|
64
|
+
const successAttemptRef = useRef(null);
|
|
65
|
+
const closeButtonRef = useRef(null);
|
|
66
|
+
const copyRef = useRef(copy);
|
|
67
|
+
const onErrorRef = useRef(onError);
|
|
68
|
+
const onStateChangeRef = useRef(onStateChange);
|
|
69
|
+
const onSuccessRef = useRef(onSuccess);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
copyRef.current = copy;
|
|
72
|
+
onErrorRef.current = onError;
|
|
73
|
+
onStateChangeRef.current = onStateChange;
|
|
74
|
+
onSuccessRef.current = onSuccess;
|
|
75
|
+
}, [copy, onError, onStateChange, onSuccess]);
|
|
76
|
+
const { discountKeyOrProviderId, failUrl, funnelEndUserId, funnelId, idempotencyKey, offerSetId, projectBillingPlanId, returnUrl, runtimeConfig, successUrl, } = request;
|
|
77
|
+
const { apiBaseUrl, funnelId: runtimeFunnelId, funnelSdkPublishableKey, funnelVersionId, } = runtimeConfig || {};
|
|
78
|
+
const hasRuntimeConfig = runtimeConfig !== undefined;
|
|
79
|
+
const normalizedRequest = useMemo(() => ({
|
|
80
|
+
funnelId: (funnelId === null || funnelId === void 0 ? void 0 : funnelId.trim()) || null,
|
|
81
|
+
funnelEndUserId: funnelEndUserId.trim(),
|
|
82
|
+
offerSetId: offerSetId.trim(),
|
|
83
|
+
projectBillingPlanId: projectBillingPlanId.trim(),
|
|
84
|
+
idempotencyKey: idempotencyKey.trim(),
|
|
85
|
+
discountKeyOrProviderId: (discountKeyOrProviderId === null || discountKeyOrProviderId === void 0 ? void 0 : discountKeyOrProviderId.trim()) || null,
|
|
86
|
+
returnUrl: returnUrl.trim(),
|
|
87
|
+
successUrl: successUrl.trim(),
|
|
88
|
+
failUrl: failUrl.trim(),
|
|
89
|
+
runtimeConfig: hasRuntimeConfig ? {
|
|
90
|
+
apiBaseUrl: (apiBaseUrl === null || apiBaseUrl === void 0 ? void 0 : apiBaseUrl.trim()) || null,
|
|
91
|
+
funnelId: (runtimeFunnelId === null || runtimeFunnelId === void 0 ? void 0 : runtimeFunnelId.trim()) || null,
|
|
92
|
+
funnelVersionId: (funnelVersionId === null || funnelVersionId === void 0 ? void 0 : funnelVersionId.trim()) || null,
|
|
93
|
+
funnelSdkPublishableKey: (funnelSdkPublishableKey === null || funnelSdkPublishableKey === void 0 ? void 0 : funnelSdkPublishableKey.trim()) || null,
|
|
94
|
+
} : undefined,
|
|
95
|
+
}), [
|
|
96
|
+
apiBaseUrl,
|
|
97
|
+
discountKeyOrProviderId,
|
|
98
|
+
failUrl,
|
|
99
|
+
funnelEndUserId,
|
|
100
|
+
funnelId,
|
|
101
|
+
funnelSdkPublishableKey,
|
|
102
|
+
funnelVersionId,
|
|
103
|
+
idempotencyKey,
|
|
104
|
+
offerSetId,
|
|
105
|
+
projectBillingPlanId,
|
|
106
|
+
returnUrl,
|
|
107
|
+
hasRuntimeConfig,
|
|
108
|
+
runtimeFunnelId,
|
|
109
|
+
successUrl,
|
|
110
|
+
]);
|
|
111
|
+
const setState = useCallback((nextView) => {
|
|
112
|
+
var _a;
|
|
113
|
+
setView(nextView);
|
|
114
|
+
(_a = onStateChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onStateChangeRef, nextView.state);
|
|
115
|
+
}, []);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
var _a, _b, _c, _d;
|
|
118
|
+
if (!open) {
|
|
119
|
+
(_a = prepareAbortRef.current) === null || _a === void 0 ? void 0 : _a.abort();
|
|
120
|
+
(_b = statusAbortRef.current) === null || _b === void 0 ? void 0 : _b.abort();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
(_c = prepareAbortRef.current) === null || _c === void 0 ? void 0 : _c.abort();
|
|
125
|
+
(_d = statusAbortRef.current) === null || _d === void 0 ? void 0 : _d.abort();
|
|
126
|
+
prepareAbortRef.current = controller;
|
|
127
|
+
successAttemptRef.current = null;
|
|
128
|
+
queueMicrotask(() => {
|
|
129
|
+
if (!controller.signal.aborted) {
|
|
130
|
+
setState(initialView);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
void prepareSolidgateCheckout(Object.assign(Object.assign({}, normalizedRequest), { signal: controller.signal }))
|
|
134
|
+
.then((checkout) => {
|
|
135
|
+
if (!controller.signal.aborted) {
|
|
136
|
+
setState({ state: 'ready', checkout, canResumeVerification: false });
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.catch((error) => {
|
|
140
|
+
var _a;
|
|
141
|
+
if (!controller.signal.aborted && !isAbortError(error)) {
|
|
142
|
+
setState({ state: 'recoverable_error', checkout: null, canResumeVerification: false });
|
|
143
|
+
(_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, copyRef.current.error);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return () => controller.abort();
|
|
147
|
+
}, [normalizedRequest, open, setState]);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
var _a;
|
|
150
|
+
if (!open) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const previouslyFocused = document.activeElement instanceof HTMLElement
|
|
154
|
+
? document.activeElement
|
|
155
|
+
: null;
|
|
156
|
+
const handleKeyDown = (event) => {
|
|
157
|
+
if (event.key === 'Escape') {
|
|
158
|
+
onClose();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
162
|
+
(_a = closeButtonRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
163
|
+
return () => {
|
|
164
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
165
|
+
previouslyFocused === null || previouslyFocused === void 0 ? void 0 : previouslyFocused.focus();
|
|
166
|
+
};
|
|
167
|
+
}, [onClose, open]);
|
|
168
|
+
useEffect(() => () => {
|
|
169
|
+
var _a, _b;
|
|
170
|
+
(_a = prepareAbortRef.current) === null || _a === void 0 ? void 0 : _a.abort();
|
|
171
|
+
(_b = statusAbortRef.current) === null || _b === void 0 ? void 0 : _b.abort();
|
|
172
|
+
}, []);
|
|
173
|
+
const verifyCanonicalStatus = useCallback(async () => {
|
|
174
|
+
var _a, _b, _c, _d;
|
|
175
|
+
const checkout = view.checkout;
|
|
176
|
+
if (!checkout || view.state === 'success') {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
(_a = statusAbortRef.current) === null || _a === void 0 ? void 0 : _a.abort();
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
statusAbortRef.current = controller;
|
|
182
|
+
setState({ state: 'verifying', checkout, canResumeVerification: false });
|
|
183
|
+
try {
|
|
184
|
+
const result = await pollSolidgateCheckoutStatus({
|
|
185
|
+
attemptId: checkout.attemptId,
|
|
186
|
+
funnelId: normalizedRequest.funnelId,
|
|
187
|
+
funnelEndUserId: normalizedRequest.funnelEndUserId,
|
|
188
|
+
runtimeConfig: normalizedRequest.runtimeConfig,
|
|
189
|
+
signal: controller.signal,
|
|
190
|
+
});
|
|
191
|
+
if (controller.signal.aborted) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (result.status === 'succeeded') {
|
|
195
|
+
setState({ state: 'success', checkout, canResumeVerification: false });
|
|
196
|
+
if (successAttemptRef.current !== checkout.attemptId) {
|
|
197
|
+
successAttemptRef.current = checkout.attemptId;
|
|
198
|
+
await ((_b = onSuccessRef.current) === null || _b === void 0 ? void 0 : _b.call(onSuccessRef, checkout));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else if (result.status === 'expired') {
|
|
202
|
+
setState({ state: 'expired', checkout, canResumeVerification: false });
|
|
203
|
+
}
|
|
204
|
+
else if (result.status === 'failed') {
|
|
205
|
+
setState({ state: 'recoverable_error', checkout, canResumeVerification: false });
|
|
206
|
+
(_c = onErrorRef.current) === null || _c === void 0 ? void 0 : _c.call(onErrorRef, copyRef.current.error);
|
|
207
|
+
}
|
|
208
|
+
else if (result.status === 'requires_payment') {
|
|
209
|
+
setState({ state: 'ready', checkout, canResumeVerification: false });
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
setState({ state: 'verifying', checkout, canResumeVerification: result.timedOut });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (!controller.signal.aborted && !isAbortError(error)) {
|
|
217
|
+
setState({ state: 'recoverable_error', checkout, canResumeVerification: true });
|
|
218
|
+
(_d = onErrorRef.current) === null || _d === void 0 ? void 0 : _d.call(onErrorRef, copyRef.current.error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}, [normalizedRequest, setState, view]);
|
|
222
|
+
if (!open) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const showForm = view.state === 'ready'
|
|
226
|
+
|| view.state === 'submitting'
|
|
227
|
+
|| view.state === 'verifying';
|
|
228
|
+
const statusMessage = view.state === 'loading'
|
|
229
|
+
? copy.loading
|
|
230
|
+
: view.state === 'submitting'
|
|
231
|
+
? copy.submitting
|
|
232
|
+
: view.state === 'verifying'
|
|
233
|
+
? copy.verifying
|
|
234
|
+
: view.state === 'success'
|
|
235
|
+
? copy.success
|
|
236
|
+
: view.state === 'expired'
|
|
237
|
+
? copy.expired
|
|
238
|
+
: view.state === 'recoverable_error'
|
|
239
|
+
? copy.error
|
|
240
|
+
: null;
|
|
241
|
+
return (_jsx("div", { className: 'fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4', children: _jsxs("div", { role: 'dialog', "aria-modal": 'true', "aria-labelledby": 'solidgate-checkout-title', "data-checkout-state": view.state, className: 'w-full max-w-lg rounded-2xl border border-border bg-card p-5 text-foreground shadow-xl', children: [_jsxs("div", { className: 'mb-4 flex items-center justify-between gap-4', children: [_jsx("h2", { id: 'solidgate-checkout-title', className: 'text-lg font-semibold text-foreground', children: copy.title }), _jsx("button", { ref: closeButtonRef, type: 'button', onClick: onClose, "aria-label": copy.closeAriaLabel, className: 'rounded-md border border-border bg-background px-3 py-1.5 text-foreground hover:bg-accent', children: "\u00D7" })] }), view.checkout ? (_jsx(SolidgatePaymentForm, { checkout: view.checkout, hidden: !showForm, loadingLabel: copy.loading, onMounted: () => {
|
|
242
|
+
if (view.state === 'loading') {
|
|
243
|
+
setState(Object.assign(Object.assign({}, view), { state: 'ready' }));
|
|
244
|
+
}
|
|
245
|
+
}, onSubmit: () => setState(Object.assign(Object.assign({}, view), { state: 'submitting' })), onVerify: () => void verifyCanonicalStatus(), onError: () => {
|
|
246
|
+
var _a;
|
|
247
|
+
setState(Object.assign(Object.assign({}, view), { state: 'recoverable_error', canResumeVerification: false }));
|
|
248
|
+
(_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, copyRef.current.error);
|
|
249
|
+
} }, view.checkout.attemptId)) : null, statusMessage && view.state !== 'ready' ? (_jsx("div", { className: 'rounded-xl border border-border bg-background p-4 text-sm text-muted-foreground', children: _jsx("p", { role: 'status', "aria-live": 'polite', children: statusMessage }) })) : null, view.state === 'verifying' && view.canResumeVerification ? (_jsx("button", { type: 'button', onClick: () => void verifyCanonicalStatus(), className: 'mt-4 w-full rounded-lg border border-border bg-accent px-4 py-2 font-medium text-foreground', children: copy.resumeVerification })) : null, (view.state === 'recoverable_error' || view.state === 'expired') && onRetry ? (_jsx("button", { type: 'button', onClick: onRetry, className: 'mt-4 w-full rounded-lg border border-border bg-accent px-4 py-2 font-medium text-foreground', children: copy.retry })) : null] }) }));
|
|
250
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SolidgateCheckoutDialog.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SolidgateCheckoutDialog.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './solidgate.service.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './solidgate.service.js';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type FunnelSdkRuntimeConfigOverrides } from '@funnelsgrove/runtime';
|
|
2
|
+
export type SolidgateCheckoutStatus = 'preparing' | 'requires_payment' | 'verifying' | 'succeeded' | 'failed' | 'expired';
|
|
3
|
+
export type SolidgateCheckoutPreparation = {
|
|
4
|
+
attemptId: string;
|
|
5
|
+
display: {
|
|
6
|
+
title: string;
|
|
7
|
+
amountMinor: number;
|
|
8
|
+
currency: string;
|
|
9
|
+
billingInterval: string | null;
|
|
10
|
+
billingIntervalCount: number | null;
|
|
11
|
+
};
|
|
12
|
+
statusVerification: {
|
|
13
|
+
attemptId: string;
|
|
14
|
+
};
|
|
15
|
+
initialization: {
|
|
16
|
+
kind: 'solidgate';
|
|
17
|
+
merchantData: {
|
|
18
|
+
merchant: string;
|
|
19
|
+
signature: string;
|
|
20
|
+
paymentIntent: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export type PrepareSolidgateCheckoutInput = {
|
|
25
|
+
funnelId?: string | null;
|
|
26
|
+
funnelEndUserId: string;
|
|
27
|
+
offerSetId: string;
|
|
28
|
+
projectBillingPlanId: string;
|
|
29
|
+
idempotencyKey: string;
|
|
30
|
+
discountKeyOrProviderId?: string | null;
|
|
31
|
+
returnUrl: string;
|
|
32
|
+
successUrl: string;
|
|
33
|
+
failUrl: string;
|
|
34
|
+
runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
};
|
|
37
|
+
export type RetrieveSolidgateCheckoutStatusInput = {
|
|
38
|
+
funnelId?: string | null;
|
|
39
|
+
funnelEndUserId: string;
|
|
40
|
+
attemptId: string;
|
|
41
|
+
runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
};
|
|
44
|
+
export type SolidgateCheckoutStatusResponse = {
|
|
45
|
+
attemptId: string;
|
|
46
|
+
status: SolidgateCheckoutStatus;
|
|
47
|
+
retryAfterMs: number | null;
|
|
48
|
+
};
|
|
49
|
+
export type PollSolidgateCheckoutStatusResult = SolidgateCheckoutStatusResponse & {
|
|
50
|
+
timedOut: boolean;
|
|
51
|
+
};
|
|
52
|
+
export declare const prepareSolidgateCheckout: (input: PrepareSolidgateCheckoutInput) => Promise<SolidgateCheckoutPreparation>;
|
|
53
|
+
export declare const retrieveSolidgateCheckoutStatus: (input: RetrieveSolidgateCheckoutStatusInput) => Promise<SolidgateCheckoutStatusResponse>;
|
|
54
|
+
export declare const pollSolidgateCheckoutStatus: (input: RetrieveSolidgateCheckoutStatusInput, options?: {
|
|
55
|
+
maxDurationMs?: number;
|
|
56
|
+
now?: () => number;
|
|
57
|
+
delay?: (delayMs: number, signal?: AbortSignal) => Promise<void>;
|
|
58
|
+
retrieveStatus?: typeof retrieveSolidgateCheckoutStatus;
|
|
59
|
+
}) => Promise<PollSolidgateCheckoutStatusResult>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { FUNNEL_ID, FUNNEL_SDK_PUBLISHABLE_KEY, buildMainApiUrl, resolvePreviewPaymentPublishableKey, } from '@funnelsgrove/runtime';
|
|
2
|
+
const trimTrailingSlash = (value) => value.replace(/\/+$/, '');
|
|
3
|
+
const requiredString = (value, name) => {
|
|
4
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
5
|
+
if (!normalized) {
|
|
6
|
+
throw new Error(`Solidgate checkout response is missing ${name}`);
|
|
7
|
+
}
|
|
8
|
+
return normalized;
|
|
9
|
+
};
|
|
10
|
+
const resolveFunnelId = (inputFunnelId, runtimeConfig) => requiredString(inputFunnelId || (runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelId) || FUNNEL_ID, 'funnelId');
|
|
11
|
+
const resolveApiUrl = (path, runtimeConfig) => {
|
|
12
|
+
var _a;
|
|
13
|
+
const configuredBaseUrl = (_a = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.apiBaseUrl) === null || _a === void 0 ? void 0 : _a.trim();
|
|
14
|
+
return configuredBaseUrl
|
|
15
|
+
? `${trimTrailingSlash(configuredBaseUrl)}${path}`
|
|
16
|
+
: buildMainApiUrl(path);
|
|
17
|
+
};
|
|
18
|
+
const resolvePublishableKey = (runtimeConfig) => {
|
|
19
|
+
var _a;
|
|
20
|
+
return resolvePreviewPaymentPublishableKey((_a = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey) !== null && _a !== void 0 ? _a : FUNNEL_SDK_PUBLISHABLE_KEY) || null;
|
|
21
|
+
};
|
|
22
|
+
const postJson = async (path, body, runtimeConfig, signal) => {
|
|
23
|
+
const publishableKey = resolvePublishableKey(runtimeConfig);
|
|
24
|
+
const response = await fetch(resolveApiUrl(path, runtimeConfig), {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: Object.assign({ 'content-type': 'application/json' }, (publishableKey ? { 'x-sdk-publishable-key': publishableKey } : {})),
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
signal,
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error('Solidgate checkout is temporarily unavailable');
|
|
32
|
+
}
|
|
33
|
+
return response.json();
|
|
34
|
+
};
|
|
35
|
+
const isRecord = (value) => (Boolean(value) && typeof value === 'object' && !Array.isArray(value));
|
|
36
|
+
const parsePreparation = (value) => {
|
|
37
|
+
if (!isRecord(value) || !isRecord(value.display) || !isRecord(value.initialization)) {
|
|
38
|
+
throw new Error('Solidgate checkout returned an invalid response');
|
|
39
|
+
}
|
|
40
|
+
if (value.initialization.kind !== 'solidgate' || !isRecord(value.initialization.merchantData)) {
|
|
41
|
+
throw new Error('Solidgate checkout returned an invalid provider');
|
|
42
|
+
}
|
|
43
|
+
const amountMinor = value.display.amountMinor;
|
|
44
|
+
if (typeof amountMinor !== 'number' || !Number.isSafeInteger(amountMinor) || amountMinor < 0) {
|
|
45
|
+
throw new Error('Solidgate checkout returned an invalid amount');
|
|
46
|
+
}
|
|
47
|
+
const billingInterval = value.display.billingInterval;
|
|
48
|
+
const billingIntervalCount = value.display.billingIntervalCount;
|
|
49
|
+
if (billingInterval !== null && typeof billingInterval !== 'string') {
|
|
50
|
+
throw new Error('Solidgate checkout returned an invalid billing interval');
|
|
51
|
+
}
|
|
52
|
+
if (billingIntervalCount !== null
|
|
53
|
+
&& (typeof billingIntervalCount !== 'number' || !Number.isSafeInteger(billingIntervalCount))) {
|
|
54
|
+
throw new Error('Solidgate checkout returned an invalid billing interval count');
|
|
55
|
+
}
|
|
56
|
+
const attemptId = requiredString(value.attemptId, 'attemptId');
|
|
57
|
+
return {
|
|
58
|
+
attemptId,
|
|
59
|
+
display: {
|
|
60
|
+
title: requiredString(value.display.title, 'display.title'),
|
|
61
|
+
amountMinor,
|
|
62
|
+
currency: requiredString(value.display.currency, 'display.currency'),
|
|
63
|
+
billingInterval,
|
|
64
|
+
billingIntervalCount,
|
|
65
|
+
},
|
|
66
|
+
statusVerification: { attemptId },
|
|
67
|
+
initialization: {
|
|
68
|
+
kind: 'solidgate',
|
|
69
|
+
merchantData: {
|
|
70
|
+
merchant: requiredString(value.initialization.merchantData.merchant, 'merchant'),
|
|
71
|
+
signature: requiredString(value.initialization.merchantData.signature, 'signature'),
|
|
72
|
+
paymentIntent: requiredString(value.initialization.merchantData.paymentIntent, 'paymentIntent'),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const statuses = new Set([
|
|
78
|
+
'preparing',
|
|
79
|
+
'requires_payment',
|
|
80
|
+
'verifying',
|
|
81
|
+
'succeeded',
|
|
82
|
+
'failed',
|
|
83
|
+
'expired',
|
|
84
|
+
]);
|
|
85
|
+
const parseStatus = (value) => {
|
|
86
|
+
if (!isRecord(value) || typeof value.status !== 'string' || !statuses.has(value.status)) {
|
|
87
|
+
throw new Error('Solidgate checkout status returned an invalid response');
|
|
88
|
+
}
|
|
89
|
+
const retryAfterMs = value.retryAfterMs;
|
|
90
|
+
if (retryAfterMs !== null
|
|
91
|
+
&& (typeof retryAfterMs !== 'number' || !Number.isFinite(retryAfterMs) || retryAfterMs < 0)) {
|
|
92
|
+
throw new Error('Solidgate checkout status returned an invalid retry delay');
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
attemptId: requiredString(value.attemptId, 'attemptId'),
|
|
96
|
+
status: value.status,
|
|
97
|
+
retryAfterMs,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
export const prepareSolidgateCheckout = async (input) => {
|
|
101
|
+
var _a;
|
|
102
|
+
return parsePreparation(await postJson('/sdk/public/payments/solidgate/checkout', {
|
|
103
|
+
funnelId: resolveFunnelId(input.funnelId, input.runtimeConfig),
|
|
104
|
+
funnelEndUserId: requiredString(input.funnelEndUserId, 'funnelEndUserId'),
|
|
105
|
+
offerSetId: requiredString(input.offerSetId, 'offerSetId'),
|
|
106
|
+
projectBillingPlanId: requiredString(input.projectBillingPlanId, 'projectBillingPlanId'),
|
|
107
|
+
idempotencyKey: requiredString(input.idempotencyKey, 'idempotencyKey'),
|
|
108
|
+
discountKeyOrProviderId: ((_a = input.discountKeyOrProviderId) === null || _a === void 0 ? void 0 : _a.trim()) || null,
|
|
109
|
+
returnUrl: requiredString(input.returnUrl, 'returnUrl'),
|
|
110
|
+
successUrl: requiredString(input.successUrl, 'successUrl'),
|
|
111
|
+
failUrl: requiredString(input.failUrl, 'failUrl'),
|
|
112
|
+
}, input.runtimeConfig, input.signal));
|
|
113
|
+
};
|
|
114
|
+
export const retrieveSolidgateCheckoutStatus = async (input) => {
|
|
115
|
+
const attemptId = requiredString(input.attemptId, 'attemptId');
|
|
116
|
+
const result = parseStatus(await postJson('/sdk/public/payments/solidgate/checkout/status', {
|
|
117
|
+
funnelId: resolveFunnelId(input.funnelId, input.runtimeConfig),
|
|
118
|
+
funnelEndUserId: requiredString(input.funnelEndUserId, 'funnelEndUserId'),
|
|
119
|
+
attemptId,
|
|
120
|
+
}, input.runtimeConfig, input.signal));
|
|
121
|
+
if (result.attemptId !== attemptId) {
|
|
122
|
+
throw new Error('Solidgate checkout status returned the wrong attempt');
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
};
|
|
126
|
+
const terminalStatuses = new Set(['succeeded', 'failed', 'expired']);
|
|
127
|
+
const abortableDelay = (delayMs, signal) => new Promise((resolve, reject) => {
|
|
128
|
+
const cleanup = () => signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
129
|
+
const timeout = setTimeout(() => {
|
|
130
|
+
cleanup();
|
|
131
|
+
resolve();
|
|
132
|
+
}, delayMs);
|
|
133
|
+
const onAbort = () => {
|
|
134
|
+
clearTimeout(timeout);
|
|
135
|
+
cleanup();
|
|
136
|
+
reject((signal === null || signal === void 0 ? void 0 : signal.reason) || new DOMException('Aborted', 'AbortError'));
|
|
137
|
+
};
|
|
138
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
139
|
+
onAbort();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
|
|
143
|
+
});
|
|
144
|
+
export const pollSolidgateCheckoutStatus = async (input, options = {}) => {
|
|
145
|
+
var _a, _b, _c, _d, _e;
|
|
146
|
+
const maxDurationMs = Math.min(60000, Math.max(0, (_a = options.maxDurationMs) !== null && _a !== void 0 ? _a : 60000));
|
|
147
|
+
const now = options.now || Date.now;
|
|
148
|
+
const delay = options.delay || abortableDelay;
|
|
149
|
+
const retrieveStatus = options.retrieveStatus || retrieveSolidgateCheckoutStatus;
|
|
150
|
+
const startedAt = now();
|
|
151
|
+
let fallbackDelayMs = 1000;
|
|
152
|
+
const deadlineController = new AbortController();
|
|
153
|
+
const forwardAbort = () => {
|
|
154
|
+
var _a;
|
|
155
|
+
return deadlineController.abort(((_a = input.signal) === null || _a === void 0 ? void 0 : _a.reason) || new DOMException('Aborted', 'AbortError'));
|
|
156
|
+
};
|
|
157
|
+
if ((_b = input.signal) === null || _b === void 0 ? void 0 : _b.aborted) {
|
|
158
|
+
forwardAbort();
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
(_c = input.signal) === null || _c === void 0 ? void 0 : _c.addEventListener('abort', forwardAbort, { once: true });
|
|
162
|
+
}
|
|
163
|
+
const deadline = setTimeout(() => deadlineController.abort(new DOMException('Checkout status polling timed out', 'TimeoutError')), maxDurationMs);
|
|
164
|
+
try {
|
|
165
|
+
while (true) {
|
|
166
|
+
const observation = await retrieveStatus(Object.assign(Object.assign({}, input), { signal: deadlineController.signal }));
|
|
167
|
+
if (terminalStatuses.has(observation.status) || observation.status === 'requires_payment') {
|
|
168
|
+
return Object.assign(Object.assign({}, observation), { timedOut: false });
|
|
169
|
+
}
|
|
170
|
+
const elapsedMs = now() - startedAt;
|
|
171
|
+
if (elapsedMs >= maxDurationMs) {
|
|
172
|
+
return Object.assign(Object.assign({}, observation), { timedOut: true });
|
|
173
|
+
}
|
|
174
|
+
const requestedDelayMs = (_d = observation.retryAfterMs) !== null && _d !== void 0 ? _d : fallbackDelayMs;
|
|
175
|
+
const delayMs = Math.min(Math.max(250, requestedDelayMs), 5000, Math.max(0, maxDurationMs - elapsedMs));
|
|
176
|
+
await delay(delayMs, deadlineController.signal);
|
|
177
|
+
fallbackDelayMs = Math.min(fallbackDelayMs * 2, 5000);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (deadlineController.signal.reason instanceof DOMException
|
|
182
|
+
&& deadlineController.signal.reason.name === 'TimeoutError') {
|
|
183
|
+
return {
|
|
184
|
+
attemptId: input.attemptId,
|
|
185
|
+
status: 'verifying',
|
|
186
|
+
retryAfterMs: 3000,
|
|
187
|
+
timedOut: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
clearTimeout(deadline);
|
|
194
|
+
(_e = input.signal) === null || _e === void 0 ? void 0 : _e.removeEventListener('abort', forwardAbort);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
@@ -355,6 +355,9 @@ export function getPaywallPlanSelectionValue(plan) {
|
|
|
355
355
|
var _a;
|
|
356
356
|
return ((_a = plan.providerPlanId) === null || _a === void 0 ? void 0 : _a.trim()) || plan.id;
|
|
357
357
|
}
|
|
358
|
+
// The API resolves offer-set mappings by the funnel plan key. Stripe's price id travels in
|
|
359
|
+
// providerPlanId; duplicating it into planId bypasses the configured test/live offer mapping.
|
|
360
|
+
const getPaywallPlanRequestId = (plan) => plan.id.trim() || getPaywallPlanSelectionValue(plan);
|
|
358
361
|
export function findPaywallPlan(plans, selectionValue) {
|
|
359
362
|
const normalizedSelectionValue = (selectionValue === null || selectionValue === void 0 ? void 0 : selectionValue.trim()) || '';
|
|
360
363
|
if (!normalizedSelectionValue) {
|
|
@@ -554,7 +557,7 @@ export async function createStripeOneTimeCheckout(input) {
|
|
|
554
557
|
return funnelSdkService.createOneTimeCheckout({
|
|
555
558
|
offerSetId: ((_a = input.plan.offerSetId) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
|
|
556
559
|
offerSetKey: ((_b = input.plan.offerSetKey) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
|
|
557
|
-
planId:
|
|
560
|
+
planId: getPaywallPlanRequestId(input.plan),
|
|
558
561
|
providerPlanId: ((_c = input.plan.providerPlanId) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
|
|
559
562
|
title: input.plan.title,
|
|
560
563
|
description: ((_d = input.plan.description) === null || _d === void 0 ? void 0 : _d.trim()) || undefined,
|
|
@@ -641,7 +644,7 @@ export async function createStripeSubscriptionCheckout(input) {
|
|
|
641
644
|
return funnelSdkService.createSubscriptionCheckout({
|
|
642
645
|
offerSetId: ((_a = input.plan.offerSetId) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
|
|
643
646
|
offerSetKey: ((_b = input.plan.offerSetKey) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
|
|
644
|
-
planId:
|
|
647
|
+
planId: getPaywallPlanRequestId(input.plan),
|
|
645
648
|
providerPlanId: ((_c = input.plan.providerPlanId) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
|
|
646
649
|
title: input.plan.title,
|
|
647
650
|
description: ((_d = input.plan.description) === null || _d === void 0 ? void 0 : _d.trim()) || undefined,
|
|
@@ -672,7 +675,7 @@ export async function updateStripeSubscriptionCheckoutPlan(input) {
|
|
|
672
675
|
checkoutSessionId: input.checkoutSessionId.trim(),
|
|
673
676
|
offerSetId: ((_a = input.plan.offerSetId) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
|
|
674
677
|
offerSetKey: ((_b = input.plan.offerSetKey) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
|
|
675
|
-
planId:
|
|
678
|
+
planId: getPaywallPlanRequestId(input.plan),
|
|
676
679
|
providerPlanId: ((_c = input.plan.providerPlanId) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
|
|
677
680
|
title: input.plan.title,
|
|
678
681
|
description: ((_d = input.plan.description) === null || _d === void 0 ? void 0 : _d.trim()) || undefined,
|
|
@@ -712,7 +715,7 @@ export async function createStripeCheckoutSession(input) {
|
|
|
712
715
|
return funnelSdkService.createHostedCheckoutSession({
|
|
713
716
|
offerSetId: ((_a = input.plan.offerSetId) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
|
|
714
717
|
offerSetKey: ((_b = input.plan.offerSetKey) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
|
|
715
|
-
planId:
|
|
718
|
+
planId: getPaywallPlanRequestId(input.plan),
|
|
716
719
|
providerPlanId: ((_c = input.plan.providerPlanId) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
|
|
717
720
|
title: input.plan.title,
|
|
718
721
|
description: ((_d = input.plan.description) === null || _d === void 0 ? void 0 : _d.trim()) || undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnelsgrove/payments",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@funnelsgrove/analytics": "^0.1.52",
|
|
36
36
|
"@funnelsgrove/runtime": "^0.7.3",
|
|
37
|
+
"@solidgate/react-sdk": "1.34.0",
|
|
37
38
|
"@stripe/react-stripe-js": "^5.6.0",
|
|
38
39
|
"@stripe/stripe-js": "^8.7.0",
|
|
39
40
|
"react": "19.2.3",
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"@types/node": "^20",
|
|
45
46
|
"@types/react": "^19",
|
|
46
47
|
"@types/react-dom": "^19",
|
|
48
|
+
"jsdom": "20.0.3",
|
|
47
49
|
"typescript": "^5",
|
|
48
50
|
"vitest": "^3.2.4"
|
|
49
51
|
}
|