@funnelsgrove/payments 0.7.9 → 0.7.11

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,6 +121,11 @@ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, cont
105
121
  onStateChangeRef.current = onStateChange;
106
122
  onSuccessRef.current = onSuccess;
107
123
  }, [checkoutAnalytics, copy, onError, onStateChange, onSuccess]);
124
+ useEffect(() => {
125
+ setEmailInput((customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || '');
126
+ setCommittedEmail(normalizedCustomerEmail);
127
+ setEmailError(null);
128
+ }, [customerEmail, normalizedCustomerEmail]);
108
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;
@@ -145,16 +166,27 @@ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, cont
145
166
  setView(nextView);
146
167
  (_a = onStateChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onStateChangeRef, nextView.state);
147
168
  }, []);
169
+ const requiresEmailCommit = Boolean(onCustomerEmailCommit);
148
170
  useEffect(() => {
149
- var _a, _b, _c, _d;
171
+ var _a, _b, _c, _d, _e, _f;
150
172
  if (!open) {
151
173
  (_a = prepareAbortRef.current) === null || _a === void 0 ? void 0 : _a.abort();
152
174
  (_b = statusAbortRef.current) === null || _b === void 0 ? void 0 : _b.abort();
153
175
  return;
154
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
+ }
155
187
  const controller = new AbortController();
156
- (_c = prepareAbortRef.current) === null || _c === void 0 ? void 0 : _c.abort();
157
- (_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();
158
190
  prepareAbortRef.current = controller;
159
191
  successAttemptRef.current = null;
160
192
  paymentInfoAttemptRef.current = null;
@@ -178,7 +210,7 @@ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, cont
178
210
  }
179
211
  });
180
212
  return () => controller.abort();
181
- }, [normalizedRequest, open, setState]);
213
+ }, [committedEmail, normalizedRequest, open, requiresEmailCommit, setState]);
182
214
  useEffect(() => {
183
215
  var _a;
184
216
  if (!open) {
@@ -253,6 +285,30 @@ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, cont
253
285
  }
254
286
  }
255
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]);
256
312
  if (!open) {
257
313
  return null;
258
314
  }
@@ -272,7 +328,10 @@ export function SolidgateCheckoutDialog({ open, request, checkoutAnalytics, cont
272
328
  : view.state === 'recoverable_error'
273
329
  ? copy.error
274
330
  : null;
275
- 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: () => {
276
335
  if (view.state === 'loading') {
277
336
  setState(Object.assign(Object.assign({}, view), { state: 'ready' }));
278
337
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",