@funnelsgrove/payments 0.1.48 → 0.1.50

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 CHANGED
@@ -22,13 +22,17 @@ Use these for new subscription paywalls:
22
22
  - `WalletSubscriptionCheckoutSlot`
23
23
  - `trackPaidStripeSubscriptionCheckoutCompleted`
24
24
 
25
+ Use these for new one-time paywall plans:
26
+
27
+ - `createStripePaymentIntent`
28
+ - `SharedStripeCheckoutV2Dialog` with `checkoutMode="payment_intent"`
29
+
25
30
  Use `chargeStripeOneClickPayment` for post-checkout one-click upsells when the shared client helper fits the funnel. It delegates to `funnelSdkService.chargeOneClickPayment`.
26
31
 
27
32
  ## Compatibility-Only Surfaces
28
33
 
29
34
  Keep these exports available for saved drafts, old published artifacts, and controlled migrations. Do not use them for new subscription checkout work:
30
35
 
31
- - `createStripePaymentIntent`
32
36
  - `SharedStripeCheckoutDialog`
33
37
  - `ApplePaySubscribeButton`
34
38
  - `GooglePaySubscribeButton`
@@ -64,7 +68,7 @@ When a live funnel still uses one of these, migrate the funnel deliberately with
64
68
  - `services/planCatalog.service`: plan resolution and selection helpers.
65
69
  - `services/runtimeBillingPlanCatalog.service`: test/live runtime catalog selection.
66
70
  - `services/paywallOffer.service`: timed discount and display-plan helpers.
67
- - `services/stripe.service`: Stripe loader, checkout-session, hosted checkout, one-click, and compatibility PaymentIntent clients.
71
+ - `services/stripe.service`: Stripe loader, checkout-session, hosted checkout, one-click, and one-time PaymentIntent clients.
68
72
  - `providers/*`: provider-neutral checkout state and Stripe subscription checkout slots.
69
73
  - `components/shared/*`: plan selector, checkout dialogs, Express Checkout wrappers, wallet placeholders, and trust assets.
70
74
  - `testing/walletCheckoutSmoke`: wallet checkout smoke assertions.
@@ -74,7 +78,9 @@ When a live funnel still uses one of these, migrate the funnel deliberately with
74
78
  - Keep provider-specific implementation under `src/providers/<provider>`.
75
79
  - Stripe owns real card fields and wallet controls. Local wallet buttons are placeholders until Stripe confirms availability.
76
80
  - Wallet unavailable state must fall back to card/manual checkout or disappear.
77
- - Do not create checkout sessions until the visitor taps a visible checkout CTA.
81
+ - Do not count checkout intent until the visitor taps or confirms a visible checkout CTA.
82
+ - Wallet slots may create render-only Checkout Sessions on mount so Apple Pay and Google Pay render before the visitor taps. These sessions must use `checkoutStartSource: 'wallet_render'`; trusted server `checkout_started` analytics ignores that source.
83
+ - Funnel paywalls should style wallet buttons through the shared slot API: `className` for the Stripe button/placeholder, `slotClassName` for the outer slot, `appearance` for Stripe Elements appearance, and `options` for Stripe Express Checkout options.
78
84
  - Keep discount math reusable here, but keep offer activation timing in funnel code.
79
85
  - Checkout metadata must avoid sensitive raw query params and use `@funnelsgrove/analytics` metadata helpers.
80
86
 
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { Stripe } from '@stripe/stripe-js';
3
+ import { type StripeSubscriptionCheckoutAnalyticsContext } from '../../services/checkoutCompletionAnalytics.service.js';
3
4
  export type SharedStripeCheckoutSummaryRow = {
4
5
  id: string;
5
6
  label: string;
@@ -8,6 +9,8 @@ export type SharedStripeCheckoutSummaryRow = {
8
9
  };
9
10
  export type SharedStripeCheckoutDialogProps = {
10
11
  amountCents: number;
12
+ checkoutAnalytics?: StripeSubscriptionCheckoutAnalyticsContext | null;
13
+ checkoutSessionId?: string | null;
11
14
  clientSecret: string;
12
15
  closeAriaLabel: string;
13
16
  customerEmailEditable?: boolean;
@@ -41,4 +44,4 @@ export type SharedStripeCheckoutDialogProps = {
41
44
  };
42
45
  export declare function useApplePayQrFallbackAllowed(): boolean;
43
46
  export declare function useApplePayExpressCheckoutAllowed(): boolean;
44
- export declare function SharedStripeCheckoutDialog({ clientSecret, onClose, stripePromise, ...props }: SharedStripeCheckoutDialogProps): import("react/jsx-runtime").JSX.Element;
47
+ export declare function SharedStripeCheckoutDialog({ checkoutAnalytics, checkoutSessionId, clientSecret, onClose, stripePromise, ...props }: SharedStripeCheckoutDialogProps): import("react/jsx-runtime").JSX.Element;
@@ -16,6 +16,7 @@ import { CardCvcElement, CardExpiryElement, CardNumberElement, Elements, useElem
16
16
  import { ApplePaySubscribeButton } from './ApplePaySubscribeButton.js';
17
17
  import { PaymentBrandMark, SecurityLockIcon, StripeWordmark, } from './CheckoutBrandAssets.js';
18
18
  import { StripeExpressCheckoutButton } from './StripeExpressCheckoutButton.js';
19
+ import { trackStripeSubscriptionCheckoutStarted, } from '../../services/checkoutCompletionAnalytics.service.js';
19
20
  const isValidEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
20
21
  const defaultPaymentMethodLabels = ['Visa', 'Mastercard', 'PayPal', 'G Pay', 'Apple Pay'];
21
22
  const normalizeBillingName = (input) => {
@@ -244,7 +245,13 @@ function SharedStripeCheckoutForm({ amountCents, clientSecret, closeAriaLabel, c
244
245
  } }) })] })] })] }), fieldError ? _jsx("p", { className: 'shared-stripe-checkout-error', children: fieldError }) : null, _jsx("button", { type: 'submit', className: 'shared-stripe-checkout-submit', disabled: !canSubmit, children: submitting ? processingLabel : submitLabel }), _jsxs("div", { className: 'shared-stripe-checkout-trust', children: [_jsxs("p", { className: 'shared-stripe-checkout-secure', children: [_jsx(SecurityLockIcon, { className: 'shared-stripe-checkout-secure-icon' }), _jsx("span", { children: secureLabel })] }), _jsxs("p", { className: 'shared-stripe-checkout-powered', children: [poweredByText ? _jsx("span", { children: poweredByText }) : null, showsStripeWordmark ? (_jsx(StripeWordmark, { className: 'shared-stripe-checkout-powered-mark' })) : null] })] }), paymentMethodLabels.length > 0 ? (_jsx("div", { className: 'shared-stripe-checkout-brands', "aria-hidden": 'true', children: paymentMethodLabels.map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-stripe-checkout-brand' }, label))) })) : null, legalNotice ? (_jsx("div", { className: 'shared-stripe-checkout-legal', children: legalNotice })) : null, renewalNotice ? (_jsx("div", { className: 'shared-stripe-checkout-renewal', children: renewalNotice })) : null] })] }));
245
246
  }
246
247
  export function SharedStripeCheckoutDialog(_a) {
247
- var { clientSecret, onClose, stripePromise } = _a, props = __rest(_a, ["clientSecret", "onClose", "stripePromise"]);
248
+ var { checkoutAnalytics, checkoutSessionId, clientSecret, onClose, stripePromise } = _a, props = __rest(_a, ["checkoutAnalytics", "checkoutSessionId", "clientSecret", "onClose", "stripePromise"]);
249
+ useEffect(() => {
250
+ if (!checkoutAnalytics) {
251
+ return;
252
+ }
253
+ void trackStripeSubscriptionCheckoutStarted(Object.assign({ checkoutSessionId }, checkoutAnalytics));
254
+ }, [checkoutAnalytics, checkoutSessionId, clientSecret]);
248
255
  useEffect(() => {
249
256
  const handleKeyDown = (event) => {
250
257
  if (event.key === 'Escape') {
@@ -5,6 +5,7 @@ export type SharedStripeCheckoutV2DialogProps = {
5
5
  buttonColor: string;
6
6
  cardSubmitLabel: string;
7
7
  checkoutAnalytics?: StripeSubscriptionCheckoutAnalyticsContext | null;
8
+ checkoutMode?: 'checkout_session' | 'payment_intent';
8
9
  checkoutSessionId?: string | null;
9
10
  clientSecret: string;
10
11
  closeAriaLabel: string;
@@ -63,5 +64,5 @@ export type SharedCheckoutSpecialOfferDialogProps = {
63
64
  themeColor: string;
64
65
  title: string;
65
66
  };
66
- export declare function SharedStripeCheckoutV2Dialog({ checkoutAnalytics, checkoutSessionId, clientSecret, onClose, stripePromise, supportedCountries, ...props }: SharedStripeCheckoutV2DialogProps): import("react/jsx-runtime").JSX.Element;
67
+ export declare function SharedStripeCheckoutV2Dialog({ checkoutAnalytics, checkoutMode, checkoutSessionId, clientSecret, onClose, stripePromise, supportedCountries, ...props }: SharedStripeCheckoutV2DialogProps): import("react/jsx-runtime").JSX.Element;
67
68
  export declare function SharedCheckoutSpecialOfferDialog({ buttonColor, buttonLabel, description, discountLabel, imageAlt, imageSrc, onAccept, themeColor, title, }: SharedCheckoutSpecialOfferDialogProps): import("react/jsx-runtime").JSX.Element;
@@ -12,8 +12,10 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  };
13
13
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
14
14
  import { useEffect, useMemo, useState, } from 'react';
15
+ import { Elements, PaymentElement as PaymentIntentPaymentElement, useElements, useStripe, } from '@stripe/react-stripe-js';
15
16
  import { CheckoutProvider, PaymentElement, useCheckout, } from '@stripe/react-stripe-js/checkout';
16
17
  import { PaymentBrandMark, SecurityLockIcon } from './CheckoutBrandAssets.js';
18
+ import { StripeExpressCheckoutButton } from './StripeExpressCheckoutButton.js';
17
19
  import { StripeCheckoutExpressCheckoutButton } from './StripeCheckoutExpressCheckoutButton.js';
18
20
  import { getUnsupportedCheckoutCountryMessage, normalizeSupportedCheckoutCountries, } from './checkoutCountries.js';
19
21
  import { isInactiveCheckoutSessionError } from '../../providers/stripe/useStripeSubscriptionCheckoutSession.js';
@@ -59,7 +61,24 @@ function getFriendlyCardErrorMessage(error) {
59
61
  ? error.message
60
62
  : 'Payment failed. Please review your card details and try again.';
61
63
  }
62
- function SharedStripeCheckoutV2Form({ amountCents, buttonColor, cardSubmitLabel, closeAriaLabel, countdownLabel, checkoutAnalytics, checkoutSessionId, customerEmail, customerEmailEditable = false, customerEmailLabel = 'Email', customerEmailPlaceholder = 'Enter your email', customerEmailInvalidMessage = 'Enter a valid email address.', customerName, discountAmountLabel, discountLabel, discountPercent, initialWalletAvailable, onClose, onCustomerEmailChange, onCustomerEmailCommit, onError, onInactiveCheckoutSession, onPaymentInfoSubmitted, onSuccess, paymentMethodLabels = defaultPaymentMethodLabels, promoActive, processingLabel, promoCode, promoCodeLabel, remainingSeconds, returnUrl, savedLabel, secureLabel, satisfactionLabel, satisfactionPercent, summaryLabel, supportedCountries, themeColor, title, totalLabel, totalValue, unsupportedCountryMessage, walletButtonLabel, originalPriceLabel, originalPriceValue, }) {
64
+ const normalizeBillingName = (input) => {
65
+ var _a, _b, _c;
66
+ const normalizedName = (_a = input.name) === null || _a === void 0 ? void 0 : _a.trim();
67
+ if (normalizedName) {
68
+ return normalizedName;
69
+ }
70
+ const normalizedEmail = (_b = input.email) === null || _b === void 0 ? void 0 : _b.trim();
71
+ if (normalizedEmail) {
72
+ const localPart = (_c = normalizedEmail
73
+ .split('@')[0]) === null || _c === void 0 ? void 0 : _c.replace(/[._-]+/g, ' ').trim();
74
+ if (localPart) {
75
+ return localPart;
76
+ }
77
+ }
78
+ return 'Customer';
79
+ };
80
+ const isSuccessfulIntentStatus = (status) => status === 'succeeded' || status === 'processing' || status === 'requires_capture';
81
+ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, cardSubmitLabel, closeAriaLabel, countdownLabel, checkoutAnalytics, checkoutSessionId, customerEmail, customerEmailEditable = false, customerEmailLabel = 'Email', customerEmailPlaceholder = 'Enter your email', customerEmailInvalidMessage = 'Enter a valid email address.', customerName, discountAmountLabel, discountLabel, discountPercent, initialWalletAvailable, onClose, onCustomerEmailChange, onCustomerEmailCommit, onError, onInactiveCheckoutSession, onPaymentInfoSubmitted, onSuccess, paymentMethodLabels = defaultPaymentMethodLabels, promoActive, processingLabel, promoCode, promoCodeLabel, remainingSeconds, returnUrl, savedLabel, secureLabel, satisfactionLabel, satisfactionPercent, summaryLabel, supportedCountries, themeColor, title, totalLabel, totalValue, unsupportedCountryMessage, walletButtonLabel, originalPriceLabel, originalPriceValue, }) {
63
82
  const checkoutState = useCheckout();
64
83
  const walletPaymentMethods = usePlatformWalletPaymentMethods();
65
84
  const [selectedMethod, setSelectedMethod] = useState('wallet');
@@ -306,10 +325,272 @@ function SharedStripeCheckoutV2Form({ amountCents, buttonColor, cardSubmitLabel,
306
325
  onCustomerEmailChange === null || onCustomerEmailChange === void 0 ? void 0 : onCustomerEmailChange(event.target.value);
307
326
  } }) })] }), _jsx(PaymentElement, { options: paymentElementOptions, onChange: handlePaymentElementChange }), fixedBillingCountry && fixedBillingCountryLabel ? (_jsxs("label", { className: 'shared-checkout-v2-country-field', children: [_jsx("span", { className: 'shared-checkout-v2-country-label', children: "Country" }), _jsx("span", { className: 'shared-checkout-v2-country-control', children: _jsx("select", { "aria-label": 'Country', className: 'shared-checkout-v2-country-select', value: fixedBillingCountry, onChange: () => setBillingCountry(fixedBillingCountry), children: _jsx("option", { value: fixedBillingCountry, children: fixedBillingCountryLabel }) }) })] })) : null] }), visibleError ? _jsx("p", { className: 'shared-checkout-v2-error', children: visibleError }) : null, _jsxs("button", { type: 'submit', className: 'shared-checkout-v2-card-submit', disabled: submitting, children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-card-submit-icon' }), submitting ? processingLabel : cardSubmitLabel] }), _jsx("div", { className: 'shared-checkout-v2-card-brands', "aria-hidden": 'true', children: cardBrandLabels.map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-checkout-v2-card-brand' }, label))) }), _jsxs("p", { className: 'shared-checkout-v2-secure-pill shared-checkout-v2-secure-pill--card', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] })] })] })] }));
308
327
  }
309
- export function SharedStripeCheckoutV2Dialog(_a) {
310
- var { checkoutAnalytics, checkoutSessionId, clientSecret, onClose, stripePromise, supportedCountries } = _a, props = __rest(_a, ["checkoutAnalytics", "checkoutSessionId", "clientSecret", "onClose", "stripePromise", "supportedCountries"]);
328
+ function SharedStripeCheckoutV2PaymentIntentForm({ amountCents, buttonColor, cardSubmitLabel, closeAriaLabel, countdownLabel, customerEmail, customerEmailEditable = false, customerEmailLabel = 'Email', customerEmailPlaceholder = 'Enter your email', customerEmailInvalidMessage = 'Enter a valid email address.', customerName, discountAmountLabel, discountLabel, discountPercent, initialWalletAvailable, onClose, onCustomerEmailChange, onCustomerEmailCommit, onError, onPaymentInfoSubmitted, onSuccess, paymentMethodLabels = defaultPaymentMethodLabels, promoActive, processingLabel, promoCode, promoCodeLabel, remainingSeconds, returnUrl, savedLabel, secureLabel, satisfactionLabel, satisfactionPercent, summaryLabel, supportedCountries, themeColor, title, totalLabel, totalValue, unsupportedCountryMessage, walletButtonLabel, originalPriceLabel, originalPriceValue, }) {
329
+ const stripe = useStripe();
330
+ const elements = useElements();
331
+ const walletPaymentMethods = usePlatformWalletPaymentMethods();
332
+ const [selectedMethod, setSelectedMethod] = useState('wallet');
333
+ const [walletAvailable, setWalletAvailable] = useState(initialWalletAvailable !== null && initialWalletAvailable !== void 0 ? initialWalletAvailable : null);
334
+ const effectiveWalletAvailable = initialWalletAvailable === true && walletAvailable !== false
335
+ ? true
336
+ : walletAvailable;
337
+ const [submitting, setSubmitting] = useState(false);
338
+ const [error, setError] = useState(null);
339
+ const [billingCountry, setBillingCountry] = useState(null);
340
+ const initialCustomerEmail = (customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || '';
341
+ const [emailDraft, setEmailDraft] = useState(initialCustomerEmail);
311
342
  const normalizedSupportedCountries = useMemo(() => normalizeSupportedCheckoutCountries(supportedCountries), [supportedCountries]);
312
343
  const fixedBillingCountry = getFixedSupportedBillingCountry(normalizedSupportedCountries);
344
+ const fixedBillingCountryLabel = fixedBillingCountry
345
+ ? getCheckoutCountryLabel(fixedBillingCountry)
346
+ : null;
347
+ const cardBrandLabels = useMemo(() => paymentMethodLabels.length > 0 ? paymentMethodLabels : defaultPaymentMethodLabels, [paymentMethodLabels]);
348
+ const normalizedPromoCode = promoCode.trim();
349
+ const showPromo = promoActive !== null && promoActive !== void 0 ? promoActive : (discountPercent > 0 && normalizedPromoCode.length > 0);
350
+ const countdownValue = formatCountdown(remainingSeconds);
351
+ const resolvedCustomerEmail = customerEmailEditable
352
+ ? emailDraft.trim() || initialCustomerEmail
353
+ : initialCustomerEmail;
354
+ const emailFieldClassName = [
355
+ 'shared-checkout-v2-email-field',
356
+ customerEmailEditable ? '' : 'is-disabled',
357
+ ]
358
+ .filter(Boolean)
359
+ .join(' ');
360
+ const checkoutStyle = {
361
+ '--shared-checkout-v2-theme-color': themeColor,
362
+ '--shared-checkout-v2-button-color': buttonColor,
363
+ };
364
+ const expressCheckoutSummaryLabel = (summaryLabel === null || summaryLabel === void 0 ? void 0 : summaryLabel.trim()) || discountLabel || title;
365
+ const walletAriaLabel = walletPaymentMethods
366
+ .map((method) => method === 'applePay' ? walletButtonLabel : 'Google Pay')
367
+ .join(' / ');
368
+ const showWalletMethod = effectiveWalletAvailable !== false && walletPaymentMethods.length > 0;
369
+ const effectiveSelectedMethod = showWalletMethod && selectedMethod === 'wallet' ? 'wallet' : 'card';
370
+ const expressCheckoutOptions = useMemo(() => {
371
+ return {
372
+ buttonHeight: 55,
373
+ buttonTheme: {
374
+ applePay: 'black',
375
+ googlePay: 'black',
376
+ },
377
+ buttonType: {
378
+ applePay: 'buy',
379
+ googlePay: 'buy',
380
+ },
381
+ layout: {
382
+ maxColumns: 1,
383
+ maxRows: 2,
384
+ overflow: 'auto',
385
+ },
386
+ lineItems: [
387
+ {
388
+ name: expressCheckoutSummaryLabel,
389
+ amount: amountCents,
390
+ },
391
+ ],
392
+ paymentMethodOrder: getStripeWalletPaymentMethodOrder(walletPaymentMethods),
393
+ paymentMethods: {
394
+ amazonPay: 'never',
395
+ applePay: walletPaymentMethods.includes('applePay') ? 'always' : 'never',
396
+ googlePay: walletPaymentMethods.includes('googlePay') ? 'always' : 'never',
397
+ klarna: 'never',
398
+ link: 'never',
399
+ paypal: 'never',
400
+ },
401
+ };
402
+ }, [amountCents, expressCheckoutSummaryLabel, walletPaymentMethods]);
403
+ const paymentElementOptions = useMemo(() => {
404
+ return Object.assign(Object.assign({ fields: {
405
+ billingDetails: {
406
+ name: 'never',
407
+ email: 'never',
408
+ address: {
409
+ country: fixedBillingCountry ? 'never' : 'auto',
410
+ },
411
+ },
412
+ } }, (fixedBillingCountry
413
+ ? {
414
+ defaultValues: {
415
+ billingDetails: {
416
+ address: {
417
+ country: fixedBillingCountry,
418
+ },
419
+ },
420
+ },
421
+ }
422
+ : {})), { layout: 'tabs', wallets: {
423
+ applePay: 'never',
424
+ googlePay: 'never',
425
+ link: 'never',
426
+ } });
427
+ }, [fixedBillingCountry]);
428
+ const setSharedError = (message) => {
429
+ setError(message);
430
+ onError === null || onError === void 0 ? void 0 : onError(message);
431
+ };
432
+ const selectWalletMethod = () => {
433
+ if (showWalletMethod) {
434
+ setSelectedMethod('wallet');
435
+ }
436
+ };
437
+ const selectCardMethod = () => setSelectedMethod('card');
438
+ const handleWalletAvailabilityChange = (available) => {
439
+ setWalletAvailable(available);
440
+ if (!available && selectedMethod === 'wallet') {
441
+ setSelectedMethod('card');
442
+ }
443
+ };
444
+ const getBillingCountryError = (country) => getUnsupportedCheckoutCountryMessage({
445
+ billingCountry: country,
446
+ message: unsupportedCountryMessage,
447
+ supportedCountries: normalizedSupportedCountries,
448
+ });
449
+ const handlePaymentElementChange = (event) => {
450
+ var _a, _b;
451
+ setBillingCountry((_b = (_a = event.value.billingDetails) === null || _a === void 0 ? void 0 : _a.address.country) !== null && _b !== void 0 ? _b : fixedBillingCountry);
452
+ };
453
+ const trackCheckoutSuccess = async () => {
454
+ await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess());
455
+ };
456
+ useEffect(() => {
457
+ if (effectiveSelectedMethod !== 'card' || typeof window === 'undefined') {
458
+ return;
459
+ }
460
+ const resizeTimer = window.setTimeout(() => {
461
+ window.dispatchEvent(new Event('resize'));
462
+ }, 0);
463
+ return () => window.clearTimeout(resizeTimer);
464
+ }, [effectiveSelectedMethod]);
465
+ const commitCustomerEmail = async (normalizedEmail) => {
466
+ if (!customerEmailEditable || !onCustomerEmailCommit) {
467
+ return normalizedEmail;
468
+ }
469
+ const committedEmail = await onCustomerEmailCommit(normalizedEmail);
470
+ const nextEmail = typeof committedEmail === 'string' && committedEmail.trim()
471
+ ? committedEmail.trim()
472
+ : normalizedEmail;
473
+ setEmailDraft(nextEmail);
474
+ onCustomerEmailChange === null || onCustomerEmailChange === void 0 ? void 0 : onCustomerEmailChange(nextEmail);
475
+ return nextEmail;
476
+ };
477
+ const commitEmailDraftIfValid = (value) => {
478
+ const normalizedEmail = value.trim();
479
+ if (!customerEmailEditable || !onCustomerEmailCommit) {
480
+ return;
481
+ }
482
+ if (!normalizedEmail || !isValidEmail(normalizedEmail)) {
483
+ return;
484
+ }
485
+ void commitCustomerEmail(normalizedEmail).catch((commitError) => {
486
+ setSharedError(getFriendlyCardErrorMessage(commitError));
487
+ });
488
+ };
489
+ const ensureCustomerEmail = async () => {
490
+ const normalizedEmail = resolvedCustomerEmail.trim();
491
+ if (!normalizedEmail || !isValidEmail(normalizedEmail)) {
492
+ setSharedError(customerEmailInvalidMessage);
493
+ return null;
494
+ }
495
+ return commitCustomerEmail(normalizedEmail);
496
+ };
497
+ const handleSubmit = async (event) => {
498
+ var _a;
499
+ event.preventDefault();
500
+ if (!stripe || !elements || submitting) {
501
+ return;
502
+ }
503
+ const billingCountryError = getBillingCountryError(billingCountry !== null && billingCountry !== void 0 ? billingCountry : fixedBillingCountry);
504
+ if (billingCountryError) {
505
+ setSharedError(billingCountryError);
506
+ return;
507
+ }
508
+ setSubmitting(true);
509
+ setSharedError(null);
510
+ try {
511
+ const syncedEmail = await ensureCustomerEmail();
512
+ if (!syncedEmail) {
513
+ setSubmitting(false);
514
+ return;
515
+ }
516
+ const submitResult = await elements.submit();
517
+ if (submitResult.error) {
518
+ setSharedError(submitResult.error.message || 'Payment form is not ready yet.');
519
+ setSubmitting(false);
520
+ return;
521
+ }
522
+ await (onPaymentInfoSubmitted === null || onPaymentInfoSubmitted === void 0 ? void 0 : onPaymentInfoSubmitted());
523
+ const result = await stripe.confirmPayment({
524
+ elements,
525
+ confirmParams: {
526
+ payment_method_data: {
527
+ billing_details: {
528
+ email: syncedEmail || undefined,
529
+ name: normalizeBillingName({
530
+ email: syncedEmail,
531
+ name: customerName,
532
+ }),
533
+ },
534
+ },
535
+ return_url: returnUrl,
536
+ },
537
+ redirect: 'if_required',
538
+ });
539
+ if (result.error) {
540
+ const message = result.error.message || 'Payment failed.';
541
+ setSharedError(message);
542
+ setSubmitting(false);
543
+ return;
544
+ }
545
+ if (typeof window !== 'undefined' &&
546
+ isSuccessfulIntentStatus((_a = result.paymentIntent) === null || _a === void 0 ? void 0 : _a.status)) {
547
+ await trackCheckoutSuccess();
548
+ window.location.assign(returnUrl);
549
+ return;
550
+ }
551
+ setSubmitting(false);
552
+ }
553
+ catch (submitError) {
554
+ setSharedError(getFriendlyCardErrorMessage(submitError));
555
+ setSubmitting(false);
556
+ }
557
+ };
558
+ const visibleError = error;
559
+ return (_jsxs("div", { className: 'shared-checkout-v2-shell', 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 })] }), _jsxs("section", { className: 'shared-checkout-v2-summary', children: [showPromo ? (_jsxs("p", { className: 'shared-checkout-v2-countdown', children: [discountPercent, "% ", countdownLabel, " ", countdownValue, " min"] })) : null, _jsxs("p", { className: 'shared-checkout-v2-satisfaction', children: [_jsx("strong", { children: satisfactionPercent }), " ", satisfactionLabel] }), showPromo ? (_jsxs("div", { className: 'shared-checkout-v2-price-stack', children: [_jsxs("div", { className: 'shared-checkout-v2-price-row is-muted', children: [_jsx("span", { children: originalPriceLabel }), _jsx("span", { className: 'shared-checkout-v2-strike', children: originalPriceValue })] }), _jsxs("div", { className: 'shared-checkout-v2-price-row is-discount', children: [_jsx("strong", { children: discountLabel }), _jsx("strong", { children: discountAmountLabel })] }), _jsxs("div", { className: 'shared-checkout-v2-promo', children: [_jsx("span", { children: promoCodeLabel }), _jsx("strong", { children: normalizedPromoCode })] })] })) : null, _jsxs("div", { className: [
560
+ 'shared-checkout-v2-total',
561
+ showPromo ? '' : 'is-plain',
562
+ ]
563
+ .filter(Boolean)
564
+ .join(' '), children: [_jsx("span", { children: totalLabel }), _jsxs("strong", { children: [_jsx("svg", { viewBox: '0 0 24 24', "aria-hidden": 'true', children: _jsx("path", { d: 'M20.6 13.4 12.4 21.6a2.2 2.2 0 0 1-3.1 0L2.4 14.7a2.2 2.2 0 0 1-.6-1.6V5.4a2 2 0 0 1 2-2h7.7a2.2 2.2 0 0 1 1.6.6l7.5 7.5a1.4 1.4 0 0 1 0 1.9ZM7.2 8.1a1.2 1.2 0 1 0 0-2.4 1.2 1.2 0 0 0 0 2.4Z' }) }), totalValue] })] }), showPromo ? (_jsxs("p", { className: 'shared-checkout-v2-saved', children: [_jsx("span", { "aria-hidden": 'true', children: "\uD83D\uDD25" }), savedLabel] })) : null] }), _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: [
565
+ 'shared-checkout-v2-method',
566
+ effectiveSelectedMethod === 'wallet' ? 'is-selected' : '',
567
+ ]
568
+ .filter(Boolean)
569
+ .join(' '), onClick: selectWalletMethod, role: 'tab', "aria-selected": effectiveSelectedMethod === 'wallet', children: [_jsxs("span", { className: 'shared-checkout-v2-tab-wallets', "aria-hidden": 'true', children: [walletPaymentMethods.includes('applePay') ? (_jsx(PaymentBrandMark, { label: 'Apple Pay', className: 'shared-checkout-v2-tab-wallet' })) : null, walletPaymentMethods.includes('googlePay') ? (_jsx(PaymentBrandMark, { label: 'Google Pay', className: 'shared-checkout-v2-tab-wallet' })) : null] }), _jsx("span", { className: 'shared-checkout-v2-sr-only', children: walletAriaLabel })] }), _jsxs("button", { type: 'button', className: [
570
+ 'shared-checkout-v2-method',
571
+ effectiveSelectedMethod === 'card' ? 'is-selected' : '',
572
+ ]
573
+ .filter(Boolean)
574
+ .join(' '), onClick: selectCardMethod, role: 'tab', "aria-selected": effectiveSelectedMethod === 'card', children: [_jsx("strong", { children: "Credit card" }), _jsx("span", { className: 'shared-checkout-v2-tab-brands', "aria-hidden": 'true', children: cardBrandLabels.slice(0, 4).map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-checkout-v2-tab-brand' }, label))) })] })] })) : null, _jsxs("div", { hidden: effectiveSelectedMethod !== 'wallet', className: [
575
+ 'shared-checkout-v2-wallet-panel',
576
+ effectiveSelectedMethod === 'wallet' ? 'is-visible' : '',
577
+ ]
578
+ .filter(Boolean)
579
+ .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(StripeExpressCheckoutButton, { amountCents: amountCents, availabilityPaymentMethods: walletPaymentMethods, beforeConfirm: ensureCustomerEmail, className: 'shared-checkout-v2-express-buttons', customerEmail: resolvedCustomerEmail, customerName: customerName, initialAvailable: initialWalletAvailable, keepInitialAvailableOnReady: initialWalletAvailable === true, onAvailabilityChange: handleWalletAvailabilityChange, onError: setSharedError, onPaymentInfoSubmitted: onPaymentInfoSubmitted, onSuccess: trackCheckoutSuccess, options: expressCheckoutOptions, returnUrl: returnUrl, summaryLabel: expressCheckoutSummaryLabel }), _jsx("span", { className: 'shared-checkout-v2-sr-only', children: walletAriaLabel })] })] }), _jsxs("div", { hidden: effectiveSelectedMethod !== 'card', className: [
580
+ 'shared-checkout-v2-card-panel',
581
+ effectiveSelectedMethod === 'card' ? 'is-visible' : '',
582
+ ]
583
+ .filter(Boolean)
584
+ .join(' '), children: [_jsxs("div", { className: 'shared-checkout-v2-card-box', children: [_jsxs("label", { className: emailFieldClassName, children: [_jsx("span", { className: 'shared-checkout-v2-email-label', children: customerEmailLabel }), _jsx("span", { className: 'shared-checkout-v2-email-control', children: _jsx("input", { type: 'email', autoComplete: 'email', className: 'shared-checkout-v2-email-input', disabled: !customerEmailEditable, inputMode: 'email', readOnly: !customerEmailEditable, value: customerEmailEditable ? emailDraft : initialCustomerEmail, placeholder: customerEmailPlaceholder, onBlur: (event) => commitEmailDraftIfValid(event.currentTarget.value), onChange: (event) => {
585
+ if (!customerEmailEditable) {
586
+ return;
587
+ }
588
+ setEmailDraft(event.target.value);
589
+ onCustomerEmailChange === null || onCustomerEmailChange === void 0 ? void 0 : onCustomerEmailChange(event.target.value);
590
+ } }) })] }), _jsx(PaymentIntentPaymentElement, { options: paymentElementOptions, onChange: handlePaymentElementChange }), fixedBillingCountry && fixedBillingCountryLabel ? (_jsxs("label", { className: 'shared-checkout-v2-country-field', children: [_jsx("span", { className: 'shared-checkout-v2-country-label', children: "Country" }), _jsx("span", { className: 'shared-checkout-v2-country-control', children: _jsx("select", { "aria-label": 'Country', className: 'shared-checkout-v2-country-select', value: fixedBillingCountry, onChange: () => setBillingCountry(fixedBillingCountry), children: _jsx("option", { value: fixedBillingCountry, children: fixedBillingCountryLabel }) }) })] })) : null] }), visibleError ? _jsx("p", { className: 'shared-checkout-v2-error', children: visibleError }) : null, _jsxs("button", { type: 'submit', className: 'shared-checkout-v2-card-submit', disabled: submitting, children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-card-submit-icon' }), submitting ? processingLabel : cardSubmitLabel] }), _jsx("div", { className: 'shared-checkout-v2-card-brands', "aria-hidden": 'true', children: cardBrandLabels.map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-checkout-v2-card-brand' }, label))) }), _jsxs("p", { className: 'shared-checkout-v2-secure-pill shared-checkout-v2-secure-pill--card', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] })] })] })] }));
591
+ }
592
+ function CheckoutSessionProvider({ children, clientSecret, fixedBillingCountry, stripePromise, }) {
593
+ const providerChildren = children;
313
594
  const checkoutProviderOptions = useMemo(() => (Object.assign(Object.assign({ clientSecret }, (fixedBillingCountry
314
595
  ? {
315
596
  defaultValues: {
@@ -323,12 +604,26 @@ export function SharedStripeCheckoutV2Dialog(_a) {
323
604
  : {})), { elementsOptions: {
324
605
  appearance: stripeCheckoutV2Appearance,
325
606
  } })), [clientSecret, fixedBillingCountry]);
607
+ return (_jsx(CheckoutProvider, { stripe: stripePromise, options: checkoutProviderOptions, children: providerChildren }, clientSecret));
608
+ }
609
+ function PaymentIntentProvider({ children, clientSecret, stripePromise, }) {
610
+ const providerChildren = children;
611
+ const elementsOptions = useMemo(() => ({
612
+ clientSecret,
613
+ appearance: stripeCheckoutV2Appearance,
614
+ }), [clientSecret]);
615
+ return (_jsx(Elements, { stripe: stripePromise, options: elementsOptions, children: providerChildren }, clientSecret));
616
+ }
617
+ export function SharedStripeCheckoutV2Dialog(_a) {
618
+ var { checkoutAnalytics, checkoutMode = 'checkout_session', checkoutSessionId, clientSecret, onClose, stripePromise, supportedCountries } = _a, props = __rest(_a, ["checkoutAnalytics", "checkoutMode", "checkoutSessionId", "clientSecret", "onClose", "stripePromise", "supportedCountries"]);
619
+ const normalizedSupportedCountries = useMemo(() => normalizeSupportedCheckoutCountries(supportedCountries), [supportedCountries]);
620
+ const fixedBillingCountry = getFixedSupportedBillingCountry(normalizedSupportedCountries);
326
621
  useEffect(() => {
327
- if (!checkoutAnalytics) {
622
+ if (checkoutMode !== 'checkout_session' || !checkoutAnalytics) {
328
623
  return;
329
624
  }
330
625
  void trackStripeSubscriptionCheckoutStarted(Object.assign({ checkoutSessionId }, checkoutAnalytics));
331
- }, [checkoutAnalytics, checkoutSessionId, clientSecret]);
626
+ }, [checkoutAnalytics, checkoutMode, checkoutSessionId, clientSecret]);
332
627
  useEffect(() => {
333
628
  const handleKeyDown = (event) => {
334
629
  if (event.key === 'Escape') {
@@ -338,7 +633,7 @@ export function SharedStripeCheckoutV2Dialog(_a) {
338
633
  window.addEventListener('keydown', handleKeyDown);
339
634
  return () => window.removeEventListener('keydown', handleKeyDown);
340
635
  }, [onClose]);
341
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: 'shared-checkout-v2-overlay', children: _jsx("div", { className: 'shared-checkout-v2-dialog', role: 'dialog', "aria-modal": 'true', "aria-labelledby": 'shared-checkout-v2-title', children: _jsx(CheckoutProvider, { stripe: stripePromise, options: checkoutProviderOptions, children: _jsx(SharedStripeCheckoutV2Form, Object.assign({}, props, { checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, clientSecret: clientSecret, onClose: onClose, supportedCountries: normalizedSupportedCountries })) }, clientSecret) }) }), _jsx("style", { children: sharedStripeCheckoutV2Styles })] }));
636
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: 'shared-checkout-v2-overlay', children: _jsx("div", { className: 'shared-checkout-v2-dialog', role: 'dialog', "aria-modal": 'true', "aria-labelledby": 'shared-checkout-v2-title', children: checkoutMode === 'payment_intent' ? (_jsx(PaymentIntentProvider, { clientSecret: clientSecret, stripePromise: stripePromise, children: _jsx(SharedStripeCheckoutV2PaymentIntentForm, Object.assign({}, props, { clientSecret: clientSecret, onClose: onClose, supportedCountries: normalizedSupportedCountries })) })) : (_jsx(CheckoutSessionProvider, { clientSecret: clientSecret, fixedBillingCountry: fixedBillingCountry, stripePromise: stripePromise, children: _jsx(SharedStripeCheckoutV2CheckoutSessionForm, Object.assign({}, props, { checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, clientSecret: clientSecret, onClose: onClose, supportedCountries: normalizedSupportedCountries })) })) }) }), _jsx("style", { children: sharedStripeCheckoutV2Styles })] }));
342
637
  }
343
638
  function SharedCheckoutSpecialOfferGift({ discountLabel, imageAlt, imageSrc, }) {
344
639
  if (imageSrc) {
@@ -1,5 +1,7 @@
1
+ import type { StripeCheckoutExpressCheckoutElementOptions } from '@stripe/stripe-js';
1
2
  import { type WalletSubscriptionCheckoutSlotProps } from './WalletSubscriptionCheckoutSlot.js';
2
- export type ApplePaySubscriptionCheckoutSlotProps = Omit<WalletSubscriptionCheckoutSlotProps, 'availabilityPaymentMethod' | 'expressCheckoutAllowed' | 'placeholderLabel' | 'renderPlaceholder' | 'renderPreparingPlaceholder'> & {
3
+ export type ApplePaySubscriptionCheckoutSlotProps = Omit<WalletSubscriptionCheckoutSlotProps, 'availabilityPaymentMethod' | 'expressCheckoutAllowed' | 'options' | 'placeholderLabel' | 'renderPlaceholder' | 'renderPreparingPlaceholder'> & {
4
+ options?: Partial<StripeCheckoutExpressCheckoutElementOptions>;
3
5
  placeholderLabel?: string;
4
6
  };
5
- export declare function ApplePaySubscriptionCheckoutSlot({ placeholderLabel, ...slotProps }: ApplePaySubscriptionCheckoutSlotProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function ApplePaySubscriptionCheckoutSlot({ options, placeholderLabel, ...slotProps }: ApplePaySubscriptionCheckoutSlotProps): import("react/jsx-runtime").JSX.Element;
@@ -13,7 +13,31 @@ var __rest = (this && this.__rest) || function (s, e) {
13
13
  import { jsx as _jsx } from "react/jsx-runtime";
14
14
  import { ApplePaySubscribeButton } from '../../components/shared/ApplePaySubscribeButton.js';
15
15
  import { WalletSubscriptionCheckoutLoadingPlaceholder, WalletSubscriptionCheckoutSlot, } from './WalletSubscriptionCheckoutSlot.js';
16
+ const applePayExpressCheckoutOptions = {
17
+ buttonHeight: 55,
18
+ buttonTheme: {
19
+ applePay: 'black',
20
+ },
21
+ buttonType: {
22
+ applePay: 'subscribe',
23
+ },
24
+ layout: {
25
+ maxColumns: 1,
26
+ maxRows: 1,
27
+ overflow: 'auto',
28
+ },
29
+ paymentMethodOrder: ['apple_pay'],
30
+ paymentMethods: {
31
+ amazonPay: 'never',
32
+ applePay: 'always',
33
+ googlePay: 'never',
34
+ klarna: 'never',
35
+ link: 'never',
36
+ paypal: 'never',
37
+ },
38
+ };
16
39
  export function ApplePaySubscriptionCheckoutSlot(_a) {
17
- var { placeholderLabel = 'Subscribe with Apple Pay' } = _a, slotProps = __rest(_a, ["placeholderLabel"]);
18
- return (_jsx(WalletSubscriptionCheckoutSlot, Object.assign({}, slotProps, { availabilityPaymentMethod: 'applePay', placeholderLabel: placeholderLabel, renderPreparingPlaceholder: (placeholderProps) => (_jsx(WalletSubscriptionCheckoutLoadingPlaceholder, Object.assign({}, placeholderProps, { label: 'Loading Apple Pay' }))), renderPlaceholder: (placeholderProps) => (_jsx(ApplePaySubscribeButton, Object.assign({}, placeholderProps))) })));
40
+ var _b, _c, _d, _e, _f, _g, _h, _j, _k;
41
+ var { options, placeholderLabel = 'Subscribe with Apple Pay' } = _a, slotProps = __rest(_a, ["options", "placeholderLabel"]);
42
+ return (_jsx(WalletSubscriptionCheckoutSlot, Object.assign({}, slotProps, { availabilityPaymentMethod: 'applePay', options: Object.assign(Object.assign(Object.assign({}, applePayExpressCheckoutOptions), (options !== null && options !== void 0 ? options : {})), { buttonTheme: Object.assign(Object.assign({}, ((_b = applePayExpressCheckoutOptions.buttonTheme) !== null && _b !== void 0 ? _b : {})), ((_c = options === null || options === void 0 ? void 0 : options.buttonTheme) !== null && _c !== void 0 ? _c : {})), buttonType: Object.assign(Object.assign({}, ((_d = applePayExpressCheckoutOptions.buttonType) !== null && _d !== void 0 ? _d : {})), ((_e = options === null || options === void 0 ? void 0 : options.buttonType) !== null && _e !== void 0 ? _e : {})), layout: Object.assign(Object.assign({}, ((_f = applePayExpressCheckoutOptions.layout) !== null && _f !== void 0 ? _f : {})), ((_g = options === null || options === void 0 ? void 0 : options.layout) !== null && _g !== void 0 ? _g : {})), paymentMethods: Object.assign(Object.assign({}, ((_h = applePayExpressCheckoutOptions.paymentMethods) !== null && _h !== void 0 ? _h : {})), ((_j = options === null || options === void 0 ? void 0 : options.paymentMethods) !== null && _j !== void 0 ? _j : {})), paymentMethodOrder: (_k = options === null || options === void 0 ? void 0 : options.paymentMethodOrder) !== null && _k !== void 0 ? _k : applePayExpressCheckoutOptions.paymentMethodOrder }), placeholderLabel: placeholderLabel, renderPreparingPlaceholder: (placeholderProps) => (_jsx(WalletSubscriptionCheckoutLoadingPlaceholder, Object.assign({}, placeholderProps, { label: 'Loading Apple Pay' }))), renderPlaceholder: (placeholderProps) => (_jsx(ApplePaySubscribeButton, Object.assign({}, placeholderProps))) })));
19
43
  }
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { AvailablePaymentMethods, StripeCheckoutExpressCheckoutElementOptions } from '@stripe/stripe-js';
3
- import type { StripeSubscriptionCheckoutAnalyticsContext } from '../../services/checkoutCompletionAnalytics.service.js';
2
+ import type { Appearance, AvailablePaymentMethods, StripeCheckoutExpressCheckoutElementOptions } from '@stripe/stripe-js';
3
+ import { type StripeSubscriptionCheckoutAnalyticsContext } from '../../services/checkoutCompletionAnalytics.service.js';
4
4
  import { type StripeSubscriptionCheckoutSession } from './useStripeSubscriptionCheckoutSession.js';
5
5
  export type WalletSubscriptionCheckoutRenderStateInput = {
6
6
  expressCheckoutAllowed: boolean;
@@ -27,6 +27,7 @@ export type WalletSubscriptionCheckoutSlotProps = {
27
27
  availabilityPaymentMethod: keyof AvailablePaymentMethods;
28
28
  beforeConfirm?: () => Promise<string | null>;
29
29
  checkoutAnalytics?: StripeSubscriptionCheckoutAnalyticsContext | null;
30
+ appearance?: Appearance;
30
31
  className?: string;
31
32
  customerEmail?: string | null;
32
33
  customerName?: string | null;
@@ -44,9 +45,10 @@ export type WalletSubscriptionCheckoutSlotProps = {
44
45
  renderPlaceholder: (props: WalletPlaceholderButtonProps) => ReactNode;
45
46
  returnUrl: string;
46
47
  session: StripeSubscriptionCheckoutSession;
48
+ slotClassName?: string;
47
49
  summaryLabel: string;
48
50
  suspended?: boolean;
49
51
  };
50
52
  export declare function WalletSubscriptionCheckoutLoadingPlaceholder({ className, label, }: Pick<WalletPlaceholderButtonProps, 'className' | 'label'>): import("react/jsx-runtime").JSX.Element;
51
- export declare function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymentMethod, beforeConfirm, checkoutAnalytics, className, customerEmail, customerName, disabled, expressCheckoutAllowed, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, options, placeholderLabel, renderPreparingPlaceholder, renderPlaceholder, returnUrl, session, summaryLabel, suspended, }: WalletSubscriptionCheckoutSlotProps): import("react/jsx-runtime").JSX.Element;
53
+ export declare function WalletSubscriptionCheckoutSlot({ amountCents, appearance, availabilityPaymentMethod, beforeConfirm, checkoutAnalytics, className, customerEmail, customerName, disabled, expressCheckoutAllowed, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, options, placeholderLabel, renderPreparingPlaceholder, renderPlaceholder, returnUrl, session, slotClassName, summaryLabel, suspended, }: WalletSubscriptionCheckoutSlotProps): import("react/jsx-runtime").JSX.Element;
52
54
  export {};
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useRef, useState } from 'react';
4
4
  import { StripeExpressCheckoutElement } from '../../components/shared/StripeExpressCheckoutElement.js';
5
+ import { trackStripeSubscriptionCheckoutStarted, } from '../../services/checkoutCompletionAnalytics.service.js';
5
6
  import { isInactiveCheckoutSessionError, } from './useStripeSubscriptionCheckoutSession.js';
6
7
  export function getWalletSubscriptionCheckoutRenderState({ expressCheckoutAllowed, hasActiveClientSecret, hasStripePromise, suspended, walletAvailable, walletPreparing, }) {
7
8
  const shouldRenderExpressCheckout = !suspended &&
@@ -27,12 +28,14 @@ export function WalletSubscriptionCheckoutLoadingPlaceholder({ className, label,
27
28
  .filter(Boolean)
28
29
  .join(' '), "aria-label": label, role: 'status', children: [_jsx("span", { className: 'wallet-subscription-checkout-slot__loading-bar' }), _jsx("style", { children: walletSubscriptionCheckoutLoadingPlaceholderStyles })] }));
29
30
  }
30
- export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymentMethod, beforeConfirm, checkoutAnalytics, className, customerEmail, customerName, disabled = false, expressCheckoutAllowed = true, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, options, placeholderLabel, renderPreparingPlaceholder, renderPlaceholder, returnUrl, session, summaryLabel, suspended = false, }) {
31
+ export function WalletSubscriptionCheckoutSlot({ amountCents, appearance, availabilityPaymentMethod, beforeConfirm, checkoutAnalytics, className, customerEmail, customerName, disabled = false, expressCheckoutAllowed = true, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, options, placeholderLabel, renderPreparingPlaceholder, renderPlaceholder, returnUrl, session, slotClassName, summaryLabel, suspended = false, }) {
31
32
  const [walletAvailability, setWalletAvailability] = useState({
32
33
  available: null,
33
34
  intentKey: session.intentKey,
34
35
  });
35
- const autoWalletAttemptedIntentKeyRef = useRef(null);
36
+ const [walletCheckoutStartedAfterClickIntentKey, setWalletCheckoutStartedAfterClickIntentKey] = useState(null);
37
+ const autoWalletPreparationIntentKeyRef = useRef(null);
38
+ const walletCheckoutStartedAfterClickSessionIdRef = useRef(null);
36
39
  const slotDisabled = disabled || !session.configured;
37
40
  const walletAvailable = walletAvailability.intentKey === session.intentKey
38
41
  ? walletAvailability.available
@@ -70,35 +73,61 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymen
70
73
  suspended ||
71
74
  !expressCheckoutAllowed ||
72
75
  walletAvailable === false ||
76
+ session.loading.wallet ||
73
77
  session.activeClientSecret) {
74
78
  return;
75
79
  }
76
- const requestKey = session.intentKey;
77
- if (autoWalletAttemptedIntentKeyRef.current === requestKey) {
80
+ if (autoWalletPreparationIntentKeyRef.current === session.intentKey) {
78
81
  return;
79
82
  }
80
- autoWalletAttemptedIntentKeyRef.current = requestKey;
81
- void (async () => {
82
- const readyClientSecret = await session.prepareWalletCheckout();
83
+ autoWalletPreparationIntentKeyRef.current = session.intentKey;
84
+ void session
85
+ .prepareWalletCheckout({
86
+ checkoutStartSource: 'wallet_render',
87
+ })
88
+ .then((readyClientSecret) => {
83
89
  if (!readyClientSecret) {
84
- reportWalletPreparationFailure(requestKey);
90
+ reportWalletPreparationFailure(session.intentKey);
85
91
  }
86
- })();
92
+ });
87
93
  }, [
88
94
  expressCheckoutAllowed,
89
95
  reportWalletPreparationFailure,
90
- session,
96
+ session.activeClientSecret,
97
+ session.intentKey,
98
+ session.loading.wallet,
99
+ session.prepareWalletCheckout,
91
100
  slotDisabled,
92
101
  suspended,
93
102
  walletAvailable,
94
103
  ]);
104
+ useEffect(() => {
105
+ if (!walletCheckoutStartedAfterClickIntentKey ||
106
+ walletCheckoutStartedAfterClickIntentKey !== session.intentKey ||
107
+ !session.checkoutSessionId) {
108
+ return;
109
+ }
110
+ if (!checkoutAnalytics ||
111
+ walletCheckoutStartedAfterClickSessionIdRef.current === session.checkoutSessionId) {
112
+ return;
113
+ }
114
+ walletCheckoutStartedAfterClickSessionIdRef.current = session.checkoutSessionId;
115
+ void trackStripeSubscriptionCheckoutStarted(Object.assign({ checkoutSessionId: session.checkoutSessionId }, checkoutAnalytics));
116
+ }, [
117
+ checkoutAnalytics,
118
+ session.checkoutSessionId,
119
+ session.intentKey,
120
+ walletCheckoutStartedAfterClickIntentKey,
121
+ ]);
95
122
  const handleError = (message) => {
96
123
  if (isInactiveCheckoutSessionError(message)) {
97
124
  setWalletAvailability({
98
125
  available: null,
99
126
  intentKey: session.intentKey,
100
127
  });
101
- void session.restartWalletCheckout();
128
+ void session.restartWalletCheckout({
129
+ checkoutStartSource: 'wallet_render',
130
+ });
102
131
  return;
103
132
  }
104
133
  session.setError(message);
@@ -115,8 +144,12 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymen
115
144
  await (onUnavailableClick === null || onUnavailableClick === void 0 ? void 0 : onUnavailableClick());
116
145
  return;
117
146
  }
118
- const readyClientSecret = await session.prepareWalletCheckout();
147
+ setWalletCheckoutStartedAfterClickIntentKey(session.intentKey);
148
+ const readyClientSecret = await session.prepareWalletCheckout({
149
+ checkoutStartSource: 'wallet_render',
150
+ });
119
151
  if (!readyClientSecret) {
152
+ setWalletCheckoutStartedAfterClickIntentKey(null);
120
153
  reportWalletPreparationFailure(session.intentKey);
121
154
  }
122
155
  };
@@ -125,13 +158,14 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymen
125
158
  : renderPlaceholder;
126
159
  return (_jsxs(_Fragment, { children: [shouldRenderPlaceholder || shouldRenderExpressCheckout ? (_jsxs("div", { className: [
127
160
  'wallet-subscription-checkout-slot',
161
+ slotClassName !== null && slotClassName !== void 0 ? slotClassName : '',
128
162
  placeholderIsPreparing ? 'is-loading' : '',
129
163
  shouldRenderExpressCheckout ? 'has-express-checkout' : '',
130
164
  ]
131
165
  .filter(Boolean)
132
166
  .join(' '), "aria-busy": placeholderIsPreparing || undefined, children: [shouldRenderExpressCheckout &&
133
167
  session.activeClientSecret &&
134
- session.stripePromise ? (_jsx(StripeExpressCheckoutElement, { amountCents: amountCents, availabilityPaymentMethods: [availabilityPaymentMethod], beforeConfirm: beforeConfirm, checkoutAnalytics: checkoutAnalytics, checkoutSessionId: session.checkoutSessionId, className: className, clientSecret: session.activeClientSecret, customerEmail: customerEmail, customerName: customerName, onAvailabilityChange: handleAvailabilityChange, onCancel: onCancel, onError: handleError, onPaymentInfoSubmitted: onPaymentInfoSubmitted, onSuccess: onSuccess, options: options, returnUrl: returnUrl, serverUpdateKey: serverUpdateKey, stripePromise: session.stripePromise, summaryLabel: summaryLabel, onServerUpdate: session.updateCheckoutSessionPlan })) : null, shouldRenderPlaceholder ? (_jsx("div", { className: 'wallet-subscription-checkout-slot__placeholder', children: placeholderRenderer({
168
+ session.stripePromise ? (_jsx(StripeExpressCheckoutElement, { amountCents: amountCents, appearance: appearance, availabilityPaymentMethods: [availabilityPaymentMethod], beforeConfirm: beforeConfirm, checkoutAnalytics: checkoutAnalytics, checkoutSessionId: session.checkoutSessionId, className: className, clientSecret: session.activeClientSecret, customerEmail: customerEmail, customerName: customerName, onAvailabilityChange: handleAvailabilityChange, onCancel: onCancel, onError: handleError, onPaymentInfoSubmitted: onPaymentInfoSubmitted, onSuccess: onSuccess, options: options, returnUrl: returnUrl, serverUpdateKey: serverUpdateKey, stripePromise: session.stripePromise, summaryLabel: summaryLabel, onServerUpdate: session.updateCheckoutSessionPlan })) : null, shouldRenderPlaceholder ? (_jsx("div", { className: 'wallet-subscription-checkout-slot__placeholder', children: placeholderRenderer({
135
169
  className,
136
170
  disabled: slotDisabled,
137
171
  label: placeholderLabel,
@@ -16,6 +16,10 @@ export type StripeSubscriptionCheckoutSessionInput = {
16
16
  runtimeConfig?: StripeRuntimeConfigOverrides;
17
17
  userId?: string | null;
18
18
  };
19
+ export type StripeSubscriptionCheckoutPreparationOptions = {
20
+ checkoutStartSource?: 'card_click' | 'wallet_click' | 'wallet_render' | (string & {});
21
+ forceNew?: boolean;
22
+ };
19
23
  export type StripeSubscriptionCheckoutSession = {
20
24
  activeClientSecret: string | null;
21
25
  activePlanKey: string | null;
@@ -24,12 +28,12 @@ export type StripeSubscriptionCheckoutSession = {
24
28
  error: string | null;
25
29
  intentKey: string;
26
30
  loading: CheckoutPreparationLoading;
27
- prepareCardCheckout: () => Promise<string | null>;
28
- prepareWalletCheckout: () => Promise<string | null>;
31
+ prepareCardCheckout: (options?: StripeSubscriptionCheckoutPreparationOptions) => Promise<string | null>;
32
+ prepareWalletCheckout: (options?: StripeSubscriptionCheckoutPreparationOptions) => Promise<string | null>;
29
33
  provider: 'stripe';
30
34
  reset: () => void;
31
- restartCardCheckout: () => Promise<string | null>;
32
- restartWalletCheckout: () => Promise<string | null>;
35
+ restartCardCheckout: (options?: StripeSubscriptionCheckoutPreparationOptions) => Promise<string | null>;
36
+ restartWalletCheckout: (options?: StripeSubscriptionCheckoutPreparationOptions) => Promise<string | null>;
33
37
  planKey: string;
34
38
  setError: (message: string | null) => void;
35
39
  stripePromise: Promise<Stripe | null> | null;
@@ -10,6 +10,14 @@ const getFriendlyStripeCheckoutError = (error) => {
10
10
  };
11
11
  const normalizeRuntimeConfigValue = (value) => (value === null || value === void 0 ? void 0 : value.trim()) || '';
12
12
  const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
13
+ const buildCheckoutPreparationAnalyticsMetadata = (analyticsMetadata, options = {}) => {
14
+ var _a;
15
+ const checkoutStartSource = (_a = options.checkoutStartSource) === null || _a === void 0 ? void 0 : _a.trim();
16
+ if (!checkoutStartSource) {
17
+ return analyticsMetadata;
18
+ }
19
+ return Object.assign(Object.assign({}, (analyticsMetadata !== null && analyticsMetadata !== void 0 ? analyticsMetadata : {})), { checkoutStartSource });
20
+ };
13
21
  export function buildStripeSubscriptionCheckoutIntentKey({ checkoutMode, couponId, customerEmail, returnUrl, runtimeConfig, userId, }) {
14
22
  const runtimeConfigSignature = [
15
23
  normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.apiBaseUrl),
@@ -110,8 +118,9 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
110
118
  ? current
111
119
  : Object.assign(Object.assign({}, current), { [source]: value }));
112
120
  }, []);
113
- const createCheckoutSubscription = useCallback(async (forceNew = false) => {
121
+ const createCheckoutSubscription = useCallback(async (options = {}) => {
114
122
  var _a;
123
+ const forceNew = options.forceNew === true;
115
124
  if (!configured || !plan || !displayPlan) {
116
125
  return null;
117
126
  }
@@ -133,7 +142,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
133
142
  try {
134
143
  const response = await createStripeSubscriptionCheckout({
135
144
  plan,
136
- analyticsMetadata: analyticsMetadataRef.current,
145
+ analyticsMetadata: buildCheckoutPreparationAnalyticsMetadata(analyticsMetadataRef.current, options),
137
146
  couponId,
138
147
  customerEmail,
139
148
  returnUrl,
@@ -192,6 +201,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
192
201
  userId,
193
202
  ]);
194
203
  const prepareCheckout = useCallback(async (source, options = {}) => {
204
+ var _a;
195
205
  if (!configured || !plan || !displayPlan) {
196
206
  return null;
197
207
  }
@@ -201,7 +211,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
201
211
  setSourceLoading(source, true);
202
212
  setError(null);
203
213
  try {
204
- return await createCheckoutSubscription(options.forceNew === true);
214
+ return await createCheckoutSubscription(Object.assign(Object.assign({}, options), { checkoutStartSource: (_a = options.checkoutStartSource) !== null && _a !== void 0 ? _a : (source === 'wallet' ? 'wallet_render' : 'card_click') }));
205
215
  }
206
216
  finally {
207
217
  setSourceLoading(source, false);
@@ -215,10 +225,10 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
215
225
  setError,
216
226
  setSourceLoading,
217
227
  ]);
218
- const prepareCardCheckout = useCallback(() => prepareCheckout('card'), [prepareCheckout]);
219
- const prepareWalletCheckout = useCallback(() => prepareCheckout('wallet'), [prepareCheckout]);
220
- const restartCardCheckout = useCallback(() => prepareCheckout('card', { forceNew: true }), [prepareCheckout]);
221
- const restartWalletCheckout = useCallback(() => prepareCheckout('wallet', { forceNew: true }), [prepareCheckout]);
228
+ const prepareCardCheckout = useCallback((options = {}) => prepareCheckout('card', options), [prepareCheckout]);
229
+ const prepareWalletCheckout = useCallback((options = {}) => prepareCheckout('wallet', options), [prepareCheckout]);
230
+ const restartCardCheckout = useCallback((options = {}) => prepareCheckout('card', Object.assign(Object.assign({}, options), { forceNew: true })), [prepareCheckout]);
231
+ const restartWalletCheckout = useCallback((options = {}) => prepareCheckout('wallet', Object.assign(Object.assign({}, options), { forceNew: true })), [prepareCheckout]);
222
232
  const updateCheckoutSessionPlan = useCallback(async () => {
223
233
  if (!configured || !plan || !displayPlan || !activeCheckoutSessionId) {
224
234
  return false;
@@ -1,4 +1,4 @@
1
- export type WalletCheckoutSmokeProfileId = 'mobile-safari' | 'mobile-chrome';
1
+ export type WalletCheckoutSmokeProfileId = 'mobile-safari' | 'mobile-chrome' | 'instagram-ios' | 'instagram-android';
2
2
  export type WalletCheckoutSmokeProfile = {
3
3
  id: WalletCheckoutSmokeProfileId;
4
4
  name: string;
@@ -22,6 +22,28 @@ export function buildWalletCheckoutSmokeProfiles() {
22
22
  isMobile: true,
23
23
  },
24
24
  },
25
+ {
26
+ id: 'instagram-ios',
27
+ name: 'Instagram iOS in-app browser',
28
+ userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Instagram 333.0.0.36.109 (iPhone16,2; iOS 17_5; en_US; en-US; scale=3.00; 1290x2796; 579418012)',
29
+ viewport: {
30
+ width: 390,
31
+ height: 844,
32
+ hasTouch: true,
33
+ isMobile: true,
34
+ },
35
+ },
36
+ {
37
+ id: 'instagram-android',
38
+ name: 'Instagram Android in-app browser',
39
+ userAgent: 'Mozilla/5.0 (Linux; Android 14; Pixel 8 Build/AP2A.240605.024; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.0.0 Mobile Safari/537.36 Instagram 333.0.0.39.92 Android (34/14; 420dpi; 1080x2400; Google/google; Pixel 8; shiba; shiba; en_US; 579418012)',
40
+ viewport: {
41
+ width: 412,
42
+ height: 915,
43
+ hasTouch: true,
44
+ isMobile: true,
45
+ },
46
+ },
25
47
  ];
26
48
  }
27
49
  export function evaluateWalletCheckoutSmokeSnapshot(snapshot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",