@funnelsgrove/payments 0.7.8 → 0.7.10

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,6 +1,6 @@
1
1
  import type { FunnelSdkRuntimeConfigOverrides, FunnelStepType, RuntimeMode } from '@funnelsgrove/runtime';
2
2
  import { type PrepareSolidgateCheckoutInput, type SolidgateCheckoutPreparation } from '../services/solidgate.service.js';
3
- export type SolidgateCheckoutDialogState = 'loading' | 'ready' | 'submitting' | 'verifying' | 'success' | 'recoverable_error' | 'expired';
3
+ export type SolidgateCheckoutDialogState = 'collecting_email' | 'loading' | 'ready' | 'submitting' | 'verifying' | 'success' | 'recoverable_error' | 'expired';
4
4
  export type SolidgateCheckoutDialogContent = {
5
5
  title: string;
6
6
  closeAriaLabel: string;
@@ -12,13 +12,20 @@ export type SolidgateCheckoutDialogContent = {
12
12
  expired: string;
13
13
  retry: string;
14
14
  resumeVerification: string;
15
+ customerEmailLabel: string;
16
+ customerEmailPlaceholder: string;
17
+ customerEmailInvalid: string;
18
+ customerEmailUnavailable: string;
19
+ customerEmailContinue: string;
15
20
  };
16
21
  export type SolidgateCheckoutDialogProps = {
17
22
  open: boolean;
18
23
  request: Omit<PrepareSolidgateCheckoutInput, 'signal'>;
24
+ customerEmail?: string | null;
19
25
  checkoutAnalytics?: SolidgateCheckoutAnalyticsContext | null;
20
26
  content?: Partial<SolidgateCheckoutDialogContent>;
21
27
  onClose: () => void;
28
+ onCustomerEmailCommit?: (email: string) => Promise<string>;
22
29
  onSuccess?: (checkout: SolidgateCheckoutPreparation) => void | Promise<void>;
23
30
  onRetry?: () => void;
24
31
  onError?: (message: string) => void;
@@ -34,4 +41,4 @@ export type SolidgateCheckoutAnalyticsContext = {
34
41
  stepType: Extract<FunnelStepType, 'checkout' | 'paywall_offer' | 'upsell_offer'>;
35
42
  stepContractVersion: number;
36
43
  };
37
- export declare function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, content, onClose, onSuccess, onRetry, onError, onStateChange, }: SolidgateCheckoutDialogProps): import("react/jsx-runtime").JSX.Element | null;
44
+ export declare function SolidgateCheckoutDialog({ open, request, customerEmail, checkoutAnalytics, content, onClose, onCustomerEmailCommit, onSuccess, onRetry, onError, onStateChange, }: SolidgateCheckoutDialogProps): import("react/jsx-runtime").JSX.Element | null;
@@ -17,6 +17,17 @@ const defaultContent = {
17
17
  expired: 'This checkout has expired. Start a new payment to continue.',
18
18
  retry: 'Try again',
19
19
  resumeVerification: 'Check payment status',
20
+ customerEmailLabel: 'Email address',
21
+ customerEmailPlaceholder: 'you@example.com',
22
+ customerEmailInvalid: 'Enter a valid email address to continue.',
23
+ customerEmailUnavailable: 'Your email could not be saved. Please try again.',
24
+ customerEmailContinue: 'Continue to payment',
25
+ };
26
+ const normalizeValidEmail = (value) => {
27
+ const normalized = (value === null || value === void 0 ? void 0 : value.trim()) || '';
28
+ return normalized.length <= 320 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)
29
+ ? normalized
30
+ : null;
20
31
  };
21
32
  const initialView = {
22
33
  state: 'loading',
@@ -85,9 +96,14 @@ function SolidgatePaymentForm({ checkout, hidden, loadingLabel, onError, onMount
85
96
  instanceRef.current = instance;
86
97
  }, onMounted: onMounted, onSubmit: onSubmit, onVerify: onVerify, onSuccess: onVerify, onFail: onVerify, onError: onError }, checkout.attemptId) })] }));
87
98
  }
88
- export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, content, onClose, onSuccess, onRetry, onError, onStateChange, }) {
99
+ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkoutAnalytics, content, onClose, onCustomerEmailCommit, onSuccess, onRetry, onError, onStateChange, }) {
89
100
  const copy = useMemo(() => (Object.assign(Object.assign({}, defaultContent), content)), [content]);
90
101
  const [view, setView] = useState(initialView);
102
+ const normalizedCustomerEmail = normalizeValidEmail(customerEmail);
103
+ const [emailInput, setEmailInput] = useState((customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || '');
104
+ const [committedEmail, setCommittedEmail] = useState(normalizedCustomerEmail);
105
+ const [emailError, setEmailError] = useState(null);
106
+ const [emailSubmitting, setEmailSubmitting] = useState(false);
91
107
  const prepareAbortRef = useRef(null);
92
108
  const statusAbortRef = useRef(null);
93
109
  const successAttemptRef = useRef(null);
@@ -105,17 +121,31 @@ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, cont
105
121
  onStateChangeRef.current = onStateChange;
106
122
  onSuccessRef.current = onSuccess;
107
123
  }, [checkoutAnalytics, copy, onError, onStateChange, onSuccess]);
108
- const { discountKeyOrProviderId, failUrl, funnelPlanKey, funnelEndUserId, funnelId, idempotencyKey, offerSetId, projectBillingPlanId, returnUrl, runtimeConfig, successUrl, } = request;
124
+ useEffect(() => {
125
+ setEmailInput((customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || '');
126
+ setCommittedEmail(normalizedCustomerEmail);
127
+ setEmailError(null);
128
+ }, [customerEmail, normalizedCustomerEmail]);
129
+ const { discountKeyOrProviderId, failUrl, funnelPlanKey, funnelEndUserId, funnelId, idempotencyKey, offerSetId, returnUrl, runtimeConfig, successUrl, } = request;
109
130
  const { apiBaseUrl, funnelId: runtimeFunnelId, funnelSdkPublishableKey, funnelVersionId, } = runtimeConfig || {};
110
131
  const hasRuntimeConfig = runtimeConfig !== undefined;
111
- const normalizedRequest = useMemo(() => (Object.assign(Object.assign({ funnelId: (funnelId === null || funnelId === void 0 ? void 0 : funnelId.trim()) || null, funnelEndUserId: funnelEndUserId.trim(), offerSetId: offerSetId.trim() }, (funnelPlanKey
112
- ? { funnelPlanKey: funnelPlanKey.trim() }
113
- : { projectBillingPlanId: (projectBillingPlanId === null || projectBillingPlanId === void 0 ? void 0 : projectBillingPlanId.trim()) || '' })), { idempotencyKey: idempotencyKey.trim(), discountKeyOrProviderId: (discountKeyOrProviderId === null || discountKeyOrProviderId === void 0 ? void 0 : discountKeyOrProviderId.trim()) || null, returnUrl: returnUrl.trim(), successUrl: successUrl.trim(), failUrl: failUrl.trim(), runtimeConfig: hasRuntimeConfig ? {
132
+ const normalizedRequest = useMemo(() => ({
133
+ funnelId: (funnelId === null || funnelId === void 0 ? void 0 : funnelId.trim()) || null,
134
+ funnelEndUserId: funnelEndUserId.trim(),
135
+ offerSetId: offerSetId.trim(),
136
+ funnelPlanKey: funnelPlanKey.trim(),
137
+ idempotencyKey: idempotencyKey.trim(),
138
+ discountKeyOrProviderId: (discountKeyOrProviderId === null || discountKeyOrProviderId === void 0 ? void 0 : discountKeyOrProviderId.trim()) || null,
139
+ returnUrl: returnUrl.trim(),
140
+ successUrl: successUrl.trim(),
141
+ failUrl: failUrl.trim(),
142
+ runtimeConfig: hasRuntimeConfig ? {
114
143
  apiBaseUrl: (apiBaseUrl === null || apiBaseUrl === void 0 ? void 0 : apiBaseUrl.trim()) || null,
115
144
  funnelId: (runtimeFunnelId === null || runtimeFunnelId === void 0 ? void 0 : runtimeFunnelId.trim()) || null,
116
145
  funnelVersionId: (funnelVersionId === null || funnelVersionId === void 0 ? void 0 : funnelVersionId.trim()) || null,
117
146
  funnelSdkPublishableKey: (funnelSdkPublishableKey === null || funnelSdkPublishableKey === void 0 ? void 0 : funnelSdkPublishableKey.trim()) || null,
118
- } : undefined })), [
147
+ } : undefined,
148
+ }), [
119
149
  apiBaseUrl,
120
150
  discountKeyOrProviderId,
121
151
  failUrl,
@@ -126,7 +156,6 @@ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, cont
126
156
  funnelVersionId,
127
157
  idempotencyKey,
128
158
  offerSetId,
129
- projectBillingPlanId,
130
159
  returnUrl,
131
160
  hasRuntimeConfig,
132
161
  runtimeFunnelId,
@@ -137,16 +166,27 @@ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, cont
137
166
  setView(nextView);
138
167
  (_a = onStateChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onStateChangeRef, nextView.state);
139
168
  }, []);
169
+ const requiresEmailCommit = Boolean(onCustomerEmailCommit);
140
170
  useEffect(() => {
141
- var _a, _b, _c, _d;
171
+ var _a, _b, _c, _d, _e, _f;
142
172
  if (!open) {
143
173
  (_a = prepareAbortRef.current) === null || _a === void 0 ? void 0 : _a.abort();
144
174
  (_b = statusAbortRef.current) === null || _b === void 0 ? void 0 : _b.abort();
145
175
  return;
146
176
  }
177
+ if (requiresEmailCommit && !committedEmail) {
178
+ (_c = prepareAbortRef.current) === null || _c === void 0 ? void 0 : _c.abort();
179
+ (_d = statusAbortRef.current) === null || _d === void 0 ? void 0 : _d.abort();
180
+ queueMicrotask(() => setState({
181
+ state: 'collecting_email',
182
+ checkout: null,
183
+ canResumeVerification: false,
184
+ }));
185
+ return;
186
+ }
147
187
  const controller = new AbortController();
148
- (_c = prepareAbortRef.current) === null || _c === void 0 ? void 0 : _c.abort();
149
- (_d = statusAbortRef.current) === null || _d === void 0 ? void 0 : _d.abort();
188
+ (_e = prepareAbortRef.current) === null || _e === void 0 ? void 0 : _e.abort();
189
+ (_f = statusAbortRef.current) === null || _f === void 0 ? void 0 : _f.abort();
150
190
  prepareAbortRef.current = controller;
151
191
  successAttemptRef.current = null;
152
192
  paymentInfoAttemptRef.current = null;
@@ -170,7 +210,7 @@ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, cont
170
210
  }
171
211
  });
172
212
  return () => controller.abort();
173
- }, [normalizedRequest, open, setState]);
213
+ }, [committedEmail, normalizedRequest, open, requiresEmailCommit, setState]);
174
214
  useEffect(() => {
175
215
  var _a;
176
216
  if (!open) {
@@ -245,6 +285,30 @@ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, cont
245
285
  }
246
286
  }
247
287
  }, [normalizedRequest, setState, view]);
288
+ const commitCustomerEmail = useCallback(async (event) => {
289
+ event.preventDefault();
290
+ const normalized = normalizeValidEmail(emailInput);
291
+ if (!normalized || !onCustomerEmailCommit) {
292
+ setEmailError(copy.customerEmailInvalid);
293
+ return;
294
+ }
295
+ setEmailSubmitting(true);
296
+ setEmailError(null);
297
+ try {
298
+ const committed = normalizeValidEmail(await onCustomerEmailCommit(normalized));
299
+ if (!committed) {
300
+ throw new Error('customer-email-not-committed');
301
+ }
302
+ setEmailInput(committed);
303
+ setCommittedEmail(committed);
304
+ }
305
+ catch (_a) {
306
+ setEmailError(copy.customerEmailUnavailable);
307
+ }
308
+ finally {
309
+ setEmailSubmitting(false);
310
+ }
311
+ }, [copy.customerEmailInvalid, copy.customerEmailUnavailable, emailInput, onCustomerEmailCommit]);
248
312
  if (!open) {
249
313
  return null;
250
314
  }
@@ -264,7 +328,10 @@ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, cont
264
328
  : view.state === 'recoverable_error'
265
329
  ? copy.error
266
330
  : null;
267
- 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: () => {
331
+ 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.state === 'collecting_email' ? (_jsxs("form", { className: 'grid gap-4', onSubmit: (event) => void commitCustomerEmail(event), children: [_jsxs("label", { className: 'grid gap-2 text-sm font-medium text-foreground', children: [copy.customerEmailLabel, _jsx("input", { type: 'email', autoComplete: 'email', inputMode: 'email', maxLength: 320, required: true, value: emailInput, onChange: (event) => {
332
+ setEmailInput(event.target.value);
333
+ setEmailError(null);
334
+ }, placeholder: copy.customerEmailPlaceholder, "aria-invalid": Boolean(emailError), className: 'w-full rounded-lg border border-border bg-background px-3 py-2 text-foreground placeholder:text-muted-foreground' })] }), emailError ? (_jsx("p", { role: 'alert', className: 'text-sm text-destructive', children: emailError })) : null, _jsx("button", { type: 'submit', disabled: emailSubmitting, className: 'w-full rounded-lg border border-border bg-accent px-4 py-2 font-medium text-foreground disabled:opacity-60', children: copy.customerEmailContinue })] })) : null, view.checkout ? (_jsx(SolidgatePaymentForm, { checkout: view.checkout, hidden: !showForm, loadingLabel: copy.loading, onMounted: () => {
268
335
  if (view.state === 'loading') {
269
336
  setState(Object.assign(Object.assign({}, view), { state: 'ready' }));
270
337
  }
@@ -25,9 +25,7 @@ export type PrepareSolidgateCheckoutInput = {
25
25
  funnelId?: string | null;
26
26
  funnelEndUserId: string;
27
27
  offerSetId: string;
28
- funnelPlanKey?: string | null;
29
- /** @deprecated Published funnels may send this during the offer-set migration window. */
30
- projectBillingPlanId?: string | null;
28
+ funnelPlanKey: string;
31
29
  idempotencyKey: string;
32
30
  discountKeyOrProviderId?: string | null;
33
31
  returnUrl: string;
@@ -99,9 +99,17 @@ const parseStatus = (value) => {
99
99
  };
100
100
  export const prepareSolidgateCheckout = async (input) => {
101
101
  var _a;
102
- return parsePreparation(await postJson('/sdk/public/payments/solidgate/checkout', Object.assign(Object.assign({ funnelId: resolveFunnelId(input.funnelId, input.runtimeConfig), funnelEndUserId: requiredString(input.funnelEndUserId, 'funnelEndUserId'), offerSetId: requiredString(input.offerSetId, 'offerSetId') }, (input.funnelPlanKey
103
- ? { funnelPlanKey: requiredString(input.funnelPlanKey, 'funnelPlanKey') }
104
- : { projectBillingPlanId: requiredString(input.projectBillingPlanId, 'projectBillingPlanId') })), { idempotencyKey: requiredString(input.idempotencyKey, 'idempotencyKey'), discountKeyOrProviderId: ((_a = input.discountKeyOrProviderId) === null || _a === void 0 ? void 0 : _a.trim()) || null, returnUrl: requiredString(input.returnUrl, 'returnUrl'), successUrl: requiredString(input.successUrl, 'successUrl'), failUrl: requiredString(input.failUrl, 'failUrl') }), input.runtimeConfig, input.signal));
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
+ funnelPlanKey: requiredString(input.funnelPlanKey, 'funnelPlanKey'),
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));
105
113
  };
106
114
  export const retrieveSolidgateCheckoutStatus = async (input) => {
107
115
  const attemptId = requiredString(input.attemptId, 'attemptId');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.7.8",
3
+ "version": "0.7.10",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@funnelsgrove/analytics": "^0.1.67",
36
- "@funnelsgrove/runtime": "^0.7.18",
36
+ "@funnelsgrove/runtime": "^0.7.19",
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",