@funnelsgrove/payments 0.15.11 → 0.20.0

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,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { completePaywallCheckout } from '@funnelsgrove/runtime';
3
4
  import { publicAnalyticsSdk } from '@funnelsgrove/analytics';
4
5
  import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
5
6
  import { pollSolidgateCheckoutStatus, prepareSolidgateCheckout, } from '../services/solidgate.service.js';
@@ -41,7 +42,7 @@ const trackSolidgateCheckoutEvent = async (eventName, attemptId, context) => {
41
42
  return;
42
43
  }
43
44
  const input = {
44
- eventId: eventName === 'checkout_started' ? attemptId : `add_payment_info:${attemptId}`,
45
+ eventId: eventName === 'add_payment_info' ? `add_payment_info:${attemptId}` : attemptId,
45
46
  featureFlags: context.featureFlags,
46
47
  stepId: context.stepId,
47
48
  stepName: context.stepName,
@@ -52,7 +53,9 @@ const trackSolidgateCheckoutEvent = async (eventName, attemptId, context) => {
52
53
  try {
53
54
  const trackedEventId = eventName === 'checkout_started'
54
55
  ? publicAnalyticsSdk.trackCheckoutStarted(input)
55
- : publicAnalyticsSdk.trackPaymentInfoSubmitted(input);
56
+ : eventName === 'checkout_completed'
57
+ ? publicAnalyticsSdk.trackCheckoutCompleted(input)
58
+ : publicAnalyticsSdk.trackPaymentInfoSubmitted(input);
56
59
  if (trackedEventId) {
57
60
  await publicAnalyticsSdk.flush().catch(() => 0);
58
61
  }
@@ -269,11 +272,15 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
269
272
  return;
270
273
  }
271
274
  if (result.status === 'succeeded') {
272
- setState({ state: 'success', checkout, canResumeVerification: false });
273
275
  if (successAttemptRef.current !== checkout.attemptId) {
276
+ const embedded = await completePaywallCheckout('solidgate', checkout.attemptId, {
277
+ beforeNotify: () => trackSolidgateCheckoutEvent('checkout_completed', checkout.attemptId, checkoutAnalyticsRef.current),
278
+ });
279
+ if (!embedded)
280
+ await ((_b = onSuccessRef.current) === null || _b === void 0 ? void 0 : _b.call(onSuccessRef, checkout));
274
281
  successAttemptRef.current = checkout.attemptId;
275
- await ((_b = onSuccessRef.current) === null || _b === void 0 ? void 0 : _b.call(onSuccessRef, checkout));
276
282
  }
283
+ setState({ state: 'success', checkout, canResumeVerification: false });
277
284
  }
278
285
  else if (result.status === 'expired') {
279
286
  setState({ state: 'expired', checkout, canResumeVerification: false });
@@ -291,8 +298,8 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
291
298
  setState({ state: 'verifying', checkout, canResumeVerification: result.timedOut });
292
299
  }
293
300
  }
294
- catch (error) {
295
- if (!controller.signal.aborted && !isAbortError(error)) {
301
+ catch (_e) {
302
+ if (!controller.signal.aborted) {
296
303
  setState({ state: 'recoverable_error', checkout, canResumeVerification: true });
297
304
  trackFailure(copyRef.current.error, 'payment_verification', checkout.attemptId);
298
305
  (_d = onErrorRef.current) === null || _d === void 0 ? void 0 : _d.call(onErrorRef, copyRef.current.error);
@@ -385,5 +392,5 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
385
392
  setState(Object.assign(Object.assign({}, view), { state: 'recoverable_error', canResumeVerification: false }));
386
393
  trackFailure(copyRef.current.error, 'payment_form', (_a = view.checkout) === null || _a === void 0 ? void 0 : _a.attemptId);
387
394
  (_b = onErrorRef.current) === null || _b === void 0 ? void 0 : _b.call(onErrorRef, copyRef.current.error);
388
- } }, view.checkout.attemptId)) : null, presentation && view.checkout && showForm ? (_jsx("p", { className: 'solidgate-checkout-secure', children: presentation.secureLabel })) : null, statusMessage && view.state !== 'ready' ? (_jsx("div", { className: 'solidgate-checkout-status', children: _jsx("p", { role: 'status', "aria-live": 'polite', children: statusMessage }) })) : null, view.state === 'verifying' && view.canResumeVerification ? (_jsx("button", { type: 'button', onClick: () => void verifyCanonicalStatus(), className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.resumeVerification })) : null, (view.state === 'recoverable_error' || view.state === 'expired') && onRetry ? (_jsx("button", { type: 'button', onClick: onRetry, className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.retry })) : null] })) : null] }), _jsx("style", { children: solidgateCheckoutDialogStyles })] }));
395
+ } }, view.checkout.attemptId)) : null, presentation && view.checkout && showForm ? (_jsx("p", { className: 'solidgate-checkout-secure', children: presentation.secureLabel })) : null, statusMessage && view.state !== 'ready' ? (_jsx("div", { className: 'solidgate-checkout-status', children: _jsx("p", { role: 'status', "aria-live": 'polite', children: statusMessage }) })) : null, view.canResumeVerification ? (_jsx("button", { type: 'button', onClick: () => void verifyCanonicalStatus(), className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.resumeVerification })) : null, (view.state === 'recoverable_error' || view.state === 'expired') && !view.canResumeVerification && onRetry ? (_jsx("button", { type: 'button', onClick: onRetry, className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.retry })) : null] })) : null] }), _jsx("style", { children: solidgateCheckoutDialogStyles })] }));
389
396
  }
@@ -1,4 +1,4 @@
1
- import { FUNNEL_ID, FUNNEL_SDK_PUBLISHABLE_KEY, buildMainApiUrl, resolvePreviewPaymentPublishableKey, } from '@funnelsgrove/runtime';
1
+ import { paywallCheckoutReturnUrl, retainPaywallCheckout, FUNNEL_ID, FUNNEL_SDK_PUBLISHABLE_KEY, buildMainApiUrl, resolvePreviewPaymentPublishableKey, } from '@funnelsgrove/runtime';
2
2
  const trimTrailingSlash = (value) => value.replace(/\/+$/, '');
3
3
  const requiredString = (value, name) => {
4
4
  const normalized = typeof value === 'string' ? value.trim() : '';
@@ -118,7 +118,7 @@ export const prepareSolidgateCheckout = async (input) => {
118
118
  if (!paymentProfileId || !providerPlanId || !checkoutType || !environment) {
119
119
  throw new Error('Solidgate V2 checkout selection is incomplete');
120
120
  }
121
- return parsePreparation(await postJson('/sdk/public/v2/payments/checkout-sessions', {
121
+ const result = parsePreparation(await postJson('/sdk/public/v2/payments/checkout-sessions', {
122
122
  checkoutType,
123
123
  uiMode: 'custom',
124
124
  environment,
@@ -131,10 +131,12 @@ export const prepareSolidgateCheckout = async (input) => {
131
131
  offerSetId: requiredString(input.offerSetId, 'offerSetId'),
132
132
  idempotencyKey: requiredString(input.idempotencyKey, 'idempotencyKey'),
133
133
  couponId: ((_c = input.discountKeyOrProviderId) === null || _c === void 0 ? void 0 : _c.trim()) || null,
134
- returnUrl: requiredString(input.returnUrl, 'returnUrl'),
135
- successUrl: requiredString(input.successUrl, 'successUrl'),
136
- failUrl: requiredString(input.failUrl, 'failUrl'),
134
+ returnUrl: paywallCheckoutReturnUrl(requiredString(input.returnUrl, 'returnUrl'), 'solidgate'),
135
+ successUrl: paywallCheckoutReturnUrl(requiredString(input.successUrl, 'successUrl'), 'solidgate'),
136
+ failUrl: paywallCheckoutReturnUrl(requiredString(input.failUrl, 'failUrl'), 'solidgate', true),
137
137
  }, input.runtimeConfig, input.signal));
138
+ retainPaywallCheckout('solidgate', result.attemptId);
139
+ return result;
138
140
  };
139
141
  export const retrieveSolidgateCheckoutStatus = async (input) => {
140
142
  const attemptId = requiredString(input.attemptId, 'attemptId');
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { completePaywallCheckout } from '@funnelsgrove/runtime';
3
4
  import { useEffect, useMemo, useRef, useState } from 'react';
4
5
  import { PaymentElement, useCheckout } from '@stripe/react-stripe-js/checkout';
5
6
  import { isValidCheckoutEmail as isValidEmail, isCheckoutEmailError } from '../../../services/checkoutEmail.js';
@@ -32,6 +33,8 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
32
33
  ? true
33
34
  : walletAvailable;
34
35
  const [submitting, setSubmitting] = useState(false);
36
+ const [completed, setCompleted] = useState(false);
37
+ const [providerConfirmed, setProviderConfirmed] = useState(false);
35
38
  const submitInFlightRef = useRef(false);
36
39
  const [paymentElementReady, setPaymentElementReady] = useState(false);
37
40
  const [paymentElementComplete, setPaymentElementComplete] = useState(false);
@@ -164,11 +167,12 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
164
167
  setPaymentElementComplete(event.complete);
165
168
  setBillingCountry((_b = (_a = event.value.billingDetails) === null || _a === void 0 ? void 0 : _a.address.country) !== null && _b !== void 0 ? _b : fixedBillingCountry);
166
169
  };
167
- const trackCheckoutSuccess = async () => {
170
+ const trackCheckoutSuccess = async (embedded = false) => {
168
171
  if (checkoutAnalytics) {
169
172
  await trackPaidStripeSubscriptionCheckoutCompleted(Object.assign({ checkoutSessionId }, checkoutAnalytics));
170
173
  }
171
- await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess());
174
+ if (!embedded)
175
+ await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess());
172
176
  };
173
177
  const trackPaymentInfoSubmitted = async () => {
174
178
  if (checkoutAnalytics) {
@@ -187,23 +191,25 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
187
191
  }, [effectiveSelectedMethod]);
188
192
  const handleSubmit = async (event) => {
189
193
  event.preventDefault();
190
- if (submitInFlightRef.current || emailSessionChanged) {
194
+ if (submitInFlightRef.current || (emailSessionChanged && !providerConfirmed)) {
191
195
  return;
192
196
  }
193
- const billingCountryError = getBillingCountryError(billingCountry !== null && billingCountry !== void 0 ? billingCountry : fixedBillingCountry);
194
- if (billingCountryError) {
195
- setSharedError(billingCountryError);
196
- return;
197
- }
198
- if (checkoutState.type !== 'success') {
199
- setSharedError(checkoutState.type === 'error'
200
- ? checkoutState.error.message
201
- : 'Payment form is not ready yet.');
202
- return;
203
- }
204
- if (!paymentElementReady) {
205
- setSharedError('Payment form is not ready yet.', true);
206
- return;
197
+ if (!providerConfirmed) {
198
+ const billingCountryError = getBillingCountryError(billingCountry !== null && billingCountry !== void 0 ? billingCountry : fixedBillingCountry);
199
+ if (billingCountryError) {
200
+ setSharedError(billingCountryError);
201
+ return;
202
+ }
203
+ if (checkoutState.type !== 'success') {
204
+ setSharedError(checkoutState.type === 'error'
205
+ ? checkoutState.error.message
206
+ : 'Payment form is not ready yet.');
207
+ return;
208
+ }
209
+ if (!paymentElementReady) {
210
+ setSharedError('Payment form is not ready yet.', true);
211
+ return;
212
+ }
207
213
  }
208
214
  submitInFlightRef.current = true;
209
215
  setSubmitting(true);
@@ -212,25 +218,36 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
212
218
  submitInFlightRef.current = false;
213
219
  setSubmitting(false);
214
220
  };
221
+ let confirmed = providerConfirmed;
215
222
  try {
216
- const syncedEmail = await ensureCustomerEmail();
217
- if (!syncedEmail) {
218
- finishSubmitting();
219
- return;
220
- }
221
- if (paymentElementComplete) {
222
- await trackPaymentInfoSubmitted();
223
- }
224
- const result = await checkoutState.checkout.confirm({
225
- redirect: 'if_required',
226
- });
227
- if (result.type === 'error') {
228
- const message = result.error.message || 'Payment failed.';
229
- setSharedError(message, false, result.error);
230
- finishSubmitting();
231
- return;
223
+ if (!providerConfirmed && checkoutState.type === 'success') {
224
+ const syncedEmail = await ensureCustomerEmail();
225
+ if (!syncedEmail) {
226
+ finishSubmitting();
227
+ return;
228
+ }
229
+ if (paymentElementComplete) {
230
+ await trackPaymentInfoSubmitted();
231
+ }
232
+ const result = await checkoutState.checkout.confirm({
233
+ redirect: 'if_required',
234
+ });
235
+ if (result.type === 'error') {
236
+ const message = result.error.message || 'Payment failed.';
237
+ setSharedError(message, false, result.error);
238
+ finishSubmitting();
239
+ return;
240
+ }
241
+ confirmed = true;
242
+ setProviderConfirmed(true);
232
243
  }
233
244
  if (typeof window !== 'undefined') {
245
+ if (await completePaywallCheckout('stripe', checkoutSessionId || '', { beforeNotify: async () => {
246
+ setCompleted(true);
247
+ await trackCheckoutSuccess(true);
248
+ } })) {
249
+ return;
250
+ }
234
251
  await trackCheckoutSuccess();
235
252
  window.location.assign(returnUrl);
236
253
  return;
@@ -238,11 +255,15 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
238
255
  finishSubmitting();
239
256
  }
240
257
  catch (submitError) {
241
- setSharedError(getFriendlyCardErrorMessage(submitError));
258
+ setSharedError(getFriendlyCardErrorMessage(submitError), confirmed);
242
259
  finishSubmitting();
243
260
  }
244
261
  };
245
262
  const visibleError = error;
263
+ if (providerConfirmed && !completed)
264
+ return _jsxs("form", { onSubmit: handleSubmit, className: 'bg-background text-foreground p-6', children: [_jsx("p", { role: error ? 'alert' : 'status', children: error || 'Verifying payment…' }), _jsx("button", { type: 'submit', disabled: submitting, className: 'bg-accent text-foreground border-border rounded border p-3', children: "Check payment status" })] });
265
+ if (completed)
266
+ return _jsx("div", { role: 'status', className: 'bg-background text-foreground p-6', children: "Payment confirmed. You can return to the app." });
246
267
  return (_jsxs("div", { className: 'shared-checkout-v2-shell', "data-variant": variant, style: checkoutStyle, children: [_jsxs("header", { className: 'shared-checkout-v2-header', children: [_jsx("button", { type: 'button', className: 'shared-checkout-v2-close', onClick: onClose, "aria-label": closeAriaLabel, children: _jsx("span", { "aria-hidden": 'true' }) }), _jsx("h2", { id: 'shared-checkout-v2-title', children: title })] }), _jsx(SharedStripeCheckoutV2Summary, { discountPercent, countdownLabel, remainingSeconds, variant, satisfactionPercent, satisfactionLabel, originalPriceLabel, originalPriceValue, discountLabel, discountAmountLabel, promoCodeLabel, promoCode, totalLabel, totalValue, savedLabel, showPromo }), _jsxs("form", { className: 'shared-checkout-v2-payment', onSubmit: (event) => void handleSubmit(event), children: [showWalletMethod ? (_jsxs("div", { className: 'shared-checkout-v2-methods', role: 'tablist', "aria-label": 'Payment method', children: [_jsxs("button", { type: 'button', className: [
247
268
  'shared-checkout-v2-method',
248
269
  effectiveSelectedMethod === 'wallet' ? 'is-selected' : '',
@@ -258,7 +279,7 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
258
279
  effectiveSelectedMethod === 'wallet' ? 'is-visible' : '',
259
280
  ]
260
281
  .filter(Boolean)
261
- .join(' '), children: [_jsxs("p", { className: 'shared-checkout-v2-secure-pill', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] }), _jsxs("div", { className: 'shared-checkout-v2-wallet-shell', "aria-label": walletAriaLabel, children: [_jsx(StripeCheckoutExpressCheckoutButton, { amountCents: amountCents, availabilityPaymentMethods: walletPaymentMethods, beforeConfirm: ensureCustomerEmail, checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, className: 'shared-checkout-v2-express-buttons', confirmEmail: false, customerEmail: resolvedCustomerEmail, customerName: customerName, initialAvailable: initialWalletAvailable, keepInitialAvailableOnReady: initialWalletAvailable === true, onAvailabilityChange: handleWalletAvailabilityChange, onError: (message) => setSharedError(message, true), onPaymentInfoSubmitted: onPaymentInfoSubmitted, onPaymentRejected: () => setSelectedMethod('card'), onSuccess: trackCheckoutSuccess, options: expressCheckoutOptions, returnUrl: returnUrl, summaryLabel: expressCheckoutSummaryLabel, supportedCountries: normalizedSupportedCountries, unsupportedCountryMessage: unsupportedCountryMessage }), _jsx("span", { className: 'shared-checkout-v2-sr-only', children: walletAriaLabel })] })] }), _jsxs("div", { hidden: effectiveSelectedMethod !== 'card', className: [
282
+ .join(' '), children: [_jsxs("p", { className: 'shared-checkout-v2-secure-pill', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] }), _jsxs("div", { className: 'shared-checkout-v2-wallet-shell', "aria-label": walletAriaLabel, children: [_jsx(StripeCheckoutExpressCheckoutButton, { amountCents: amountCents, availabilityPaymentMethods: walletPaymentMethods, beforeConfirm: ensureCustomerEmail, checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, className: 'shared-checkout-v2-express-buttons', confirmEmail: false, customerEmail: resolvedCustomerEmail, customerName: customerName, initialAvailable: initialWalletAvailable, keepInitialAvailableOnReady: initialWalletAvailable === true, onAvailabilityChange: handleWalletAvailabilityChange, onError: (message) => setSharedError(message, true), onPaymentInfoSubmitted: onPaymentInfoSubmitted, onPaymentRejected: () => setSelectedMethod('card'), onSuccess: () => trackCheckoutSuccess(), onEmbeddedComplete: () => setCompleted(true), onVerificationPending: () => setProviderConfirmed(true), options: expressCheckoutOptions, returnUrl: returnUrl, summaryLabel: expressCheckoutSummaryLabel, supportedCountries: normalizedSupportedCountries, unsupportedCountryMessage: unsupportedCountryMessage }), _jsx("span", { className: 'shared-checkout-v2-sr-only', children: walletAriaLabel })] })] }), _jsxs("div", { hidden: effectiveSelectedMethod !== 'card', className: [
262
283
  'shared-checkout-v2-card-panel',
263
284
  effectiveSelectedMethod === 'card' ? 'is-visible' : '',
264
285
  ]
@@ -30,7 +30,9 @@ export type StripeCheckoutExpressCheckoutButtonProps = {
30
30
  message: string;
31
31
  paymentMethod?: string;
32
32
  }) => void;
33
+ onEmbeddedComplete?: () => void;
34
+ onVerificationPending?: () => void;
33
35
  onSuccess?: (result: StripeWalletCheckoutSuccess) => void | Promise<void>;
34
36
  unsupportedCountryMessage?: string;
35
37
  };
36
- export declare function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail, customerEmail, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onPaymentRejected, onSuccess, unsupportedCountryMessage, }: StripeCheckoutExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
38
+ export declare function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail, customerEmail, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onPaymentRejected, onSuccess, onEmbeddedComplete, onVerificationPending, unsupportedCountryMessage, }: StripeCheckoutExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { completePaywallCheckout } from '@funnelsgrove/runtime';
3
4
  import { isValidCheckoutEmail, isCheckoutEmailError } from '../../../services/checkoutEmail.js';
4
5
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
6
  import { ExpressCheckoutElement, useCheckout, } from '@stripe/react-stripe-js/checkout';
@@ -52,8 +53,13 @@ const normalizeStripeCheckoutProviderError = (error) => {
52
53
  type: typeof record.type === 'string' ? record.type : undefined,
53
54
  };
54
55
  };
55
- export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail = true, customerEmail, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onPaymentRejected, onSuccess, unsupportedCountryMessage, }) {
56
+ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail = true, customerEmail, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onPaymentRejected, onSuccess, onEmbeddedComplete, onVerificationPending, unsupportedCountryMessage, }) {
56
57
  const checkoutState = useCheckout();
58
+ const [completed, setCompleted] = useState(false);
59
+ const completedRef = useRef(false);
60
+ const verificationInFlightRef = useRef(false);
61
+ const [verificationInFlight, setVerificationInFlight] = useState(false);
62
+ const [verificationError, setVerificationError] = useState(null);
57
63
  const [loadRetry, setLoadRetry] = useState(0);
58
64
  const loadRetryUsedRef = useRef(false);
59
65
  const walletReadyRef = useRef(false);
@@ -160,11 +166,12 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
160
166
  }
161
167
  void ensureServerUpdated();
162
168
  }, [checkoutState.type, ensureServerUpdated, normalizedServerUpdateKey, onServerUpdate]);
163
- const trackCheckoutSuccess = async () => {
169
+ const trackCheckoutSuccess = async (embedded = false) => {
164
170
  if (checkoutAnalytics) {
165
171
  await trackPaidStripeSubscriptionCheckoutCompleted(Object.assign({ checkoutSessionId }, checkoutAnalytics));
166
172
  }
167
- await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess({ checkoutSessionId: checkoutSessionId !== null && checkoutSessionId !== void 0 ? checkoutSessionId : null }));
173
+ if (!embedded)
174
+ await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess({ checkoutSessionId: checkoutSessionId !== null && checkoutSessionId !== void 0 ? checkoutSessionId : null }));
168
175
  };
169
176
  const queueCheckoutStarted = (paymentMethod, checkoutStartSource = 'wallet_click') => {
170
177
  if (!checkoutAnalytics) {
@@ -191,8 +198,35 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
191
198
  }
192
199
  onError === null || onError === void 0 ? void 0 : onError(isCheckoutEmailError(providerError) ? 'Enter a valid email address to continue.' : message);
193
200
  };
201
+ const verifyPaywall = async () => {
202
+ if (verificationInFlightRef.current || completedRef.current)
203
+ return true;
204
+ verificationInFlightRef.current = true;
205
+ setVerificationInFlight(true);
206
+ try {
207
+ return await completePaywallCheckout('stripe', checkoutSessionId || '', { beforeNotify: async () => {
208
+ completedRef.current = true;
209
+ setCompleted(true);
210
+ onEmbeddedComplete === null || onEmbeddedComplete === void 0 ? void 0 : onEmbeddedComplete();
211
+ await trackCheckoutSuccess(true);
212
+ } });
213
+ }
214
+ catch (error) {
215
+ const message = error instanceof Error ? error.message : 'Unable to confirm payment';
216
+ setVerificationError(message);
217
+ onError === null || onError === void 0 ? void 0 : onError(message);
218
+ onVerificationPending === null || onVerificationPending === void 0 ? void 0 : onVerificationPending();
219
+ return true;
220
+ }
221
+ finally {
222
+ verificationInFlightRef.current = false;
223
+ setVerificationInFlight(false);
224
+ }
225
+ };
194
226
  const handleConfirm = async (event) => {
195
227
  var _a, _b, _c, _d, _e, _f;
228
+ if (completedRef.current)
229
+ return;
196
230
  // Checkout Sessions omits the Express Checkout onClick callback. Confirmation
197
231
  // and cancellation are the reliable wallet-intent boundaries for this provider.
198
232
  // Session-based analytics deduplication keeps this idempotent if onClick fires.
@@ -287,6 +321,8 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
287
321
  }
288
322
  onError === null || onError === void 0 ? void 0 : onError(null);
289
323
  if (typeof window !== 'undefined') {
324
+ if (await verifyPaywall())
325
+ return;
290
326
  await trackCheckoutSuccess();
291
327
  window.location.assign(returnUrl);
292
328
  }
@@ -320,6 +356,10 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
320
356
  onCancel === null || onCancel === void 0 ? void 0 : onCancel();
321
357
  };
322
358
  const inertWhileUpdating = (serverUpdatePending ? { inert: true } : {});
359
+ if (verificationError && !completed)
360
+ return _jsxs("div", { className: 'bg-background text-foreground p-6', children: [_jsx("p", { role: 'alert', children: verificationError }), _jsx("button", { type: 'button', disabled: verificationInFlight, className: 'bg-accent text-foreground border-border rounded border p-3', onClick: () => void verifyPaywall(), children: "Check payment status" })] });
361
+ if (completed)
362
+ return _jsx("div", { role: 'status', className: 'bg-background text-foreground p-6', children: "Payment confirmed. You can return to the app." });
323
363
  return (_jsxs(_Fragment, { children: [_jsx("div", Object.assign({}, inertWhileUpdating, { "aria-busy": serverUpdatePending || undefined, "aria-disabled": serverUpdatePending || undefined, className: [
324
364
  'stripe-express-checkout-button',
325
365
  className !== null && className !== void 0 ? className : '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.15.11",
3
+ "version": "0.20.0",
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.106",
36
- "@funnelsgrove/runtime": "0.14.4",
35
+ "@funnelsgrove/analytics": "0.1.110",
36
+ "@funnelsgrove/runtime": "0.19.0",
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",