From 6a07e7ca57aec1afb22902cd72273da340d5c360 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Fri, 13 Feb 2026 10:38:05 -0600 Subject: [PATCH 01/14] add ACH payment method --- examples/nextjs/app/page.tsx | 4 + packages/localizations/src/deDe.ts | 2 + packages/localizations/src/enIe.ts | 2 + packages/localizations/src/enUs.ts | 2 + packages/localizations/src/esAr.ts | 2 + packages/localizations/src/esCl.ts | 2 + packages/localizations/src/esCo.ts | 2 + packages/localizations/src/esEs.ts | 2 + packages/localizations/src/esMx.ts | 2 + packages/localizations/src/esPe.ts | 2 + packages/localizations/src/esUs.ts | 2 + packages/localizations/src/frCa.ts | 2 + packages/localizations/src/frFr.ts | 2 + packages/localizations/src/idId.ts | 2 + packages/localizations/src/itIt.ts | 2 + packages/localizations/src/ptBr.ts | 2 + packages/localizations/src/qaPs.ts | 2 + packages/localizations/src/trTr.ts | 2 + packages/localizations/src/viVn.ts | 2 + packages/localizations/src/zhCn.ts | 2 + packages/localizations/src/zhSg.ts | 2 + .../payment/checkout-buttons/ach/godaddy.tsx | 45 ++++ .../checkout-buttons/credit-card/godaddy.tsx | 1 + .../checkout/payment/lazy-payment-loader.tsx | 29 +++ .../checkout/payment/payment-form.tsx | 13 +- .../payment/payment-methods/ach/godaddy.tsx | 218 ++++++++++++++++++ .../payment/utils/conditional-providers.tsx | 20 +- .../payment/utils/poynt-ach-provider.tsx | 45 ++++ .../react/src/lib/godaddy/checkout-env.ts | 48 ++++ .../src/lib/godaddy/checkout-mutations.ts | 4 + .../react/src/lib/godaddy/checkout-queries.ts | 4 + packages/react/src/types.ts | 1 + 32 files changed, 469 insertions(+), 3 deletions(-) create mode 100644 packages/react/src/components/checkout/payment/checkout-buttons/ach/godaddy.tsx create mode 100644 packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx create mode 100644 packages/react/src/components/checkout/payment/utils/poynt-ach-provider.tsx diff --git a/examples/nextjs/app/page.tsx b/examples/nextjs/app/page.tsx index 9ae69eb8..e1f27976 100644 --- a/examples/nextjs/app/page.tsx +++ b/examples/nextjs/app/page.tsx @@ -58,6 +58,10 @@ export default async function Home() { processor: 'godaddy', checkoutTypes: ['standard'], }, + ach: { + processor: 'godaddy', + checkoutTypes: ['standard'], + }, express: { processor: 'godaddy', checkoutTypes: ['express'], diff --git a/packages/localizations/src/deDe.ts b/packages/localizations/src/deDe.ts index a58bf364..94d3e052 100644 --- a/packages/localizations/src/deDe.ts +++ b/packages/localizations/src/deDe.ts @@ -106,6 +106,7 @@ export const deDe = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Offline-Zahlungen', + ach: 'Bankkonto', }, descriptions: { creditCard: '', @@ -114,6 +115,7 @@ export const deDe = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'Keine Zahlungsmethoden verfügbar', cardNumber: 'Kartennummer', diff --git a/packages/localizations/src/enIe.ts b/packages/localizations/src/enIe.ts index 5751044b..816d87d4 100644 --- a/packages/localizations/src/enIe.ts +++ b/packages/localizations/src/enIe.ts @@ -106,6 +106,7 @@ export const enIe = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Offline payments', + ach: 'Bank Account', }, descriptions: { creditCard: '', @@ -114,6 +115,7 @@ export const enIe = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'No payment methods available', cardNumber: 'Card number', diff --git a/packages/localizations/src/enUs.ts b/packages/localizations/src/enUs.ts index b9472309..2c911677 100644 --- a/packages/localizations/src/enUs.ts +++ b/packages/localizations/src/enUs.ts @@ -106,6 +106,7 @@ export const enUs = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Offline payments', + ach: 'Bank Account', }, descriptions: { creditCard: '', @@ -114,6 +115,7 @@ export const enUs = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'No payment methods available', cardNumber: 'Card number', diff --git a/packages/localizations/src/esAr.ts b/packages/localizations/src/esAr.ts index 92f66711..c8424024 100644 --- a/packages/localizations/src/esAr.ts +++ b/packages/localizations/src/esAr.ts @@ -107,6 +107,7 @@ export const esAr = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos en efectivo', + ach: 'Cuenta Bancaria', }, descriptions: { creditCard: '', @@ -115,6 +116,7 @@ export const esAr = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esCl.ts b/packages/localizations/src/esCl.ts index d1438c84..f6d32ef9 100644 --- a/packages/localizations/src/esCl.ts +++ b/packages/localizations/src/esCl.ts @@ -107,6 +107,7 @@ export const esCl = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos offline', + ach: 'Cuenta Bancaria', }, descriptions: { creditCard: '', @@ -115,6 +116,7 @@ export const esCl = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esCo.ts b/packages/localizations/src/esCo.ts index e82930d5..bf499fa9 100644 --- a/packages/localizations/src/esCo.ts +++ b/packages/localizations/src/esCo.ts @@ -107,6 +107,7 @@ export const esCo = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos sin conexión', + ach: 'Cuenta bancaria', }, descriptions: { creditCard: '', @@ -115,6 +116,7 @@ export const esCo = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esEs.ts b/packages/localizations/src/esEs.ts index 51a51eec..41a0c6ce 100644 --- a/packages/localizations/src/esEs.ts +++ b/packages/localizations/src/esEs.ts @@ -107,6 +107,7 @@ export const esEs = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos sin conexión', + ach: 'Cuenta bancaria', }, descriptions: { creditCard: '', @@ -115,6 +116,7 @@ export const esEs = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esMx.ts b/packages/localizations/src/esMx.ts index 2a34cb82..b91985a3 100644 --- a/packages/localizations/src/esMx.ts +++ b/packages/localizations/src/esMx.ts @@ -107,6 +107,7 @@ export const esMx = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos fuera de línea', + ach: 'Cuenta Bancaria', }, descriptions: { creditCard: '', @@ -115,6 +116,7 @@ export const esMx = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esPe.ts b/packages/localizations/src/esPe.ts index 9dbeca8e..95902a60 100644 --- a/packages/localizations/src/esPe.ts +++ b/packages/localizations/src/esPe.ts @@ -107,6 +107,7 @@ export const esPe = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos en efectivo', + ach: 'Cuenta Bancaria', }, descriptions: { creditCard: '', @@ -115,6 +116,7 @@ export const esPe = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esUs.ts b/packages/localizations/src/esUs.ts index ad231176..c7f11edc 100644 --- a/packages/localizations/src/esUs.ts +++ b/packages/localizations/src/esUs.ts @@ -107,6 +107,7 @@ export const esUs = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos offline', + ach: 'Cuenta Bancaria', }, descriptions: { creditCard: '', @@ -115,6 +116,7 @@ export const esUs = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/frCa.ts b/packages/localizations/src/frCa.ts index 1e136e32..4f5088bd 100644 --- a/packages/localizations/src/frCa.ts +++ b/packages/localizations/src/frCa.ts @@ -107,6 +107,7 @@ export const frCa = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Paiements hors ligne', + ach: 'Compte bancaire', }, descriptions: { creditCard: '', @@ -115,6 +116,7 @@ export const frCa = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'Aucune méthode de paiement disponible', cardNumber: 'Numéro de carte', diff --git a/packages/localizations/src/frFr.ts b/packages/localizations/src/frFr.ts index 04b2a67c..3dce2f50 100644 --- a/packages/localizations/src/frFr.ts +++ b/packages/localizations/src/frFr.ts @@ -107,6 +107,7 @@ export const frFr = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Paiements hors ligne', + ach: 'Compte bancaire', }, descriptions: { creditCard: '', @@ -115,6 +116,7 @@ export const frFr = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'Aucune méthode de paiement disponible', cardNumber: 'Numéro de carte', diff --git a/packages/localizations/src/idId.ts b/packages/localizations/src/idId.ts index a0b2b8a6..2507c51c 100644 --- a/packages/localizations/src/idId.ts +++ b/packages/localizations/src/idId.ts @@ -106,6 +106,7 @@ export const idId = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pembayaran offline', + ach: 'Rekening Bank', }, descriptions: { creditCard: '', @@ -114,6 +115,7 @@ export const idId = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'Tidak ada metode pembayaran tersedia', cardNumber: 'Nomor kartu', diff --git a/packages/localizations/src/itIt.ts b/packages/localizations/src/itIt.ts index e6d5a3d3..8157cc61 100644 --- a/packages/localizations/src/itIt.ts +++ b/packages/localizations/src/itIt.ts @@ -107,6 +107,7 @@ export const itIt = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagamenti offline', + ach: 'Conto Bancario', }, descriptions: { creditCard: '', @@ -115,6 +116,7 @@ export const itIt = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'Nessun metodo di pagamento disponibile', cardNumber: 'Numero della carta', diff --git a/packages/localizations/src/ptBr.ts b/packages/localizations/src/ptBr.ts index 40dac240..bac6845e 100644 --- a/packages/localizations/src/ptBr.ts +++ b/packages/localizations/src/ptBr.ts @@ -106,6 +106,7 @@ export const ptBr = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagamentos offline', + ach: 'Conta Bancária', }, descriptions: { creditCard: '', @@ -114,6 +115,7 @@ export const ptBr = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'Nenhum método de pagamento disponível', cardNumber: 'Número do cartão', diff --git a/packages/localizations/src/qaPs.ts b/packages/localizations/src/qaPs.ts index 2824a45a..d53858ff 100644 --- a/packages/localizations/src/qaPs.ts +++ b/packages/localizations/src/qaPs.ts @@ -107,6 +107,7 @@ export const qaPs = { googlePay: '[Göögië Þâÿ Þâÿmëñţ]', paze: '[Þâžë Þâÿmëñţ Šërvîçë]', offline: '[Öfflîñë þâÿmëñţ mëţhödš]', + ach: '[Bâñk Âççöüñţ Þâÿmëñţ]', }, descriptions: { creditCard: '', @@ -115,6 +116,7 @@ export const qaPs = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: '[Ñö þâÿmëñţ mëţhödš âvâîlâblë âţ ţhîš ţîmë]', cardNumber: '[Çârd ñümkër îñþüţ fîëld]', diff --git a/packages/localizations/src/trTr.ts b/packages/localizations/src/trTr.ts index 72afea53..1a35fef6 100644 --- a/packages/localizations/src/trTr.ts +++ b/packages/localizations/src/trTr.ts @@ -106,6 +106,7 @@ export const trTr = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Çevrimdışı ödemeler', + ach: 'Banka Hesabı', }, descriptions: { creditCard: '', @@ -114,6 +115,7 @@ export const trTr = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'Kullanılabilir ödeme yöntemi yok', cardNumber: 'Kart numarası', diff --git a/packages/localizations/src/viVn.ts b/packages/localizations/src/viVn.ts index c1460e6c..1cc19db0 100644 --- a/packages/localizations/src/viVn.ts +++ b/packages/localizations/src/viVn.ts @@ -106,6 +106,7 @@ export const viVn = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Thanh toán ngoại tuyến', + ach: 'Tài khoản ngân hàng', }, descriptions: { creditCard: '', @@ -114,6 +115,7 @@ export const viVn = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: 'Không có phương thức thanh toán nào', cardNumber: 'Số thẻ', diff --git a/packages/localizations/src/zhCn.ts b/packages/localizations/src/zhCn.ts index e71929b9..bf048ffd 100644 --- a/packages/localizations/src/zhCn.ts +++ b/packages/localizations/src/zhCn.ts @@ -102,6 +102,7 @@ export const zhCn = { googlePay: 'Google Pay', paze: 'Paze', offline: '线下付款', + ach: '银行账户', }, descriptions: { creditCard: '', @@ -110,6 +111,7 @@ export const zhCn = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: '暂无可用的付款方式', cardNumber: '卡号', diff --git a/packages/localizations/src/zhSg.ts b/packages/localizations/src/zhSg.ts index 286d58ff..049bb565 100644 --- a/packages/localizations/src/zhSg.ts +++ b/packages/localizations/src/zhSg.ts @@ -102,6 +102,7 @@ export const zhSg = { googlePay: 'Google Pay', paze: 'Paze', offline: '线下付款', + ach: '银行账户', }, descriptions: { creditCard: '', @@ -110,6 +111,7 @@ export const zhSg = { googlePay: '', paze: '', offline: '', + ach: '', }, noMethodsAvailable: '无可用付款方式', cardNumber: '卡号', diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/ach/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/ach/godaddy.tsx new file mode 100644 index 00000000..6fd8c587 --- /dev/null +++ b/packages/react/src/components/checkout/payment/checkout-buttons/ach/godaddy.tsx @@ -0,0 +1,45 @@ +import { useCallback, useRef } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { useCheckoutContext } from '@/components/checkout/checkout'; +import { usePoyntACHCollect } from '@/components/checkout/payment/utils/poynt-ach-provider'; +import { useIsPaymentDisabled } from '@/components/checkout/payment/utils/use-is-payment-disabled'; +import { Button } from '@/components/ui/button'; +import { useGoDaddyContext } from '@/godaddy-provider'; + +export function ACHCheckoutButton() { + const { collect, isLoadingNonce } = usePoyntACHCollect(); + const { isConfirmingCheckout } = useCheckoutContext(); + const isPaymentDisabled = useIsPaymentDisabled(); + const form = useFormContext(); + const buttonRef = useRef(null); + const { t } = useGoDaddyContext(); + + const handleSubmit = useCallback(async () => { + console.log('handle ACH submit'); + const valid = await form.trigger(); + if (!valid) { + const firstError = Object.keys(form.formState.errors)[0]; + if (firstError) { + form.setFocus(firstError); + } + return; + } + + collect?.getNonce({}); + }, [form, collect]); + + if (!collect) return null; + + return ( + + ); +} diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx index 72aafb79..42a19544 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx @@ -17,6 +17,7 @@ export function CreditCardCheckoutButton() { const { t } = useGoDaddyContext(); const handleSubmit = useCallback(async () => { + console.log('handle CARD submit'); const valid = await form.trigger(); if (!valid) { const firstError = Object.keys(form.formState.errors)[0]; diff --git a/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx index 00bb6c53..0b8962d9 100644 --- a/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx +++ b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx @@ -70,6 +70,23 @@ const LazyComponents = { default: module.PayPalCreditCardCheckoutButton, })) ), + // ACH Form + GoDaddyACHForm: lazy(() => + import('@/components/checkout/payment/payment-methods/ach/godaddy').then( + module => ({ + default: module.GoDaddyACHForm, + }) + ) + ), + + // ACH Buttons + ACHCheckoutButton: lazy(() => + import('@/components/checkout/payment/checkout-buttons/ach/godaddy').then( + module => ({ + default: module.ACHCheckoutButton, + }) + ) + ), // Express Buttons ExpressCheckoutButton: lazy(() => @@ -179,6 +196,12 @@ type PaymentComponentRegistry = { button: PaymentComponentKey; }; }; + [PaymentMethodType.ACH]?: { + [PaymentProvider.GODADDY]: { + form: PaymentComponentKey; + button: PaymentComponentKey; + }; + }; }; export const lazyPaymentComponentRegistry: PaymentComponentRegistry = { @@ -228,6 +251,12 @@ export const lazyPaymentComponentRegistry: PaymentComponentRegistry = { button: 'PazeCheckoutButton', }, }, + [PaymentMethodType.ACH]: { + [PaymentProvider.GODADDY]: { + form: 'GoDaddyACHForm', + button: 'ACHCheckoutButton', + }, + }, }; // Payment loading skeleton component diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index 3155d71e..5e899629 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -1,4 +1,10 @@ -import { Circle, CreditCard, LoaderCircle, Wallet } from 'lucide-react'; +import { + Circle, + CreditCard, + Landmark, + LoaderCircle, + Wallet, +} from 'lucide-react'; import React, { useCallback, useEffect, @@ -62,6 +68,7 @@ import { // UI config for payment methods (labels will be resolved from translations) const PAYMENT_METHOD_ICONS: Record = { card: , + ach: , paypal: , applePay: , googlePay: , @@ -104,6 +111,8 @@ export function PaymentForm( switch (key) { case PaymentMethodType.CREDIT_CARD: return t.payment.methods.creditCard; + case PaymentMethodType.ACH: + return t.payment.methods.ach; case PaymentMethodType.PAYPAL: return t.payment.methods.paypal; case PaymentMethodType.APPLE_PAY: @@ -127,6 +136,8 @@ export function PaymentForm( switch (key) { case PaymentMethodType.CREDIT_CARD: return t.payment.descriptions?.creditCard; + case PaymentMethodType.ACH: + return t.payment.descriptions?.ach; case PaymentMethodType.PAYPAL: return t.payment.descriptions?.paypal; case PaymentMethodType.APPLE_PAY: diff --git a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx new file mode 100644 index 00000000..d27f91cc --- /dev/null +++ b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx @@ -0,0 +1,218 @@ +import { useLayoutEffect, useRef, useState } from 'react'; +import { useCheckoutContext } from '@/components/checkout/checkout'; +import type { + TokenizeJs, + TokenizeJsEvent, +} from '@/components/checkout/payment/types'; +import { usePoyntACHCollect } from '@/components/checkout/payment/utils/poynt-ach-provider'; +import { + PaymentProvider, + useConfirmCheckout, +} from '@/components/checkout/payment/utils/use-confirm-checkout'; +import { useLoadPoyntCollect } from '@/components/checkout/payment/utils/use-load-poynt-collect'; +import { useGoDaddyContext } from '@/godaddy-provider'; +import { GraphQLErrorWithCodes } from '@/lib/graphql-with-errors'; +import { PaymentMethodType } from '@/types'; + +export function GoDaddyACHForm() { + const { t } = useGoDaddyContext(); + const { session } = useCheckoutContext(); + const { setCollect, setIsLoadingNonce } = usePoyntACHCollect(); + const { isPoyntLoaded } = useLoadPoyntCollect(); + const { godaddyPaymentsConfig, setCheckoutErrors } = useCheckoutContext(); + const [error, setError] = useState(''); + + const confirmCheckout = useConfirmCheckout(); + + const fontFamily = + '"GD Sherpa", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'; + + const baseInputStyle = ` + display: flex; + height: 48px; + width: 100%; + border-radius: 0.4375rem; + border: 1px solid oklch(0.9 0.025 245); + background: oklch(1 0 0); + padding-left: 12px; + padding-right: 12px; + padding-top: 8px; + padding-bottom: 8px; + font-size: 14px; + line-height: 1.5; + color: oklch(0.13 0 0); + font-family: ${fontFamily}; + transition: color 0.2s, background 0.2s, border-color 0.2s; + + &::placeholder { + color: oklch(0.556 0 0); + } + + &:focus-visible { + outline: none; + border-color: oklch(0.57 0.22 255); + box-shadow: 0px 0px 0px 2px oklch(0.57 0.22 255) inset; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + `; + + const options = { + iFrame: { + width: '100%', + height: '360px', + border: '0px', + }, + paymentMethods: ['ach'], + displayComponents: { + labels: true, + }, + customCss: { + container: ` + --collect-radio-size: 16px; + --collect-radio-dot-size: 7px; + --collect-radio-checked-bg-color: oklch(0.8 0.17 185); + --collect-radio-border-color: oklch(0.9 0.025 245); + --collect-radio-dot-color: oklch(0.13 0 0); + --collect-radio-bg-color: oklch(1 0 0); + --collect-border-radius-round: 50%; + --collect-spacing-lg: 10px; + --collect-spacing-md: 8px; + font-family: ${fontFamily}; + `, + + rowAccountHolderName: ` + flex: 1 1 100%; + padding: 0; + margin: 0; + margin-bottom: 16px; + `, + rowAccountHolderType: ` + flex: 1 1 100%; + padding: 0; + margin: 0; + margin-bottom: 16px; + `, + rowRoutingNumber: ` + flex: 1 1 100%; + padding: 0; + margin: 0; + `, + rowBankAccountNumber: ` + flex: 1 1 100%; + padding: 0; + margin: 0; + padding-top: 16px; + `, + + radio: { + accountHolderType: { + label: ` + font-size: 14px; + font-weight: 500; + line-height: 1.5; + color: oklch(0.13 0 0); + cursor: pointer;`, + container: ` + display: inline-flex; + margin-right: 24px; + margin-bottom: 0px; + + .poynt-collect-payment-radio-input:checked + .poynt-collect-payment-radio-label::after { + left: 5.5px; + top: 9px; + } + `, + }, + }, + + input: { + ownerName: baseInputStyle, + routingNumber: baseInputStyle, + accountNumber: baseInputStyle, + }, + }, + }; + + const collect = useRef(null); + + useLayoutEffect(() => { + if ( + !isPoyntLoaded || + !godaddyPaymentsConfig || + collect.current || + (!godaddyPaymentsConfig?.businessId && !session?.businessId) + ) + return; + + collect.current = new (window as any).TokenizeJs({ + businessId: godaddyPaymentsConfig?.businessId || session?.businessId, + storeId: session?.storeId, + channelId: session?.channelId, + applicationId: godaddyPaymentsConfig?.appId, + }); + + collect?.current?.on('ready', () => { + setCollect(collect.current); + }); + + collect?.current?.mount('gdpay-ach-element', document, options); + + collect?.current?.on('nonce', async (event: TokenizeJsEvent) => { + const nonce = event?.data?.nonce; + + if (nonce) { + try { + await confirmCheckout.mutateAsync({ + paymentToken: nonce, + paymentType: PaymentMethodType.ACH, + paymentProvider: PaymentProvider.POYNT, + }); + setIsLoadingNonce(false); + setError(''); + } catch (err: unknown) { + if (err instanceof GraphQLErrorWithCodes) { + setCheckoutErrors(err.codes); + } + } + } else { + setCheckoutErrors(['TRANSACTION_PROCESSING_FAILED']); + setIsLoadingNonce(false); + } + }); + + collect?.current?.on('error', (event: TokenizeJsEvent) => { + setError(event?.data?.error?.message || t.errors.errorProcessingPayment); + setIsLoadingNonce(false); + }); + + collect?.current?.on('validated', event => { + if (event?.data?.validated) { + setError(''); + } + }); + }, [ + isPoyntLoaded, + godaddyPaymentsConfig, + confirmCheckout.mutateAsync, + setCollect, + setCheckoutErrors, + t, + setIsLoadingNonce, + session?.businessId, + session?.storeId, + session?.channelId, + ]); + + return ( + <> +
+ {error ? ( +

{error}

+ ) : null} + + ); +} diff --git a/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx b/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx index 95df6409..24acdab2 100644 --- a/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx +++ b/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx @@ -1,6 +1,7 @@ import { PayPalScriptProvider } from '@paypal/react-paypal-js'; import { useCheckoutContext } from '@/components/checkout/checkout'; import { PayPalProvider } from './paypal-provider'; +import { PoyntACHCollectProvider } from './poynt-ach-provider'; import { PoyntCollectProvider } from './poynt-provider'; import { SquareProvider } from './square-provider'; import { StripeProvider } from './stripe-provider'; @@ -17,8 +18,13 @@ interface ConditionalPaymentProvidersProps { export function ConditionalPaymentProviders({ children, }: ConditionalPaymentProvidersProps) { - const { stripeConfig, godaddyPaymentsConfig, squareConfig, paypalConfig } = - useCheckoutContext(); + const { + stripeConfig, + godaddyPaymentsConfig, + squareConfig, + paypalConfig, + session, + } = useCheckoutContext(); const { payPalRequest } = useBuildPaymentRequest(); // Start with the children and conditionally wrap with providers @@ -29,6 +35,16 @@ export function ConditionalPaymentProviders({ wrappedChildren = {wrappedChildren}; } + // Only wrap with PoyntACHCollectProvider if GoDaddy ACH is configured + if ( + godaddyPaymentsConfig?.appId?.trim() && + session?.paymentMethods?.ach?.processor === 'godaddy' + ) { + wrappedChildren = ( + {wrappedChildren} + ); + } + // Only wrap with PoyntCollectProvider (GoDaddy Payments) if configured if (godaddyPaymentsConfig?.appId?.trim()) { wrappedChildren = ( diff --git a/packages/react/src/components/checkout/payment/utils/poynt-ach-provider.tsx b/packages/react/src/components/checkout/payment/utils/poynt-ach-provider.tsx new file mode 100644 index 00000000..56b514c8 --- /dev/null +++ b/packages/react/src/components/checkout/payment/utils/poynt-ach-provider.tsx @@ -0,0 +1,45 @@ +import React, { + createContext, + type ReactNode, + useContext, + useState, +} from 'react'; +import type { TokenizeJs } from '@/components/checkout/payment/types'; + +type PoyntACHCollectContextType = { + collect: TokenizeJs | null; + setCollect: (collect: TokenizeJs | null) => void; + isLoadingNonce: boolean; + setIsLoadingNonce: (loading: boolean) => void; +}; + +const PoyntACHCollectContext = createContext< + PoyntACHCollectContextType | undefined +>(undefined); + +export const PoyntACHCollectProvider = ({ + children, +}: { + children: ReactNode; +}) => { + const [collect, setCollect] = useState(null); + const [isLoadingNonce, setIsLoadingNonce] = useState(false); + + return ( + + {children} + + ); +}; + +export const usePoyntACHCollect = () => { + const context = useContext(PoyntACHCollectContext); + if (!context) { + throw new Error( + 'usePoyntACHCollect must be used within a PoyntACHCollectProvider' + ); + } + return context; +}; diff --git a/packages/react/src/lib/godaddy/checkout-env.ts b/packages/react/src/lib/godaddy/checkout-env.ts index 946b67bc..641911a1 100644 --- a/packages/react/src/lib/godaddy/checkout-env.ts +++ b/packages/react/src/lib/godaddy/checkout-env.ts @@ -2984,6 +2984,15 @@ const introspection = { kind: 'OBJECT', name: 'CheckoutSessionPaymentMethods', fields: [ + { + name: 'ach', + type: { + kind: 'OBJECT', + name: 'CheckoutSessionPaymentMethodConfig', + }, + args: [], + isDeprecated: false, + }, { name: 'applePay', type: { @@ -3002,6 +3011,15 @@ const introspection = { args: [], isDeprecated: false, }, + { + name: 'ccavenue', + type: { + kind: 'OBJECT', + name: 'CheckoutSessionPaymentMethodConfig', + }, + args: [], + isDeprecated: false, + }, { name: 'express', type: { @@ -3020,6 +3038,15 @@ const introspection = { args: [], isDeprecated: false, }, + { + name: 'mercadopago', + type: { + kind: 'OBJECT', + name: 'CheckoutSessionPaymentMethodConfig', + }, + args: [], + isDeprecated: false, + }, { name: 'offline', type: { @@ -3054,6 +3081,13 @@ const introspection = { kind: 'INPUT_OBJECT', name: 'CheckoutSessionPaymentMethodsInput', inputFields: [ + { + name: 'ach', + type: { + kind: 'INPUT_OBJECT', + name: 'CheckoutSessionPaymentMethodConfigInput', + }, + }, { name: 'applePay', type: { @@ -3068,6 +3102,13 @@ const introspection = { name: 'CheckoutSessionPaymentMethodConfigInput', }, }, + { + name: 'ccavenue', + type: { + kind: 'INPUT_OBJECT', + name: 'CheckoutSessionPaymentMethodConfigInput', + }, + }, { name: 'express', type: { @@ -3082,6 +3123,13 @@ const introspection = { name: 'CheckoutSessionPaymentMethodConfigInput', }, }, + { + name: 'mercadopago', + type: { + kind: 'INPUT_OBJECT', + name: 'CheckoutSessionPaymentMethodConfigInput', + }, + }, { name: 'offline', type: { diff --git a/packages/react/src/lib/godaddy/checkout-mutations.ts b/packages/react/src/lib/godaddy/checkout-mutations.ts index c4e3815e..08d67fee 100644 --- a/packages/react/src/lib/godaddy/checkout-mutations.ts +++ b/packages/react/src/lib/godaddy/checkout-mutations.ts @@ -105,6 +105,10 @@ export const CreateCheckoutSessionMutation = graphql(` processor checkoutTypes } + ach { + processor + checkoutTypes + } } draftOrder { id diff --git a/packages/react/src/lib/godaddy/checkout-queries.ts b/packages/react/src/lib/godaddy/checkout-queries.ts index 38491fb7..9dc7a8b0 100644 --- a/packages/react/src/lib/godaddy/checkout-queries.ts +++ b/packages/react/src/lib/godaddy/checkout-queries.ts @@ -105,6 +105,10 @@ export const GetCheckoutSessionQuery = graphql(` processor checkoutTypes } + ach { + processor + checkoutTypes + } } locations { id diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 920c95b8..18033d06 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -56,6 +56,7 @@ export type AvailablePaymentProviders = export const PaymentMethodType = { CREDIT_CARD: 'card', + ACH: 'ach', EXPRESS: 'express', PAYPAL: 'paypal', APPLE_PAY: 'applePay', From 371c9c4ca9276a0dca7a8bae3dcb6862e25afee7 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Fri, 13 Feb 2026 11:37:53 -0600 Subject: [PATCH 02/14] dynamic IDs for collect components --- .../checkout/payment/checkout-buttons/ach/godaddy.tsx | 1 - .../payment/checkout-buttons/credit-card/godaddy.tsx | 1 - .../checkout/payment/checkout-buttons/express/godaddy.tsx | 8 +++++--- .../checkout/payment/checkout-buttons/paze/godaddy.tsx | 7 ++++--- .../checkout/payment/payment-methods/ach/godaddy.tsx | 8 +++++--- .../payment/payment-methods/credit-card/godaddy.tsx | 8 +++++--- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/ach/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/ach/godaddy.tsx index 6fd8c587..f70b7cf9 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/ach/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/ach/godaddy.tsx @@ -15,7 +15,6 @@ export function ACHCheckoutButton() { const { t } = useGoDaddyContext(); const handleSubmit = useCallback(async () => { - console.log('handle ACH submit'); const valid = await form.trigger(); if (!valid) { const firstError = Object.keys(form.formState.errors)[0]; diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx index 42a19544..72aafb79 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx @@ -17,7 +17,6 @@ export function CreditCardCheckoutButton() { const { t } = useGoDaddyContext(); const handleSubmit = useCallback(async () => { - console.log('handle CARD submit'); const valid = await form.trigger(); if (!valid) { const firstError = Object.keys(form.formState.errors)[0]; diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx index a574ee64..cb79f40e 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, + useId, useLayoutEffect, useMemo, useRef, @@ -58,6 +59,7 @@ export function ExpressCheckoutButton() { const { godaddyPaymentsConfig } = useCheckoutContext(); const { t } = useGoDaddyContext(); const [isCollectLoading, setIsCollectLoading] = useState(true); + const elementId = `gdpay-express-pay-element-${useId()}`; const [walletSource, setWalletSource] = useState( undefined ); @@ -500,7 +502,7 @@ export function ExpressCheckoutButton() { if (paymentMethods.length > 0 && !hasMounted.current) { hasMounted.current = true; // console.log("[poynt collect] Mounting"); - collect?.current?.mount('gdpay-express-pay-element', document, { + collect?.current?.mount(elementId, document, { paymentMethods: paymentMethods, buttonsContainerOptions: { className: 'gap-1 !flex-col sm:!flex-row place-items-center', @@ -1446,7 +1448,7 @@ export function ExpressCheckoutButton() { // return function unmount() { // if (collect.current) { // console.log("poynt collect unmounting"); - // collect.current.unmount("gdpay-express-pay-element", document); + // collect.current.unmount(elementId, document); // } // }; }, [ @@ -1475,7 +1477,7 @@ export function ExpressCheckoutButton() { return ( <> -
+
{isCollectLoading ? (
diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx index 850a3e76..91e3a917 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useId, useRef, useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { useCheckoutContext } from '@/components/checkout/checkout'; import { useDraftOrderTotals } from '@/components/checkout/order/use-draft-order'; @@ -27,6 +27,7 @@ export function PazeCheckoutButton() { const { t } = useGoDaddyContext(); const [isCollectLoading, setIsCollectLoading] = useState(true); const [error, setError] = useState(''); + const elementId = `paze-pay-element-${useId()}`; const { data: totals } = useDraftOrderTotals(); const { poyntStandardRequest } = useBuildPaymentRequest(); @@ -67,7 +68,7 @@ export function PazeCheckoutButton() { if (!hasMounted.current && collect?.current) { hasMounted.current = true; // console.log("[poynt collect] Mounting paze-pay-element"); - collect?.current.mount('paze-pay-element', document, { + collect?.current.mount(elementId, document, { paymentMethods: ['paze'], buttonsContainerOptions: { className: 'gap-1 !flex-col sm:!flex-row place-items-center', @@ -244,7 +245,7 @@ export function PazeCheckoutButton() { return ( <> -
+
{isCollectLoading ? (
diff --git a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx index d27f91cc..bf8c0472 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef, useState } from 'react'; +import { useId, useLayoutEffect, useRef, useState } from 'react'; import { useCheckoutContext } from '@/components/checkout/checkout'; import type { TokenizeJs, @@ -24,6 +24,8 @@ export function GoDaddyACHForm() { const confirmCheckout = useConfirmCheckout(); + const elementId = `gdpay-ach-element-${useId()}`; + const fontFamily = '"GD Sherpa", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'; @@ -159,7 +161,7 @@ export function GoDaddyACHForm() { setCollect(collect.current); }); - collect?.current?.mount('gdpay-ach-element', document, options); + collect?.current?.mount(elementId, document, options); collect?.current?.on('nonce', async (event: TokenizeJsEvent) => { const nonce = event?.data?.nonce; @@ -209,7 +211,7 @@ export function GoDaddyACHForm() { return ( <> -
+
{error ? (

{error}

) : null} diff --git a/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx b/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx index e269af99..49323175 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef, useState } from 'react'; +import { useId, useLayoutEffect, useRef, useState } from 'react'; import { useCheckoutContext } from '@/components/checkout/checkout'; import type { TokenizeJs, @@ -24,6 +24,8 @@ export function GoDaddyCreditCardForm() { const confirmCheckout = useConfirmCheckout(); + const elementId = `gdpay-card-element-${useId()}`; + const options = { iFrame: { width: '100%', @@ -206,7 +208,7 @@ export function GoDaddyCreditCardForm() { setCollect(collect.current); }); - collect?.current?.mount('gdpay-card-element', document, options); + collect?.current?.mount(elementId, document, options); collect?.current?.on('nonce', async (event: TokenizeJsEvent) => { const nonce = event?.data?.nonce; @@ -256,7 +258,7 @@ export function GoDaddyCreditCardForm() { return ( <> -
+
{error ? (

{error}

) : null} From 32096a218b2898399bc8ae8737894359c96d0118 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Tue, 17 Feb 2026 08:39:04 -0600 Subject: [PATCH 03/14] include tips in totals --- .../components/checkout/tips/tips-form.tsx | 266 ++++++++++++++---- .../src/components/checkout/totals/totals.tsx | 8 +- .../checkout/utils/format-currency.ts | 6 +- 3 files changed, 230 insertions(+), 50 deletions(-) diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index 4d015870..30c97179 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -1,7 +1,12 @@ -import { useState } from 'react'; +import { useDebouncedValue } from '@tanstack/react-pacer'; +import { useEffect, useRef, useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { useCheckoutContext } from '@/components/checkout/checkout'; -import { useFormatCurrency } from '@/components/checkout/utils/format-currency'; +import { + convertMajorToMinorUnits, + currencyConfigs, + useFormatCurrency, +} from '@/components/checkout/utils/format-currency'; import { Button } from '@/components/ui/button'; import { FormControl, @@ -23,7 +28,6 @@ interface TipsFormProps { export function TipsForm({ total, currencyCode }: TipsFormProps) { const { t } = useGoDaddyContext(); - const { requiredFields } = useCheckoutContext(); const form = useFormContext(); const formatCurrency = useFormatCurrency(); const [showCustomTip, setShowCustomTip] = useState(false); @@ -91,7 +95,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { return (
@@ -122,7 +126,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) {
@@ -150,49 +154,215 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) {
{showCustomTip && ( - ( - - - {t.tips.customTipAmount} - - + + )} +
+ ); +} + +/** + * Isolated component for the custom tip input. + * + * Uses the "format on blur" pattern — the industry standard for currency inputs + * (Stripe, Square, Shopify, etc.): + * + * - While focused: the user edits raw text freely (local state). + * Only non-numeric characters are stripped; intermediate states like + * "10.", "10.5", "" are all preserved so delete/backspace work naturally. + * - On blur: the raw text is parsed, converted to minor units, synced to + * form state, and the display is reformatted (e.g. "10.5" → "10.50"). + * - On focus: if a formatted value exists it is shown as an editable raw + * number so the user can continue editing from where they left off. + */ +interface CustomTipInputProps { + currencyCode?: string; + total: number; + formatCurrency: (options: { + amount: number; + currencyCode: string; + inputInMinorUnits?: boolean; + returnRaw?: boolean; + }) => string; +} + +/** + * Currencies where the symbol is conventionally placed after the number. + * Derived from currencyConfigs entries with `pattern: '#!'`. + */ +const SUFFIX_CURRENCIES = new Set( + Object.entries(currencyConfigs) + .filter(([, cfg]) => cfg.pattern === '#!') + .map(([code]) => code) +); + +/** + * Map symbol character length to Tailwind padding classes. + * Arabic / multi-char symbols need more room than a single `$`. + */ +function symbolPadding(symbol: string, position: 'prefix' | 'suffix') { + const len = symbol.length; + if (position === 'prefix') { + if (len <= 1) return 'pl-7'; // $, €, ¥, ₩, etc. + if (len <= 2) return 'pl-10'; // R$, Rp, S/ + if (len <= 3) return 'pl-12'; // NT$, د.إ, د.ك + return 'pl-14'; // .د.ب, ر.ع. + } + // suffix + if (len <= 1) return 'pr-7'; + if (len <= 2) return 'pr-10'; + if (len <= 3) return 'pr-12'; + return 'pr-14'; +} + +function CustomTipInput({ + currencyCode, + total, + formatCurrency, +}: CustomTipInputProps) { + const { t } = useGoDaddyContext(); + const { requiredFields } = useCheckoutContext(); + const form = useFormContext(); + + const code = currencyCode || 'USD'; + const config = currencyConfigs[code] || { symbol: '$', precision: 2 }; + const { symbol, precision } = config; + const isSuffix = SUFFIX_CURRENCIES.has(code); + + // Local state holds the raw text the user is actively typing. + // `null` means "not focused — derive display from form state". + const [localValue, setLocalValue] = useState(null); + const isFocused = useRef(false); + + // Debounce the local value so the form syncs after 3s of inactivity, + // even if the user hasn't blurred the input yet. This keeps the order + // summary / totals up-to-date while the input stays focused. + const [debouncedLocal] = useDebouncedValue(localValue, { wait: 1500 }); + + /** + * Sanitize input: allow only digits and (for currencies with decimals) + * a single decimal point with at most `precision` fractional digits. + */ + const sanitize = (raw: string): string => { + // Strip everything except digits and '.' + let cleaned = raw.replace(/[^\d.]/g, ''); + + // For zero-precision currencies (JPY, KRW, etc.), strip any decimal + if (precision === 0) { + return cleaned.replace(/\./g, ''); + } + + // Allow only one decimal point + const dotIndex = cleaned.indexOf('.'); + if (dotIndex !== -1) { + const before = cleaned.slice(0, dotIndex); + const after = cleaned.slice(dotIndex + 1).replace(/\./g, ''); + // Limit fractional digits to currency precision + cleaned = `${before}.${after.slice(0, precision)}`; + } + + return cleaned; + }; + + /** + * Format a minor-units value as a raw numeric string for display + * (e.g. 1050 → "10.50" for USD). + */ + const formatRaw = (minorUnits: number): string => { + if (minorUnits <= 0) return ''; + return formatCurrency({ + amount: minorUnits, + currencyCode: code, + inputInMinorUnits: true, + returnRaw: true, + }); + }; + + // When the debounced value settles and the input is still focused, + // sync to form state and format the display — the same effect as blur + // but triggered by 3s of inactivity. This keeps the order summary + // up-to-date and gives the user visual confirmation of their amount. + useEffect(() => { + if (!isFocused.current || debouncedLocal === null) return; + const tipAmount = convertMajorToMinorUnits(debouncedLocal ?? '', code); + form.setValue('tipAmount', tipAmount); + // Clear local state so the display derives from the formatted form + // value (e.g. "10.5" → "10.50"), same as the blur handler. + setLocalValue(null); + }, [debouncedLocal]); // eslint-disable-line react-hooks/exhaustive-deps + + const symbolEl = ( + + ); + + return ( + { + // While focused, show local text. Otherwise, derive from form state. + const displayValue = + localValue !== null ? localValue : formatRaw(field.value); + + return ( + + {t.tips.customTipAmount} + +
+ {symbolEl} 0 - ? formatCurrency({ - amount: field.value, - currencyCode: currencyCode || 'USD', - inputInMinorUnits: true, - returnRaw: true, - }) - : '' + placeholder={ + precision > 0 ? `0.${'0'.repeat(precision)}` : '0' } + className={cn( + 'h-12', + isSuffix + ? symbolPadding(symbol, 'suffix') + : symbolPadding(symbol, 'prefix') + )} + value={displayValue} + onFocus={() => { + isFocused.current = true; + // Seed local state with the current formatted value so + // the user can continue editing naturally. + setLocalValue(formatRaw(field.value)); + }} onChange={e => { - // User inputs in major units (e.g., $10.50), convert to minor units for storage - const inputValue = Number.parseFloat(e.target.value); - if (!Number.isNaN(inputValue)) { - const tipAmount = Math.round(inputValue * 100); - field.onChange(tipAmount); - } else { - field.onChange(0); - } + // Only sanitize (strip invalid chars) — do NOT parse or + // round-trip through minor units. This preserves intermediate + // states like "10.", "10.5", "" so editing feels natural. + setLocalValue(sanitize(e.target.value)); }} onBlur={e => { - // User inputs in major units (e.g., $10.50), convert to minor units for storage - const inputValue = Number.parseFloat(e.target.value); - const tipAmount = !Number.isNaN(inputValue) - ? Math.round(inputValue * 100) - : 0; + isFocused.current = false; + + // Parse the raw text and sync to form state + const tipAmount = convertMajorToMinorUnits( + e.target.value, + code + ); + + field.onChange(tipAmount); + + // Clear local state so display derives from formatted form value + setLocalValue(null); + // Track custom tip amount entry track({ eventId: eventIds.enterCustomTip, @@ -208,12 +378,12 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { }); }} /> - - - - )} - /> - )} - +
+
+ +
+ ); + }} + /> ); } diff --git a/packages/react/src/components/checkout/totals/totals.tsx b/packages/react/src/components/checkout/totals/totals.tsx index acb0ee32..3975e0da 100644 --- a/packages/react/src/components/checkout/totals/totals.tsx +++ b/packages/react/src/components/checkout/totals/totals.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { DiscountStandalone } from '@/components/checkout/discount/discount-standalone'; import { TotalLineItemSkeleton } from '@/components/checkout/totals/totals-skeleton'; import { useFormatCurrency } from '@/components/checkout/utils/format-currency'; @@ -79,6 +80,11 @@ export function DraftOrderTotals({ // Discount changes are handled by the DiscountStandalone component }; + // Calculates the total plus tips and surcharge + const calculatedTotal = useMemo(() => { + return total + tip; + }, [total, tip]); + return (
@@ -158,7 +164,7 @@ export function DraftOrderTotals({ {formatCurrency({ - amount: total, + amount: calculatedTotal, currencyCode, inputInMinorUnits, })} diff --git a/packages/react/src/components/checkout/utils/format-currency.ts b/packages/react/src/components/checkout/utils/format-currency.ts index e72ab591..46f95cae 100644 --- a/packages/react/src/components/checkout/utils/format-currency.ts +++ b/packages/react/src/components/checkout/utils/format-currency.ts @@ -114,9 +114,12 @@ export function formatCurrency({ * - 0 decimals (JPY, KRW, VND, etc.): multiply by 1 * - 3 decimals (KWD, BHD, JOD, OMR): multiply by 1000 * + * Returns 0 for NaN, negative, null-ish, or otherwise unparseable input + * so callers don't need to guard individually. + * * @param amount - The amount in major units (e.g., "10.50" or 10.50) * @param currencyCode - ISO 4217 currency code (e.g., 'USD', 'JPY', 'KWD') - * @returns The amount in minor units (e.g., 1050 for USD, 10 for JPY, 10500 for KWD) + * @returns The amount in minor units (e.g., 1050 for USD, 10 for JPY, 10500 for KWD), or 0 for invalid input */ export function convertMajorToMinorUnits( amount: number | string, @@ -124,6 +127,7 @@ export function convertMajorToMinorUnits( ): number { const config = currencyConfigs[currencyCode] || { precision: 2 }; const numAmount = typeof amount === 'string' ? Number(amount) : amount; + if (!Number.isFinite(numAmount) || numAmount < 0) return 0; return Math.round(numAmount * Math.pow(10, config.precision)); } From 2a8f73cddad536756356a434895689dd2cb5127f Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Thu, 19 Feb 2026 14:07:23 -0600 Subject: [PATCH 04/14] add new authorize failed translation --- packages/localizations/src/deDe.ts | 1 + packages/localizations/src/enIe.ts | 1 + packages/localizations/src/enUs.ts | 1 + packages/localizations/src/esAr.ts | 1 + packages/localizations/src/esCl.ts | 1 + packages/localizations/src/esCo.ts | 1 + packages/localizations/src/esEs.ts | 1 + packages/localizations/src/esMx.ts | 1 + packages/localizations/src/esPe.ts | 1 + packages/localizations/src/esUs.ts | 1 + packages/localizations/src/frCa.ts | 1 + packages/localizations/src/frFr.ts | 1 + packages/localizations/src/idId.ts | 1 + packages/localizations/src/itIt.ts | 1 + packages/localizations/src/ptBr.ts | 1 + packages/localizations/src/qaPs.ts | 1 + packages/localizations/src/trTr.ts | 1 + packages/localizations/src/viVn.ts | 1 + packages/localizations/src/zhCn.ts | 1 + packages/localizations/src/zhSg.ts | 1 + 20 files changed, 20 insertions(+) diff --git a/packages/localizations/src/deDe.ts b/packages/localizations/src/deDe.ts index 94d3e052..5c6ccecb 100644 --- a/packages/localizations/src/deDe.ts +++ b/packages/localizations/src/deDe.ts @@ -356,6 +356,7 @@ export const deDe = { 'Lieferadresse oder -methode konnte nicht angewendet werden', DEPENDENCY_ERROR: 'Wir können Ihre Bestellung derzeit nicht bearbeiten. Bitte warten Sie einen Moment und versuchen Sie es erneut', + AUTHORIZATION_FAILED: 'Zahlungsautorisierung fehlgeschlagen', }, storefront: { product: 'Produkt', diff --git a/packages/localizations/src/enIe.ts b/packages/localizations/src/enIe.ts index 816d87d4..01409f94 100644 --- a/packages/localizations/src/enIe.ts +++ b/packages/localizations/src/enIe.ts @@ -332,6 +332,7 @@ export const enIe = { MISSING_SHIPPING_INFO: 'Shipping address or method failed to apply', DEPENDENCY_ERROR: "We're unable to process your order right now. Please wait a moment and try again", + AUTHORIZATION_FAILED: 'Failed to authorise payment', }, storefront: { product: 'Product', diff --git a/packages/localizations/src/enUs.ts b/packages/localizations/src/enUs.ts index 2c911677..49881872 100644 --- a/packages/localizations/src/enUs.ts +++ b/packages/localizations/src/enUs.ts @@ -332,6 +332,7 @@ export const enUs = { MISSING_SHIPPING_INFO: 'Shipping address or method failed to apply', DEPENDENCY_ERROR: "We're unable to process your order right now. Please wait a moment and try again", + AUTHORIZATION_FAILED: 'Failed to authorize payment', }, storefront: { product: 'Product', diff --git a/packages/localizations/src/esAr.ts b/packages/localizations/src/esAr.ts index c8424024..f37ac3c5 100644 --- a/packages/localizations/src/esAr.ts +++ b/packages/localizations/src/esAr.ts @@ -339,6 +339,7 @@ export const esAr = { MISSING_SHIPPING_INFO: 'No se pudo aplicar la dirección o método de envío', DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', + AUTHORIZATION_FAILED: 'Error al autorizar el pago', }, storefront: { product: 'Producto', diff --git a/packages/localizations/src/esCl.ts b/packages/localizations/src/esCl.ts index f6d32ef9..319e8394 100644 --- a/packages/localizations/src/esCl.ts +++ b/packages/localizations/src/esCl.ts @@ -341,6 +341,7 @@ export const esCl = { MISSING_SHIPPING_INFO: 'No se pudo aplicar la dirección o método de envío', DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', + AUTHORIZATION_FAILED: 'Error al autorizar el pago', }, storefront: { product: 'Producto', diff --git a/packages/localizations/src/esCo.ts b/packages/localizations/src/esCo.ts index bf499fa9..21073ae8 100644 --- a/packages/localizations/src/esCo.ts +++ b/packages/localizations/src/esCo.ts @@ -339,6 +339,7 @@ export const esCo = { MISSING_SHIPPING_INFO: 'No se pudo aplicar la dirección o método de envío', DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', + AUTHORIZATION_FAILED: 'Error al autorizar el pago', }, storefront: { product: 'Producto', diff --git a/packages/localizations/src/esEs.ts b/packages/localizations/src/esEs.ts index 41a0c6ce..3ae73715 100644 --- a/packages/localizations/src/esEs.ts +++ b/packages/localizations/src/esEs.ts @@ -344,6 +344,7 @@ export const esEs = { MISSING_SHIPPING_INFO: 'No se pudo aplicar la dirección o método de envío', DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', + AUTHORIZATION_FAILED: 'Error al autorizar el pago', }, storefront: { product: 'Producto', diff --git a/packages/localizations/src/esMx.ts b/packages/localizations/src/esMx.ts index b91985a3..e38036d7 100644 --- a/packages/localizations/src/esMx.ts +++ b/packages/localizations/src/esMx.ts @@ -340,6 +340,7 @@ export const esMx = { MISSING_SHIPPING_INFO: 'No se pudo aplicar la dirección o método de envío', DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', + AUTHORIZATION_FAILED: 'Error al autorizar el pago', }, storefront: { product: 'Producto', diff --git a/packages/localizations/src/esPe.ts b/packages/localizations/src/esPe.ts index 95902a60..9578044b 100644 --- a/packages/localizations/src/esPe.ts +++ b/packages/localizations/src/esPe.ts @@ -339,6 +339,7 @@ export const esPe = { MISSING_SHIPPING_INFO: 'No se pudo aplicar la dirección o método de envío', DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', + AUTHORIZATION_FAILED: 'Error al autorizar el pago', }, storefront: { product: 'Producto', diff --git a/packages/localizations/src/esUs.ts b/packages/localizations/src/esUs.ts index c7f11edc..389eb902 100644 --- a/packages/localizations/src/esUs.ts +++ b/packages/localizations/src/esUs.ts @@ -339,6 +339,7 @@ export const esUs = { MISSING_SHIPPING_INFO: 'No se pudo aplicar la dirección o método de envío', DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', + AUTHORIZATION_FAILED: 'Error al autorizar el pago', }, storefront: { product: 'Producto', diff --git a/packages/localizations/src/frCa.ts b/packages/localizations/src/frCa.ts index 4f5088bd..b5fab743 100644 --- a/packages/localizations/src/frCa.ts +++ b/packages/localizations/src/frCa.ts @@ -356,6 +356,7 @@ export const frCa = { "L'adresse ou la méthode de livraison n'a pas pu être appliquée", DEPENDENCY_ERROR: 'Nous ne pouvons pas traiter votre commande actuellement. Veuillez patienter un moment et réessayer', + AUTHORIZATION_FAILED: "Échec de l'autorisation du paiement", }, storefront: { product: 'Produit', diff --git a/packages/localizations/src/frFr.ts b/packages/localizations/src/frFr.ts index 3dce2f50..dc20a533 100644 --- a/packages/localizations/src/frFr.ts +++ b/packages/localizations/src/frFr.ts @@ -357,6 +357,7 @@ export const frFr = { "L'adresse ou la méthode de livraison n'a pas pu être appliquée", DEPENDENCY_ERROR: 'Nous ne pouvons pas traiter votre commande actuellement. Veuillez patienter un moment et réessayer', + AUTHORIZATION_FAILED: "Échec de l'autorisation du paiement", }, storefront: { product: 'Produit', diff --git a/packages/localizations/src/idId.ts b/packages/localizations/src/idId.ts index 2507c51c..839c30d6 100644 --- a/packages/localizations/src/idId.ts +++ b/packages/localizations/src/idId.ts @@ -331,6 +331,7 @@ export const idId = { MISSING_SHIPPING_INFO: 'Alamat atau metode pengiriman gagal diterapkan', DEPENDENCY_ERROR: 'Kami tidak dapat memproses pesanan Anda saat ini. Silakan tunggu sebentar dan coba lagi', + AUTHORIZATION_FAILED: 'Gagal mengotorisasi pembayaran', }, storefront: { product: 'Produk', diff --git a/packages/localizations/src/itIt.ts b/packages/localizations/src/itIt.ts index 8157cc61..60ceba8b 100644 --- a/packages/localizations/src/itIt.ts +++ b/packages/localizations/src/itIt.ts @@ -355,6 +355,7 @@ export const itIt = { "Impossibile applicare l'indirizzo o il metodo di spedizione", DEPENDENCY_ERROR: 'Non riusciamo a elaborare il tuo ordine in questo momento. Aspetta un momento e riprova', + AUTHORIZATION_FAILED: "Errore nell'autorizzazione del pagamento", }, storefront: { product: 'Prodotto', diff --git a/packages/localizations/src/ptBr.ts b/packages/localizations/src/ptBr.ts index bac6845e..4f64c7ba 100644 --- a/packages/localizations/src/ptBr.ts +++ b/packages/localizations/src/ptBr.ts @@ -337,6 +337,7 @@ export const ptBr = { MISSING_SHIPPING_INFO: 'Falha ao aplicar endereço ou método de entrega', DEPENDENCY_ERROR: 'Não conseguimos processar seu pedido no momento. Aguarde um momento e tente novamente', + AUTHORIZATION_FAILED: 'Falha ao autorizar pagamento', }, storefront: { product: 'Produto', diff --git a/packages/localizations/src/qaPs.ts b/packages/localizations/src/qaPs.ts index d53858ff..252a5ae6 100644 --- a/packages/localizations/src/qaPs.ts +++ b/packages/localizations/src/qaPs.ts @@ -341,6 +341,7 @@ export const qaPs = { MISSING_SHIPPING_INFO: '[Šĥîţţîñg âddrëšš ör mëţĥöd fâîlëd ţö âţţļÿ]', DEPENDENCY_ERROR: 'موږ اوس ستاسو امر پروسس نشو کولی. مهرباني وکړئ یو شېبه انتظار وکړئ او بیا هڅه وکړئ', + AUTHORIZATION_FAILED: '[Fâîlëd ţö âüţhörîžë þâÿmëñţ]', }, storefront: { product: '[Product]', diff --git a/packages/localizations/src/trTr.ts b/packages/localizations/src/trTr.ts index 1a35fef6..1efe0b14 100644 --- a/packages/localizations/src/trTr.ts +++ b/packages/localizations/src/trTr.ts @@ -332,6 +332,7 @@ export const trTr = { MISSING_SHIPPING_INFO: 'Kargo adresi veya yöntemi uygulanamadı', DEPENDENCY_ERROR: 'Şu anda siparişinizi işleme alamıyoruz. Lütfen bir dakika bekleyin ve tekrar deneyin', + AUTHORIZATION_FAILED: 'Ödeme yetkilendirmesi başarısız', }, storefront: { product: 'Ürün', diff --git a/packages/localizations/src/viVn.ts b/packages/localizations/src/viVn.ts index 1cc19db0..052e4765 100644 --- a/packages/localizations/src/viVn.ts +++ b/packages/localizations/src/viVn.ts @@ -332,6 +332,7 @@ export const viVn = { 'Địa chỉ hoặc phương thức giao hàng không thể áp dụng', DEPENDENCY_ERROR: 'Chúng tôi không thể xử lý đơn hàng của bạn ngay bây giờ. Vui lòng đợi một chút và thử lại', + AUTHORIZATION_FAILED: 'Không thể ủy quyền thanh toán', }, storefront: { product: 'Sản phẩm', diff --git a/packages/localizations/src/zhCn.ts b/packages/localizations/src/zhCn.ts index bf048ffd..fbd344c4 100644 --- a/packages/localizations/src/zhCn.ts +++ b/packages/localizations/src/zhCn.ts @@ -321,6 +321,7 @@ export const zhCn = { BILLING_ADDRESS_VERIFICATION_FAILED: '账单地址验证失败', MISSING_SHIPPING_INFO: '配送地址或方式应用失败', DEPENDENCY_ERROR: '我们目前无法处理您的订单。请稍等片刻再试', + AUTHORIZATION_FAILED: '付款授权失败', }, storefront: { product: '产品', diff --git a/packages/localizations/src/zhSg.ts b/packages/localizations/src/zhSg.ts index 049bb565..beb090b8 100644 --- a/packages/localizations/src/zhSg.ts +++ b/packages/localizations/src/zhSg.ts @@ -321,6 +321,7 @@ export const zhSg = { BILLING_ADDRESS_VERIFICATION_FAILED: '账单地址验证失败', MISSING_SHIPPING_INFO: '配送地址或方式应用失败', DEPENDENCY_ERROR: '我們目前無法處理您的訂單。請稍等片刻再試', + AUTHORIZATION_FAILED: '付款授权失败', }, storefront: { product: '产品', From a12f566d266e4aa43c054bbf588b4bb8bc1c8e02 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Wed, 4 Mar 2026 16:18:23 -0600 Subject: [PATCH 05/14] fix 0 dollar item and local pickup day rollover issues --- .../src/components/checkout/line-items/line-items.tsx | 2 +- .../react/src/components/checkout/pickup/local-pickup.tsx | 7 +++++++ packages/react/src/lib/godaddy/checkout-env.ts | 7 ------- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/checkout/line-items/line-items.tsx b/packages/react/src/components/checkout/line-items/line-items.tsx index 196e9315..8acc2262 100644 --- a/packages/react/src/components/checkout/line-items/line-items.tsx +++ b/packages/react/src/components/checkout/line-items/line-items.tsx @@ -153,7 +153,7 @@ export function DraftOrderLineItems({ ) : null}
- {item.originalPrice && item.quantity ? ( + {item.originalPrice != null && item.quantity ? (
diff --git a/packages/react/src/components/checkout/pickup/local-pickup.tsx b/packages/react/src/components/checkout/pickup/local-pickup.tsx index 6b79944a..fa3eb21d 100644 --- a/packages/react/src/components/checkout/pickup/local-pickup.tsx +++ b/packages/react/src/components/checkout/pickup/local-pickup.tsx @@ -374,7 +374,14 @@ export function LocalPickupForm({ } } + const selectedDayStr = format(selectedDate, 'yyyy-MM-dd'); + while (true) { + // Break if we've rolled past the selected date (midnight overflow) + if (format(currentTime, 'yyyy-MM-dd') !== selectedDayStr) { + break; + } + // Get the current slot's hour and minute for comparison const currentSlotHours = currentTime.getHours(); const currentSlotMins = currentTime.getMinutes(); diff --git a/packages/react/src/lib/godaddy/checkout-env.ts b/packages/react/src/lib/godaddy/checkout-env.ts index 69917bd5..641911a1 100644 --- a/packages/react/src/lib/godaddy/checkout-env.ts +++ b/packages/react/src/lib/godaddy/checkout-env.ts @@ -3151,13 +3151,6 @@ const introspection = { name: 'CheckoutSessionPaymentMethodConfigInput', }, }, - { - name: 'mercadopago', - type: { - kind: 'INPUT_OBJECT', - name: 'CheckoutSessionPaymentMethodConfigInput', - }, - }, ], isOneOf: false, }, From 01dc43687b05f6ab8e3c1d6ee54fe066d94a8d16 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Fri, 6 Mar 2026 09:21:57 -0600 Subject: [PATCH 06/14] add changeset --- .changeset/eight-knives-cough.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/eight-knives-cough.md diff --git a/.changeset/eight-knives-cough.md b/.changeset/eight-knives-cough.md new file mode 100644 index 00000000..f477662e --- /dev/null +++ b/.changeset/eight-knives-cough.md @@ -0,0 +1,6 @@ +--- +"@godaddy/localizations": patch +"@godaddy/react": patch +--- + +Add ACH Payment Support From 1737643dbd6415aaf3f327327208bbca5e4a7c0b Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Thu, 21 May 2026 11:14:00 -0500 Subject: [PATCH 07/14] adjust ach payment address logic and form display --- .../checkout/address/address-form.tsx | 16 ++++-- .../payment/checkout-buttons/ach/godaddy.tsx | 14 ++++-- .../checkout-buttons/credit-card/godaddy.tsx | 14 ++++-- .../checkout/payment/payment-form.tsx | 18 +++++-- .../payment/payment-methods/ach/godaddy.tsx | 50 +++++++++++++++++++ .../payment-methods/credit-card/container.tsx | 2 +- 6 files changed, 96 insertions(+), 18 deletions(-) diff --git a/packages/react/src/components/checkout/address/address-form.tsx b/packages/react/src/components/checkout/address/address-form.tsx index ef8e9258..03d98d1a 100644 --- a/packages/react/src/components/checkout/address/address-form.tsx +++ b/packages/react/src/components/checkout/address/address-form.tsx @@ -58,7 +58,10 @@ interface AddressFormProps { onlyNames?: boolean; } -export function AddressForm({ sectionKey, onlyNames = false }: AddressFormProps) { +export function AddressForm({ + sectionKey, + onlyNames = false, +}: AddressFormProps) { const form = useFormContext(); const { session } = useCheckoutContext(); const { t } = useGoDaddyContext(); @@ -442,8 +445,7 @@ export function AddressForm({ sectionKey, onlyNames = false }: AddressFormProps) render={({ field, fieldState }) => ( - {t.shipping.firstName}{' '} - {!onlyNames && `(${t.general.optional})`} + {t.shipping.firstName} {!onlyNames && `(${t.general.optional})`} (null); const { t } = useGoDaddyContext(); + const isDisabled = + isLoadingNonce || isConfirmingCheckout || isPaymentDisabled; + const handleSubmit = useCallback(async () => { + if (isDisabled || !collect) return; + const valid = await form.trigger(); if (!valid) { const firstError = Object.keys(form.formState.errors)[0]; @@ -24,8 +29,9 @@ export function ACHCheckoutButton() { return; } - collect?.getNonce({}); - }, [form, collect]); + setIsLoadingNonce(true); + collect.getNonce({}); + }, [collect, form, isDisabled, setIsLoadingNonce]); if (!collect) return null; @@ -36,7 +42,7 @@ export function ACHCheckoutButton() { type='button' onClick={handleSubmit} ref={buttonRef} - disabled={isLoadingNonce || isConfirmingCheckout || isPaymentDisabled} + disabled={isDisabled} > {t.payment.payNow} diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx index 72aafb79..08124af9 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx @@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button'; import { useGoDaddyContext } from '@/godaddy-provider'; export function CreditCardCheckoutButton() { - const { collect, isLoadingNonce } = usePoyntCollect(); + const { collect, isLoadingNonce, setIsLoadingNonce } = usePoyntCollect(); const { isConfirmingCheckout } = useCheckoutContext(); const isPaymentDisabled = useIsPaymentDisabled(); const form = useFormContext(); @@ -16,7 +16,12 @@ export function CreditCardCheckoutButton() { const buttonRef = useRef(null); const { t } = useGoDaddyContext(); + const isDisabled = + isLoadingNonce || isConfirmingCheckout || isPaymentDisabled; + const handleSubmit = useCallback(async () => { + if (isDisabled || !collect) return; + const valid = await form.trigger(); if (!valid) { const firstError = Object.keys(form.formState.errors)[0]; @@ -26,8 +31,9 @@ export function CreditCardCheckoutButton() { return; } - collect?.getNonce(poyntCardRequest); - }, [poyntCardRequest, form, collect]); + setIsLoadingNonce(true); + collect.getNonce(poyntCardRequest); + }, [collect, form, isDisabled, poyntCardRequest, setIsLoadingNonce]); if (!collect) return null; @@ -38,7 +44,7 @@ export function CreditCardCheckoutButton() { type='button' onClick={handleSubmit} ref={buttonRef} - disabled={isLoadingNonce || isConfirmingCheckout || isPaymentDisabled} + disabled={isDisabled} > {t.payment.payNow} diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index 44b7e7d5..85c2231e 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -81,6 +81,11 @@ const PAYMENT_METHOD_ICONS: Record = { ccavenue: , }; +const INLINE_BILLING_PAYMENT_METHODS: PaymentMethodValue[] = [ + PaymentMethodType.CREDIT_CARD, + PaymentMethodType.ACH, +]; + export function PaymentForm( props: DraftOrderTotalsProps & { items: Product[] } ) { @@ -99,6 +104,11 @@ export function PaymentForm( const useShippingAddress = form.watch('paymentUseShippingAddress'); const isPickup = deliveryMethod === DeliveryMethods.PICKUP; const isShipping = deliveryMethod === DeliveryMethods.SHIP; + const isPaymentMethodWithInlineBilling = paymentMethod + ? INLINE_BILLING_PAYMENT_METHODS.includes( + paymentMethod as PaymentMethodValue + ) + : false; const methodConfig = useGetSelectedPaymentMethod( paymentMethod as PaymentMethodValue ); @@ -264,15 +274,15 @@ export function PaymentForm( }, [session, pazeSupported, applePaySupported, googlePaySupported]); const shouldShowBillingNamesOnly = - paymentMethod !== PaymentMethodType.CREDIT_CARD && + !isPaymentMethodWithInlineBilling && session?.enableBillingAddressCollection === false && - !useShippingAddress; + (!useShippingAddress || isPickup); const isBillingAddressRequired = shouldShowBillingNamesOnly || (session?.enableBillingAddressCollection && (!useShippingAddress || isPickup) && - paymentMethod !== PaymentMethodType.CREDIT_CARD); + !isPaymentMethodWithInlineBilling); const billingCopy = shouldShowBillingNamesOnly && t.payment.billingInformation @@ -492,7 +502,7 @@ export function PaymentForm( {isShipping && session?.enableShipping && - paymentMethod !== PaymentMethodType.CREDIT_CARD ? ( + !isPaymentMethodWithInlineBilling ? ( ) : null} {isBillingAddressRequired ? ( diff --git a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx index bf8c0472..51885bf5 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx @@ -1,9 +1,15 @@ import { useId, useLayoutEffect, useRef, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { AddressForm } from '@/components/checkout/address'; import { useCheckoutContext } from '@/components/checkout/checkout'; +import { CheckoutSection } from '@/components/checkout/checkout-section'; +import { CheckoutSectionHeader } from '@/components/checkout/checkout-section-header'; +import { DeliveryMethods } from '@/components/checkout/delivery/delivery-method'; import type { TokenizeJs, TokenizeJsEvent, } from '@/components/checkout/payment/types'; +import { PaymentAddressToggle } from '@/components/checkout/payment/utils/payment-address-toggle'; import { usePoyntACHCollect } from '@/components/checkout/payment/utils/poynt-ach-provider'; import { PaymentProvider, @@ -22,6 +28,32 @@ export function GoDaddyACHForm() { const { godaddyPaymentsConfig, setCheckoutErrors } = useCheckoutContext(); const [error, setError] = useState(''); + const form = useFormContext(); + const paymentMethod = form.watch('paymentMethod'); + const useShippingAddress = form.watch('paymentUseShippingAddress'); + const deliveryMethod = form.watch('deliveryMethod'); + const isShipping = deliveryMethod === DeliveryMethods.SHIP; + const isPickup = deliveryMethod === DeliveryMethods.PICKUP; + + const shouldShowBillingNamesOnly = + paymentMethod === PaymentMethodType.ACH && + session?.enableBillingAddressCollection === false && + (!useShippingAddress || isPickup); + + const isBillingAddressRequired = + !session?.enableShipping || + shouldShowBillingNamesOnly || + (session?.enableBillingAddressCollection && + (!useShippingAddress || isPickup) && + paymentMethod === PaymentMethodType.ACH); + + const billingCopy = + shouldShowBillingNamesOnly && t.payment.billingInformation + ? t.payment.billingInformation + : t.payment.billingAddress; + + const description = t.payment.descriptions?.ach; + const confirmCheckout = useConfirmCheckout(); const elementId = `gdpay-ach-element-${useId()}`; @@ -211,10 +243,28 @@ export function GoDaddyACHForm() { return ( <> + {description ?
{description}
: null}
{error ? (

{error}

) : null} + {session?.enableShipping && + isShipping && + paymentMethod === PaymentMethodType.ACH ? ( + + ) : null} + {isBillingAddressRequired ? ( + + + + + ) : null} ); } diff --git a/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx b/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx index c7d3eaa0..5eb7a9ab 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx @@ -22,7 +22,7 @@ export function CreditCardContainer({ children }: { children?: ReactNode }) { const shouldShowBillingNamesOnly = paymentMethod === PaymentMethodType.CREDIT_CARD && session?.enableBillingAddressCollection === false && - !useShippingAddress; + (!useShippingAddress || isPickup); const isBillingAddressRequired = !session?.enableShipping || From 3ecda5a7b783099ad5e6349fcbedb395132356f3 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Thu, 21 May 2026 12:48:57 -0500 Subject: [PATCH 08/14] fix review feedback --- packages/localizations/src/enAu.ts | 3 +++ .../checkout/payment/checkout-buttons/applePay/godaddy.tsx | 7 +++---- .../checkout/payment/checkout-buttons/express/godaddy.tsx | 7 +++---- .../payment/checkout-buttons/googlePay/godaddy.tsx | 7 +++---- .../checkout/payment/checkout-buttons/paze/godaddy.tsx | 7 +++---- .../react/src/components/checkout/payment/payment-form.tsx | 7 +++---- .../checkout/payment/payment-methods/ach/godaddy.tsx | 6 +++++- .../payment/payment-methods/credit-card/godaddy.tsx | 5 ++++- packages/react/src/components/checkout/tips/tips-form.tsx | 4 ++-- 9 files changed, 29 insertions(+), 24 deletions(-) diff --git a/packages/localizations/src/enAu.ts b/packages/localizations/src/enAu.ts index 68fbe932..2420a7aa 100644 --- a/packages/localizations/src/enAu.ts +++ b/packages/localizations/src/enAu.ts @@ -110,6 +110,7 @@ export const enAu = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Offline payments', + ach: 'Bank Account', mercadopago: 'Mercado Pago', ccavenue: 'Pay with CCAvenue', }, @@ -120,6 +121,7 @@ export const enAu = { googlePay: '', paze: '', offline: '', + ach: '', mercadopago: 'Use the MercadoPago form below to complete your purchase securely.', ccavenue: '', @@ -339,6 +341,7 @@ export const enAu = { MISSING_SHIPPING_INFO: 'Shipping address or method failed to apply', DEPENDENCY_ERROR: "We're unable to process your order right now. Please wait a moment and try again", + AUTHORIZATION_FAILED: 'Failed to authorise payment', }, storefront: { product: 'Product', diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/applePay/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/applePay/godaddy.tsx index 06155e81..62cde7bc 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/applePay/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/applePay/godaddy.tsx @@ -36,6 +36,7 @@ export function GoDaddyApplePayCheckoutButton() { const currencyCode = totals?.total?.currencyCode || 'USD'; const countryCode = session?.shipping?.originAddress?.countryCode || 'US'; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); const confirmCheckout = useConfirmCheckout(); const collect = useRef(null); @@ -91,10 +92,7 @@ export function GoDaddyApplePayCheckoutButton() { businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId( - session, - godaddyPaymentsConfig?.appId - ), + applicationId, }, { country: countryCode, @@ -113,6 +111,7 @@ export function GoDaddyApplePayCheckoutButton() { session?.businessId, session?.storeId, session?.channelId, + applicationId, isPoyntLoaded, isCollectLoading, ]); diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx index 9b3e3d65..17d79001 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx @@ -85,6 +85,7 @@ export function ExpressCheckoutButton() { const updateTaxes = useUpdateTaxes(); const countryCode = session?.shipping?.originAddress?.countryCode || 'US'; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); const confirmCheckout = useConfirmCheckout(); const collect = useRef(null); @@ -439,10 +440,7 @@ export function ExpressCheckoutButton() { businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId( - session, - godaddyPaymentsConfig?.appId - ), + applicationId, }, { country: countryCode, @@ -512,6 +510,7 @@ export function ExpressCheckoutButton() { session?.businessId, session?.storeId, session?.channelId, + applicationId, session?.enablePromotionCodes, session?.enableShippingAddressCollection, session?.storeName, diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/googlePay/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/googlePay/godaddy.tsx index 1b2b08b4..a7bfa20f 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/googlePay/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/googlePay/godaddy.tsx @@ -36,6 +36,7 @@ export function GoDaddyGooglePayCheckoutButton() { const currencyCode = totals?.total?.currencyCode || 'USD'; const countryCode = session?.shipping?.originAddress?.countryCode || 'US'; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); const confirmCheckout = useConfirmCheckout(); const collect = useRef(null); @@ -91,10 +92,7 @@ export function GoDaddyGooglePayCheckoutButton() { businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId( - session, - godaddyPaymentsConfig?.appId - ), + applicationId, }, { country: countryCode, @@ -113,6 +111,7 @@ export function GoDaddyGooglePayCheckoutButton() { session?.businessId, session?.storeId, session?.channelId, + applicationId, isPoyntLoaded, isCollectLoading, ]); diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx index 3d508f8d..ffd2b83c 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx @@ -36,6 +36,7 @@ export function PazeCheckoutButton() { const currencyCode = totals?.total?.currencyCode || 'USD'; const countryCode = session?.shipping?.originAddress?.countryCode || 'US'; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); const confirmCheckout = useConfirmCheckout(); const collect = useRef(null); @@ -90,10 +91,7 @@ export function PazeCheckoutButton() { businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId( - session, - godaddyPaymentsConfig?.appId - ), + applicationId, }, { country: countryCode, @@ -112,6 +110,7 @@ export function PazeCheckoutButton() { session?.businessId, session?.storeId, session?.channelId, + applicationId, isPoyntLoaded, isCollectLoading, ]); diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index 85c2231e..ef7671d7 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -125,6 +125,7 @@ export function PaymentForm( const currencyCode = props.currencyCode || 'USD'; const countryCode = session?.shipping?.originAddress?.countryCode || 'US'; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); // Helper function to get translated payment method labels const getPaymentMethodLabel = useCallback( @@ -206,10 +207,7 @@ export function PaymentForm( businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId( - session, - godaddyPaymentsConfig?.appId - ), + applicationId, }, { country: countryCode, @@ -233,6 +231,7 @@ export function PaymentForm( session?.businessId, session?.storeId, session?.channelId, + applicationId, isPoyntLoaded, ]); diff --git a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx index 51885bf5..46098438 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx @@ -9,6 +9,7 @@ import type { TokenizeJs, TokenizeJsEvent, } from '@/components/checkout/payment/types'; +import { getApplicationId } from '@/components/checkout/payment/utils/get-application-id'; import { PaymentAddressToggle } from '@/components/checkout/payment/utils/payment-address-toggle'; import { usePoyntACHCollect } from '@/components/checkout/payment/utils/poynt-ach-provider'; import { @@ -57,6 +58,7 @@ export function GoDaddyACHForm() { const confirmCheckout = useConfirmCheckout(); const elementId = `gdpay-ach-element-${useId()}`; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); const fontFamily = '"GD Sherpa", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'; @@ -186,7 +188,7 @@ export function GoDaddyACHForm() { businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: godaddyPaymentsConfig?.appId, + applicationId, }); collect?.current?.on('ready', () => { @@ -239,6 +241,8 @@ export function GoDaddyACHForm() { session?.businessId, session?.storeId, session?.channelId, + applicationId, + elementId, ]); return ( diff --git a/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx b/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx index 79358e3c..8033df5b 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx @@ -26,6 +26,7 @@ export function GoDaddyCreditCardForm() { const confirmCheckout = useConfirmCheckout(); const elementId = `gdpay-card-element-${useId()}`; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); const options = { iFrame: { @@ -202,7 +203,7 @@ export function GoDaddyCreditCardForm() { businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId(session, godaddyPaymentsConfig?.appId), + applicationId, }); collect?.current?.on('ready', () => { @@ -255,6 +256,8 @@ export function GoDaddyCreditCardForm() { session?.businessId, session?.storeId, session?.channelId, + applicationId, + elementId, ]); return ( diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index 30c97179..ec0f63b9 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -237,7 +237,7 @@ function CustomTipInput({ const [localValue, setLocalValue] = useState(null); const isFocused = useRef(false); - // Debounce the local value so the form syncs after 3s of inactivity, + // Debounce the local value so the form syncs after 1.5s of inactivity, // even if the user hasn't blurred the input yet. This keeps the order // summary / totals up-to-date while the input stays focused. const [debouncedLocal] = useDebouncedValue(localValue, { wait: 1500 }); @@ -283,7 +283,7 @@ function CustomTipInput({ // When the debounced value settles and the input is still focused, // sync to form state and format the display — the same effect as blur - // but triggered by 3s of inactivity. This keeps the order summary + // but triggered by 1.5s of inactivity. This keeps the order summary // up-to-date and gives the user visual confirmation of their amount. useEffect(() => { if (!isFocused.current || debouncedLocal === null) return; From 453fdefd372084ff476bbefcf18b23b2ae29046d Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Thu, 21 May 2026 13:04:56 -0500 Subject: [PATCH 09/14] fix stuck nonce loading state --- .../checkout/payment/lazy-payment-loader.tsx | 19 +++++++++++++++++++ .../payment/payment-methods/ach/godaddy.tsx | 3 +++ .../payment-methods/credit-card/godaddy.tsx | 3 +++ .../components/checkout/tips/tips-form.tsx | 17 +++++++---------- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx index 401dab01..822ef7ed 100644 --- a/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx +++ b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx @@ -1,6 +1,7 @@ 'use client'; import { type ComponentType, lazy, Suspense } from 'react'; +import { useCheckoutContext } from '@/components/checkout/checkout'; import { Skeleton } from '@/components/ui/skeleton'; import { type AvailablePaymentProviders, @@ -339,6 +340,24 @@ export function LazyPaymentMethodRenderer({ provider, isExpress, }: LazyPaymentMethodRendererProps) { + const { godaddyPaymentsConfig, session } = useCheckoutContext(); + + if (provider === PaymentProvider.GODADDY) { + const hasGoDaddyAppId = !!godaddyPaymentsConfig?.appId?.trim(); + + if (method === PaymentMethodType.CREDIT_CARD && !hasGoDaddyAppId) { + return null; + } + + if ( + method === PaymentMethodType.ACH && + (!hasGoDaddyAppId || + session?.paymentMethods?.ach?.processor !== PaymentProvider.GODADDY) + ) { + return null; + } + } + const methodRegistry = lazyPaymentComponentRegistry[ method as keyof typeof lazyPaymentComponentRegistry diff --git a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx index 46098438..3bfcdc86 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx @@ -213,6 +213,7 @@ export function GoDaddyACHForm() { if (err instanceof GraphQLErrorWithCodes) { setCheckoutErrors(err.codes); } + setIsLoadingNonce(false); } } else { setCheckoutErrors(['TRANSACTION_PROCESSING_FAILED']); @@ -228,6 +229,8 @@ export function GoDaddyACHForm() { collect?.current?.on('validated', event => { if (event?.data?.validated) { setError(''); + } else { + setIsLoadingNonce(false); } }); }, [ diff --git a/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx b/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx index 8033df5b..8f3e6573 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx @@ -228,6 +228,7 @@ export function GoDaddyCreditCardForm() { if (err instanceof GraphQLErrorWithCodes) { setCheckoutErrors(err.codes); } + setIsLoadingNonce(false); } } else { setCheckoutErrors(['TRANSACTION_PROCESSING_FAILED']); @@ -243,6 +244,8 @@ export function GoDaddyCreditCardForm() { collect?.current?.on('validated', event => { if (event?.data?.validated) { setError(''); + } else { + setIsLoadingNonce(false); } }); }, [ diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index ec0f63b9..0f6b00cb 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -5,6 +5,7 @@ import { useCheckoutContext } from '@/components/checkout/checkout'; import { convertMajorToMinorUnits, currencyConfigs, + type FormatCurrencyOptions, useFormatCurrency, } from '@/components/checkout/utils/format-currency'; import { Button } from '@/components/ui/button'; @@ -181,12 +182,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { interface CustomTipInputProps { currencyCode?: string; total: number; - formatCurrency: (options: { - amount: number; - currencyCode: string; - inputInMinorUnits?: boolean; - returnRaw?: boolean; - }) => string; + formatCurrency: (options: FormatCurrencyOptions) => string; } /** @@ -292,7 +288,7 @@ function CustomTipInput({ // Clear local state so the display derives from the formatted form // value (e.g. "10.5" → "10.50"), same as the blur handler. setLocalValue(null); - }, [debouncedLocal]); // eslint-disable-line react-hooks/exhaustive-deps + }, [debouncedLocal, code, form]); const symbolEl = ( 0 + ? Number(((tipAmount / total) * 100).toFixed(2)) + : 0, currencyCode, }, }); From d33041aa5b2f52d09fce95efeef5d85e3ee91612 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Thu, 21 May 2026 15:17:47 -0500 Subject: [PATCH 10/14] display shipping, fees, taxes if order has values even when features are disabled --- packages/localizations/src/deDe.ts | 1 + packages/localizations/src/enAu.ts | 1 + packages/localizations/src/enIe.ts | 1 + packages/localizations/src/enUs.ts | 1 + packages/localizations/src/esAr.ts | 1 + packages/localizations/src/esCl.ts | 1 + packages/localizations/src/esCo.ts | 1 + packages/localizations/src/esEs.ts | 1 + packages/localizations/src/esMx.ts | 1 + packages/localizations/src/esPe.ts | 1 + packages/localizations/src/esUs.ts | 1 + packages/localizations/src/frCa.ts | 1 + packages/localizations/src/frFr.ts | 1 + packages/localizations/src/idId.ts | 1 + packages/localizations/src/itIt.ts | 1 + packages/localizations/src/ptBr.ts | 1 + packages/localizations/src/qaPs.ts | 1 + packages/localizations/src/trTr.ts | 1 + packages/localizations/src/viVn.ts | 1 + packages/localizations/src/zhCn.ts | 1 + packages/localizations/src/zhSg.ts | 1 + .../checkout/form/checkout-form.tsx | 49 ++++++++++++------- .../checkout/payment/payment-form.tsx | 5 +- .../src/components/checkout/target/target.tsx | 1 + .../src/components/checkout/totals/totals.tsx | 18 +++++++ 25 files changed, 75 insertions(+), 19 deletions(-) diff --git a/packages/localizations/src/deDe.ts b/packages/localizations/src/deDe.ts index 9261eceb..d9871810 100644 --- a/packages/localizations/src/deDe.ts +++ b/packages/localizations/src/deDe.ts @@ -168,6 +168,7 @@ export const deDe = { shipping: 'Versand', tip: 'Trinkgeld', estimatedTaxes: 'Geschätzte Steuern', + fees: 'Gebühren', totalDue: 'Gesamtbetrag', orderSummary: 'Bestellübersicht', itemCount: 'Artikel', diff --git a/packages/localizations/src/enAu.ts b/packages/localizations/src/enAu.ts index 2420a7aa..cf205cb9 100644 --- a/packages/localizations/src/enAu.ts +++ b/packages/localizations/src/enAu.ts @@ -168,6 +168,7 @@ export const enAu = { shipping: 'Shipping', tip: 'Tip', estimatedTaxes: 'Estimated GST', + fees: 'Fees', totalDue: 'Total Due', orderSummary: 'Order Summary', itemCount: 'items', diff --git a/packages/localizations/src/enIe.ts b/packages/localizations/src/enIe.ts index aa060e3d..881fd736 100644 --- a/packages/localizations/src/enIe.ts +++ b/packages/localizations/src/enIe.ts @@ -168,6 +168,7 @@ export const enIe = { shipping: 'Shipping', tip: 'Tip', estimatedTaxes: 'Estimated VAT', + fees: 'Fees', totalDue: 'Total Due', orderSummary: 'Order Summary', itemCount: 'items', diff --git a/packages/localizations/src/enUs.ts b/packages/localizations/src/enUs.ts index d9b2b441..f485b21c 100644 --- a/packages/localizations/src/enUs.ts +++ b/packages/localizations/src/enUs.ts @@ -168,6 +168,7 @@ export const enUs = { shipping: 'Shipping', tip: 'Tip', estimatedTaxes: 'Estimated taxes', + fees: 'Fees', totalDue: 'Total Due', orderSummary: 'Order Summary', itemCount: 'items', diff --git a/packages/localizations/src/esAr.ts b/packages/localizations/src/esAr.ts index 80190021..9cfd4cdd 100644 --- a/packages/localizations/src/esAr.ts +++ b/packages/localizations/src/esAr.ts @@ -169,6 +169,7 @@ export const esAr = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a Pagar', orderSummary: 'Resumen del Pedido', itemCount: 'productos', diff --git a/packages/localizations/src/esCl.ts b/packages/localizations/src/esCl.ts index 246f3fb8..d7defa1b 100644 --- a/packages/localizations/src/esCl.ts +++ b/packages/localizations/src/esCl.ts @@ -169,6 +169,7 @@ export const esCl = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a Pagar', orderSummary: 'Resumen del Pedido', itemCount: 'artículos', diff --git a/packages/localizations/src/esCo.ts b/packages/localizations/src/esCo.ts index 55f7a4fb..ac8d83a7 100644 --- a/packages/localizations/src/esCo.ts +++ b/packages/localizations/src/esCo.ts @@ -169,6 +169,7 @@ export const esCo = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a pagar', orderSummary: 'Resumen del pedido', itemCount: 'artículos', diff --git a/packages/localizations/src/esEs.ts b/packages/localizations/src/esEs.ts index 863c608c..07804024 100644 --- a/packages/localizations/src/esEs.ts +++ b/packages/localizations/src/esEs.ts @@ -169,6 +169,7 @@ export const esEs = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a pagar', orderSummary: 'Resumen del pedido', itemCount: 'artículos', diff --git a/packages/localizations/src/esMx.ts b/packages/localizations/src/esMx.ts index 9b94c1fb..a1d49236 100644 --- a/packages/localizations/src/esMx.ts +++ b/packages/localizations/src/esMx.ts @@ -169,6 +169,7 @@ export const esMx = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a Pagar', orderSummary: 'Resumen del Pedido', itemCount: 'artículos', diff --git a/packages/localizations/src/esPe.ts b/packages/localizations/src/esPe.ts index e74c2f85..88dfcfcb 100644 --- a/packages/localizations/src/esPe.ts +++ b/packages/localizations/src/esPe.ts @@ -169,6 +169,7 @@ export const esPe = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a Pagar', orderSummary: 'Resumen del Pedido', itemCount: 'productos', diff --git a/packages/localizations/src/esUs.ts b/packages/localizations/src/esUs.ts index ea83d7b7..a951a673 100644 --- a/packages/localizations/src/esUs.ts +++ b/packages/localizations/src/esUs.ts @@ -169,6 +169,7 @@ export const esUs = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a Pagar', orderSummary: 'Resumen del Pedido', itemCount: 'artículos', diff --git a/packages/localizations/src/frCa.ts b/packages/localizations/src/frCa.ts index 63a459ac..5268a2d9 100644 --- a/packages/localizations/src/frCa.ts +++ b/packages/localizations/src/frCa.ts @@ -169,6 +169,7 @@ export const frCa = { shipping: 'Expédition', tip: 'Pourboire', estimatedTaxes: 'Taxes estimées', + fees: 'Frais', totalDue: 'Total à payer', orderSummary: 'Résumé de la commande', itemCount: 'articles', diff --git a/packages/localizations/src/frFr.ts b/packages/localizations/src/frFr.ts index a1f6ae49..c59d74a4 100644 --- a/packages/localizations/src/frFr.ts +++ b/packages/localizations/src/frFr.ts @@ -169,6 +169,7 @@ export const frFr = { shipping: 'Expédition', tip: 'Pourboire', estimatedTaxes: 'Taxes estimées', + fees: 'Frais', totalDue: 'Total à payer', orderSummary: 'Récapitulatif de la commande', itemCount: 'articles', diff --git a/packages/localizations/src/idId.ts b/packages/localizations/src/idId.ts index 4c597c82..948417cb 100644 --- a/packages/localizations/src/idId.ts +++ b/packages/localizations/src/idId.ts @@ -168,6 +168,7 @@ export const idId = { shipping: 'Pengiriman', tip: 'Tip', estimatedTaxes: 'Perkiraan pajak', + fees: 'Biaya', totalDue: 'Total Pembayaran', orderSummary: 'Ringkasan Pesanan', itemCount: 'item', diff --git a/packages/localizations/src/itIt.ts b/packages/localizations/src/itIt.ts index c3212728..0e735589 100644 --- a/packages/localizations/src/itIt.ts +++ b/packages/localizations/src/itIt.ts @@ -169,6 +169,7 @@ export const itIt = { shipping: 'Spedizione', tip: 'Mancia', estimatedTaxes: 'Tasse stimate', + fees: 'Commissioni', totalDue: 'Totale Dovuto', orderSummary: 'Riepilogo Ordine', itemCount: 'articoli', diff --git a/packages/localizations/src/ptBr.ts b/packages/localizations/src/ptBr.ts index fe892e5d..dcf0eb34 100644 --- a/packages/localizations/src/ptBr.ts +++ b/packages/localizations/src/ptBr.ts @@ -168,6 +168,7 @@ export const ptBr = { shipping: 'Envio', tip: 'Gorjeta', estimatedTaxes: 'Impostos estimados', + fees: 'Taxas', totalDue: 'Total a Pagar', orderSummary: 'Resumo do Pedido', itemCount: 'itens', diff --git a/packages/localizations/src/qaPs.ts b/packages/localizations/src/qaPs.ts index 6d8c0842..28e5a478 100644 --- a/packages/localizations/src/qaPs.ts +++ b/packages/localizations/src/qaPs.ts @@ -169,6 +169,7 @@ export const qaPs = { shipping: '[Šhîþþîñg Çöšţš]', tip: '[Ţîþ Âmöüñţ]', estimatedTaxes: '[Ëšţîmâţëd ţâxëš çâlçülâţîöñ]', + fees: '[Fëëš Çhârgëd]', totalDue: '[Tötâl Âmöüñţ Düë]', orderSummary: '[Ördër Šümmârÿ Dëţâîlš]', itemCount: '[îţëmš îñ çârţ]', diff --git a/packages/localizations/src/trTr.ts b/packages/localizations/src/trTr.ts index e9c64ca2..97427426 100644 --- a/packages/localizations/src/trTr.ts +++ b/packages/localizations/src/trTr.ts @@ -168,6 +168,7 @@ export const trTr = { shipping: 'Kargo', tip: 'Bahşiş', estimatedTaxes: 'Tahmini vergiler', + fees: 'Ücretler', totalDue: 'Ödenecek Toplam', orderSummary: 'Sipariş Özeti', itemCount: 'ürün', diff --git a/packages/localizations/src/viVn.ts b/packages/localizations/src/viVn.ts index 18d34a5a..b838a6cd 100644 --- a/packages/localizations/src/viVn.ts +++ b/packages/localizations/src/viVn.ts @@ -168,6 +168,7 @@ export const viVn = { shipping: 'Vận chuyển', tip: 'Tip', estimatedTaxes: 'Thuế ước tính', + fees: 'Phí', totalDue: 'Tổng cộng', orderSummary: 'Tóm tắt đơn hàng', itemCount: 'sản phẩm', diff --git a/packages/localizations/src/zhCn.ts b/packages/localizations/src/zhCn.ts index 7c676fe5..8feb4056 100644 --- a/packages/localizations/src/zhCn.ts +++ b/packages/localizations/src/zhCn.ts @@ -163,6 +163,7 @@ export const zhCn = { shipping: '配送费', tip: '小费', estimatedTaxes: '预估税费', + fees: '费用', totalDue: '应付总计', orderSummary: '订单摘要', itemCount: '件商品', diff --git a/packages/localizations/src/zhSg.ts b/packages/localizations/src/zhSg.ts index 6fdcadf9..dbe6bb16 100644 --- a/packages/localizations/src/zhSg.ts +++ b/packages/localizations/src/zhSg.ts @@ -163,6 +163,7 @@ export const zhSg = { shipping: '运费', tip: '小费', estimatedTaxes: '预估税费', + fees: '费用', totalDue: '应付总额', orderSummary: '订单摘要', itemCount: '件商品', diff --git a/packages/react/src/components/checkout/form/checkout-form.tsx b/packages/react/src/components/checkout/form/checkout-form.tsx index 1655cf4b..872cb906 100644 --- a/packages/react/src/components/checkout/form/checkout-form.tsx +++ b/packages/react/src/components/checkout/form/checkout-form.tsx @@ -24,10 +24,7 @@ import { type Product, } from '@/components/checkout/line-items/line-items'; import { NotesForm } from '@/components/checkout/notes/notes-form'; -import { - useDraftOrder, - useDraftOrderTotals, -} from '@/components/checkout/order/use-draft-order'; +import { useDraftOrderTotals } from '@/components/checkout/order/use-draft-order'; import { PaymentForm } from '@/components/checkout/payment/payment-form'; import { ConditionalExpressProviders, @@ -103,21 +100,21 @@ export function CheckoutForm({ mutationKey: ['update-draft-order-taxes', { id: session?.id }], }) > 0; + const isUpdatingFees = + useIsMutating({ + mutationKey: ['update-draft-order-fees', { id: session?.id }], + }) > 0; + const draftOrderTotalsQuery = useDraftOrderTotals(); - const draftOrder = useDraftOrder(); const { data: totals, isLoading: totalsLoading } = draftOrderTotalsQuery; - const { data: order } = draftOrder; // Order summary calculations - keep all values in minor units const subtotal = totals?.subTotal?.value || 0; const orderDiscount = totals?.discountTotal?.value || 0; - const shipping = - order?.shippingLines?.reduce( - (sum, line) => sum + (line?.amount?.value || 0), - 0 - ) || 0; + const shipping = totals?.shippingTotal?.value || 0; const taxTotal = totals?.taxTotal?.value || 0; + const feeTotal = totals?.feeTotal?.value || 0; const orderTotal = totals?.total?.value || 0; const tipTotal = tipAmount || 0; const currencyCode = totals?.total?.currencyCode || 'USD'; @@ -126,6 +123,14 @@ export function CheckoutForm({ const isFree = orderTotal <= 0; const showExpressButtons = subtotal > 0; + // Show shipping/taxes/fees lines if collection is enabled OR if there's + // a preset amount on the order. This way merchants who disable collection + // but pre-apply a value still see it reflected in the summary. + const showShippingLine = + (isShipping && !!session?.enableShipping) || shipping > 0; + const showTaxesLine = !!session?.enableTaxCollection || taxTotal > 0; + const showFeesLine = feeTotal > 0; + useEffect(() => { if (!totalsLoading && isFree) { form.setValue('paymentMethod', PaymentMethodType.OFFLINE); @@ -394,7 +399,9 @@ export function CheckoutForm({ currencyCode={currencyCode} tip={tipTotal} taxes={taxTotal} + fees={feeTotal} isTaxLoading={isUpdatingTaxes} + isFeeLoading={isUpdatingFees} isShippingLoading={isUpdatingShipping} isDiscountLoading={isDiscountApplying} subtotal={subtotal} @@ -403,9 +410,9 @@ export function CheckoutForm({ totalSavings={totalSavings} itemCount={itemCount} total={orderTotal} - enableShipping={ - isShipping && session?.enableShipping - } + enableShipping={showShippingLine} + enableTaxes={showTaxesLine} + enableFees={showFeesLine} /> ) : ( @@ -460,7 +467,9 @@ export function CheckoutForm({ currencyCode={currencyCode} tip={tipTotal} taxes={taxTotal} + fees={feeTotal} isTaxLoading={isUpdatingTaxes} + isFeeLoading={isUpdatingFees} isShippingLoading={isUpdatingShipping} subtotal={subtotal} discount={orderDiscount} @@ -470,8 +479,9 @@ export function CheckoutForm({ itemCount={itemCount} total={orderTotal} enableDiscounts={session?.enablePromotionCodes} - enableTaxes={session?.enableTaxCollection} - enableShipping={isShipping && session?.enableShipping} + enableTaxes={showTaxesLine} + enableFees={showFeesLine} + enableShipping={showShippingLine} />
@@ -491,7 +501,9 @@ export function CheckoutForm({ currencyCode={currencyCode} tip={tipTotal} taxes={taxTotal} + fees={feeTotal} isTaxLoading={isUpdatingTaxes} + isFeeLoading={isUpdatingFees} isShippingLoading={isUpdatingShipping} subtotal={subtotal} discount={orderDiscount} @@ -501,8 +513,9 @@ export function CheckoutForm({ itemCount={itemCount} total={orderTotal} enableDiscounts={session?.enablePromotionCodes} - enableTaxes={session?.enableTaxCollection} - enableShipping={isShipping && session?.enableShipping} + enableTaxes={showTaxesLine} + enableFees={showFeesLine} + enableShipping={showShippingLine} />
diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index ef7671d7..05d90362 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -554,7 +554,9 @@ export function PaymentForm( currencyCode={props.currencyCode} tip={props.tip} taxes={props.taxes} + fees={props.fees} isTaxLoading={props.isTaxLoading} + isFeeLoading={props.isFeeLoading} isShippingLoading={props.isShippingLoading} subtotal={props.subtotal} discount={props.discount} @@ -564,7 +566,8 @@ export function PaymentForm( total={props.total} enableShipping={props.enableShipping} enableDiscounts={session?.enablePromotionCodes} - enableTaxes={session?.enableTaxCollection} + enableTaxes={props.enableTaxes ?? session?.enableTaxCollection} + enableFees={props.enableFees ?? session?.enableSurcharge} />
diff --git a/packages/react/src/components/checkout/target/target.tsx b/packages/react/src/components/checkout/target/target.tsx index 682571cc..b3956809 100644 --- a/packages/react/src/components/checkout/target/target.tsx +++ b/packages/react/src/components/checkout/target/target.tsx @@ -34,6 +34,7 @@ export type Target = | 'checkout.summary.totals.shipping.before' | 'checkout.summary.totals.tip.before' | 'checkout.summary.totals.taxes.before' + | 'checkout.summary.totals.fees.before' | 'checkout.summary.totals.total-due.before' | 'checkout.summary.totals.total-due.after' | 'checkout.summary.totals.after' diff --git a/packages/react/src/components/checkout/totals/totals.tsx b/packages/react/src/components/checkout/totals/totals.tsx index 288771eb..ffee47e2 100644 --- a/packages/react/src/components/checkout/totals/totals.tsx +++ b/packages/react/src/components/checkout/totals/totals.tsx @@ -18,6 +18,9 @@ export interface DraftOrderTotalsProps { tip?: number; taxes?: number; isTaxLoading?: boolean; + fees?: number; + isFeeLoading?: boolean; + enableFees?: boolean | null; enableTaxes?: boolean | null; enableDiscounts?: boolean | null; enableShipping?: boolean | null; @@ -69,11 +72,14 @@ export function DraftOrderTotals({ total = 0, tip = 0, taxes = 0, + fees = 0, enableDiscounts = false, enableTaxes = false, + enableFees = false, isTaxLoading = false, isShippingLoading = false, isDiscountLoading = false, + isFeeLoading = false, enableShipping = true, inputInMinorUnits = false, }: DraftOrderTotalsProps) { @@ -149,6 +155,18 @@ export function DraftOrderTotals({ inputInMinorUnits={inputInMinorUnits} /> ))} + + {enableFees && + (isFeeLoading ? ( + + ) : ( + + ))}
From 20f5f83f929a4aef9dd91286dc4efe2b333f0426 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Thu, 21 May 2026 15:19:11 -0500 Subject: [PATCH 11/14] remove other surcharge check --- packages/react/src/components/checkout/payment/payment-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index 05d90362..92d809ca 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -567,7 +567,7 @@ export function PaymentForm( enableShipping={props.enableShipping} enableDiscounts={session?.enablePromotionCodes} enableTaxes={props.enableTaxes ?? session?.enableTaxCollection} - enableFees={props.enableFees ?? session?.enableSurcharge} + enableFees={props.enableFees} />
From d2b51cbb4da640d5b5c6acb5cd7cf7aad03ac4b2 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Thu, 21 May 2026 16:36:07 -0500 Subject: [PATCH 12/14] add loading parent loading state prop --- packages/react/README.md | 17 +++++++++++++++++ .../react/src/components/checkout/checkout.tsx | 2 ++ .../checkout/form/checkout-form-container.tsx | 8 ++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/react/README.md b/packages/react/README.md index 2047b067..c8f578df 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -184,6 +184,23 @@ All fields are optional strings. Pass any subset to override the defaults. | `secondaryBackground` | Secondary background | | `secondaryForeground` | Text on secondary backgrounds | +### Loading State + +The `` component manages its own loading state internally — it shows a skeleton while it fetches its JWT and draft order, then reveals the form. If you need to coordinate with your own preload work (user profile, theme, feature flags, etc.), the following props let you keep the skeleton up while your data loads. + +| Prop | Type | Description | +|------|------|-------------| +| `isLoading` | `boolean` | When `true`, the checkout keeps showing its loading state even after its own internal data has finished loading. Internal queries still run in the background, so the checkout is warm and ready to render the moment you flip this back to `false`. Mount the component early and reveal late — you avoid both the layout flicker of a late-mount and the wasted time of gating the mount entirely. | +| `loadingFallback` | `ReactNode` | Optional override for the loading UI. Defaults to the built-in ``. Rendered whenever `isLoading` is `true` or the checkout is loading its own internal data. | + +```tsx +} +/> +``` + ## AI Agent Skills This package ships a [TanStack Intent](https://tanstack.com/intent/latest) skill that teaches AI coding agents how to authenticate with the GoDaddy Commerce Platform using OAuth2 client credentials and create checkout sessions. For API discovery and testing, the skill directs agents to use [`@godaddy/cli`](https://www.npmjs.com/package/@godaddy/cli). diff --git a/packages/react/src/components/checkout/checkout.tsx b/packages/react/src/components/checkout/checkout.tsx index e07c2d3a..4bca02ec 100644 --- a/packages/react/src/components/checkout/checkout.tsx +++ b/packages/react/src/components/checkout/checkout.tsx @@ -223,6 +223,8 @@ export interface CheckoutProps { targets?: Partial ReactNode>>; checkoutFormSchema?: CheckoutFormSchema; defaultValues?: Pick; + isLoading?: boolean; + loadingFallback?: ReactNode; } export function Checkout(props: CheckoutProps) { diff --git a/packages/react/src/components/checkout/form/checkout-form-container.tsx b/packages/react/src/components/checkout/form/checkout-form-container.tsx index bc17109d..1a9c8b32 100644 --- a/packages/react/src/components/checkout/form/checkout-form-container.tsx +++ b/packages/react/src/components/checkout/form/checkout-form-container.tsx @@ -59,8 +59,12 @@ export function CheckoutFormContainer({ } } - if (draftOrderQuery.isLoading || isLoadingJWT) { - return ; + if (props.isLoading || draftOrderQuery.isLoading || isLoadingJWT) { + return ( + props.loadingFallback ?? ( + + ) + ); } return ( From 2d29be9d8293e2d1d486760b800144551f9fdc8d Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Thu, 21 May 2026 18:42:40 -0500 Subject: [PATCH 13/14] refactor billing and shipping form collection to be either ship or not ship --- .../src/components/checkout/checkout.tsx | 20 +++++++++++++++---- .../checkout/form/checkout-form-container.tsx | 4 +--- .../checkout/form/custom-form-provider.tsx | 7 +++++-- .../checkout/payment/payment-form.tsx | 17 +++++++++++----- .../payment/payment-methods/ach/godaddy.tsx | 19 +++++++++++------- .../payment-methods/credit-card/container.tsx | 20 ++++++++++++------- 6 files changed, 59 insertions(+), 28 deletions(-) diff --git a/packages/react/src/components/checkout/checkout.tsx b/packages/react/src/components/checkout/checkout.tsx index 4bca02ec..9fe9fc2f 100644 --- a/packages/react/src/components/checkout/checkout.tsx +++ b/packages/react/src/components/checkout/checkout.tsx @@ -262,6 +262,7 @@ export function Checkout(props: CheckoutProps) { const enableBillingAddressCollection = session?.enableBillingAddressCollection !== false; + const enableShipping = session?.enableShipping !== false; return extendedSchema.superRefine((data, ctx) => { if (data.billingPhone) { @@ -288,11 +289,17 @@ export function Checkout(props: CheckoutProps) { // BUT skip for free orders (paymentMethod === 'offline') const isFreeOrder = data.paymentMethod === PaymentMethodType.OFFLINE; const isPickup = data.deliveryMethod === DeliveryMethods.PICKUP; + const isShipping = data.deliveryMethod === DeliveryMethods.SHIP; const isFreePickup = isFreeOrder && isPickup; + // Billing is separate from shipping when shipping is disabled, the order + // has no shipping fulfillment (PICKUP, PURCHASE / all-NONE items), or the + // user opted out of "use shipping address as billing". + const billingIsSeparateFromShipping = + !enableShipping || !isShipping || !data.paymentUseShippingAddress; + const requireBillingNamesOnly = - (!enableBillingAddressCollection && - (!data.paymentUseShippingAddress || isPickup)) || + (!enableBillingAddressCollection && billingIsSeparateFromShipping) || isFreePickup; if (requireBillingNamesOnly) { @@ -315,7 +322,7 @@ export function Checkout(props: CheckoutProps) { const requireBillingAddress = enableBillingAddressCollection && !isFreePickup && - (!data.paymentUseShippingAddress || isPickup); + billingIsSeparateFromShipping; if (requireBillingAddress) { // Basic billing fields required for all countries @@ -385,7 +392,12 @@ export function Checkout(props: CheckoutProps) { } } }); - }, [checkoutFormSchema, session?.enableBillingAddressCollection, t]); + }, [ + checkoutFormSchema, + session?.enableBillingAddressCollection, + session?.enableShipping, + t, + ]); const requiredFields = React.useMemo(() => { return getRequiredFieldsFromSchema(formSchema); diff --git a/packages/react/src/components/checkout/form/checkout-form-container.tsx b/packages/react/src/components/checkout/form/checkout-form-container.tsx index 1a9c8b32..95d62adb 100644 --- a/packages/react/src/components/checkout/form/checkout-form-container.tsx +++ b/packages/react/src/components/checkout/form/checkout-form-container.tsx @@ -61,9 +61,7 @@ export function CheckoutFormContainer({ if (props.isLoading || draftOrderQuery.isLoading || isLoadingJWT) { return ( - props.loadingFallback ?? ( - - ) + props.loadingFallback ?? ); } diff --git a/packages/react/src/components/checkout/form/custom-form-provider.tsx b/packages/react/src/components/checkout/form/custom-form-provider.tsx index 4602126c..7e0ead41 100644 --- a/packages/react/src/components/checkout/form/custom-form-provider.tsx +++ b/packages/react/src/components/checkout/form/custom-form-provider.tsx @@ -71,8 +71,11 @@ export function CustomFormProvider< fieldName === 'billingFirstName' || fieldName === 'billingLastName' ); - } else if (paymentUseShippingAddress && !isPickup) { - /* If using shipping address for billing (and not pickup), filter out billing-related field validations */ + } else if (paymentUseShippingAddress && isShipping) { + /* If using shipping address for billing, filter out billing-related field validations. + * We require isShipping (not just !isPickup) so that PURCHASE / all-NONE + * fulfillment orders, or sessions with enableShipping: false, still validate + * billing fields — there's no shipping address to copy from in those cases. */ fieldNames = fieldNames.filter( fieldName => !fieldName.startsWith('billing') ); diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index 92d809ca..e6cbdb07 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -272,16 +272,23 @@ export function PaymentForm( }); }, [session, pazeSupported, applePaySupported, googlePaySupported]); + // Billing is separate from shipping when: + // - shipping is disabled at the session level, or + // - the order has no shipping fulfillment (e.g. PICKUP, PURCHASE / all-NONE items), or + // - the user opted out of "use shipping address as billing". + const billingIsSeparateFromShipping = + !session?.enableShipping || !isShipping || !useShippingAddress; + const shouldShowBillingNamesOnly = !isPaymentMethodWithInlineBilling && session?.enableBillingAddressCollection === false && - (!useShippingAddress || isPickup); + billingIsSeparateFromShipping; const isBillingAddressRequired = - shouldShowBillingNamesOnly || - (session?.enableBillingAddressCollection && - (!useShippingAddress || isPickup) && - !isPaymentMethodWithInlineBilling); + !isPaymentMethodWithInlineBilling && + billingIsSeparateFromShipping && + (shouldShowBillingNamesOnly || + session?.enableBillingAddressCollection !== false); const billingCopy = shouldShowBillingNamesOnly && t.payment.billingInformation diff --git a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx index 3bfcdc86..5ecb5dc0 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx @@ -34,19 +34,24 @@ export function GoDaddyACHForm() { const useShippingAddress = form.watch('paymentUseShippingAddress'); const deliveryMethod = form.watch('deliveryMethod'); const isShipping = deliveryMethod === DeliveryMethods.SHIP; - const isPickup = deliveryMethod === DeliveryMethods.PICKUP; + + // Billing is separate from shipping when: + // - shipping is disabled at the session level, or + // - the order has no shipping fulfillment (e.g. PICKUP, PURCHASE / all-NONE items), or + // - the user opted out of "use shipping address as billing". + const billingIsSeparateFromShipping = + !session?.enableShipping || !isShipping || !useShippingAddress; const shouldShowBillingNamesOnly = paymentMethod === PaymentMethodType.ACH && session?.enableBillingAddressCollection === false && - (!useShippingAddress || isPickup); + billingIsSeparateFromShipping; const isBillingAddressRequired = - !session?.enableShipping || - shouldShowBillingNamesOnly || - (session?.enableBillingAddressCollection && - (!useShippingAddress || isPickup) && - paymentMethod === PaymentMethodType.ACH); + paymentMethod === PaymentMethodType.ACH && + billingIsSeparateFromShipping && + (shouldShowBillingNamesOnly || + session?.enableBillingAddressCollection !== false); const billingCopy = shouldShowBillingNamesOnly && t.payment.billingInformation diff --git a/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx b/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx index 5eb7a9ab..c51d3120 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx @@ -18,18 +18,24 @@ export function CreditCardContainer({ children }: { children?: ReactNode }) { const useShippingAddress = form.watch('paymentUseShippingAddress'); const deliveryMethod = form.watch('deliveryMethod'); const isShipping = deliveryMethod === DeliveryMethods.SHIP; - const isPickup = deliveryMethod === DeliveryMethods.PICKUP; + + // Billing is separate from shipping when: + // - shipping is disabled at the session level, or + // - the order has no shipping fulfillment (e.g. PICKUP, PURCHASE / all-NONE items), or + // - the user opted out of "use shipping address as billing". + const billingIsSeparateFromShipping = + !session?.enableShipping || !isShipping || !useShippingAddress; + const shouldShowBillingNamesOnly = paymentMethod === PaymentMethodType.CREDIT_CARD && session?.enableBillingAddressCollection === false && - (!useShippingAddress || isPickup); + billingIsSeparateFromShipping; const isBillingAddressRequired = - !session?.enableShipping || - shouldShowBillingNamesOnly || - (session?.enableBillingAddressCollection && - (!useShippingAddress || isPickup) && - paymentMethod === PaymentMethodType.CREDIT_CARD); + paymentMethod === PaymentMethodType.CREDIT_CARD && + billingIsSeparateFromShipping && + (shouldShowBillingNamesOnly || + session?.enableBillingAddressCollection !== false); const billingCopy = shouldShowBillingNamesOnly && t.payment.billingInformation From 65da8571b0c1c2e6811274dea6db2ec0d486837f Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Thu, 21 May 2026 19:06:57 -0500 Subject: [PATCH 14/14] PURCHASE mode if enableShipping and enableLocalPickup false --- .../src/components/checkout/checkout.tsx | 16 ++++-- .../checkout/form/checkout-form-container.tsx | 10 +++- .../checkout-buttons/express/godaddy.tsx | 2 + .../checkout/payment/payment-form.tsx | 13 ++--- .../payment/payment-methods/ach/godaddy.tsx | 13 ++--- .../payment-methods/credit-card/container.tsx | 13 ++--- .../checkout/utils/checkout-transformers.ts | 51 ++++++++++++++----- 7 files changed, 81 insertions(+), 37 deletions(-) diff --git a/packages/react/src/components/checkout/checkout.tsx b/packages/react/src/components/checkout/checkout.tsx index 9fe9fc2f..984b2b04 100644 --- a/packages/react/src/components/checkout/checkout.tsx +++ b/packages/react/src/components/checkout/checkout.tsx @@ -292,11 +292,13 @@ export function Checkout(props: CheckoutProps) { const isShipping = data.deliveryMethod === DeliveryMethods.SHIP; const isFreePickup = isFreeOrder && isPickup; - // Billing is separate from shipping when shipping is disabled, the order - // has no shipping fulfillment (PICKUP, PURCHASE / all-NONE items), or the - // user opted out of "use shipping address as billing". + // Billing is separate from shipping when there is no shipping address + // to copy from. `mapOrderToFormValues` canonicalizes deliveryMethod + // against session capabilities, so `!isShipping` already covers both + // session.enableShipping=false and orders with no SHIP fulfillment. + // The remaining case is the user opting out of "use shipping for billing". const billingIsSeparateFromShipping = - !enableShipping || !isShipping || !data.paymentUseShippingAddress; + !isShipping || !data.paymentUseShippingAddress; const requireBillingNamesOnly = (!enableBillingAddressCollection && billingIsSeparateFromShipping) || @@ -357,8 +359,12 @@ export function Checkout(props: CheckoutProps) { } // Shipping address validation - only required if delivery method is SHIP + // AND shipping is enabled at the session level. This guards against the + // contradictory case where line items declare SHIP fulfillment but the + // session has enableShipping: false (the shipping form is not rendered + // in that case, so requiring the fields would block the user). const requireShippingAddress = - data.deliveryMethod === DeliveryMethods.SHIP; + data.deliveryMethod === DeliveryMethods.SHIP && enableShipping; if (requireShippingAddress) { // Basic shipping fields required for all countries diff --git a/packages/react/src/components/checkout/form/checkout-form-container.tsx b/packages/react/src/components/checkout/form/checkout-form-container.tsx index 95d62adb..3fb5271b 100644 --- a/packages/react/src/components/checkout/form/checkout-form-container.tsx +++ b/packages/react/src/components/checkout/form/checkout-form-container.tsx @@ -46,9 +46,17 @@ export function CheckoutFormContainer({ order, defaultValues: props.defaultValues, defaultCountryCode: session?.shipping?.originAddress?.countryCode, + enableShipping: session?.enableShipping, + enableLocalPickup: session?.enableLocalPickup, }), }), - [order, props.defaultValues, session?.shipping?.originAddress?.countryCode] + [ + order, + props.defaultValues, + session?.shipping?.originAddress?.countryCode, + session?.enableShipping, + session?.enableLocalPickup, + ] ); if (!isConfirmingCheckout && !draftOrderQuery.isLoading && !order) { diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx index 17d79001..1fad71e2 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx @@ -575,6 +575,8 @@ export function ExpressCheckoutButton() { mapOrderToFormValues({ order: draftOrder, defaultCountryCode: session?.shipping?.originAddress?.countryCode, + enableShipping: session?.enableShipping, + enableLocalPickup: session?.enableLocalPickup, }) ); diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index e6cbdb07..54237224 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -272,12 +272,13 @@ export function PaymentForm( }); }, [session, pazeSupported, applePaySupported, googlePaySupported]); - // Billing is separate from shipping when: - // - shipping is disabled at the session level, or - // - the order has no shipping fulfillment (e.g. PICKUP, PURCHASE / all-NONE items), or - // - the user opted out of "use shipping address as billing". - const billingIsSeparateFromShipping = - !session?.enableShipping || !isShipping || !useShippingAddress; + // Billing is separate from shipping when there is no shipping address to + // copy from. `mapOrderToFormValues` canonicalizes deliveryMethod against + // session capabilities, so `!isShipping` already covers: + // - session.enableShipping = false + // - line items have no SHIP fulfillment (PICKUP / PURCHASE / all-NONE) + // The remaining case is the user opting out of "use shipping for billing". + const billingIsSeparateFromShipping = !isShipping || !useShippingAddress; const shouldShowBillingNamesOnly = !isPaymentMethodWithInlineBilling && diff --git a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx index 5ecb5dc0..812081e8 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx @@ -35,12 +35,13 @@ export function GoDaddyACHForm() { const deliveryMethod = form.watch('deliveryMethod'); const isShipping = deliveryMethod === DeliveryMethods.SHIP; - // Billing is separate from shipping when: - // - shipping is disabled at the session level, or - // - the order has no shipping fulfillment (e.g. PICKUP, PURCHASE / all-NONE items), or - // - the user opted out of "use shipping address as billing". - const billingIsSeparateFromShipping = - !session?.enableShipping || !isShipping || !useShippingAddress; + // Billing is separate from shipping when there is no shipping address to + // copy from. `mapOrderToFormValues` canonicalizes deliveryMethod against + // session capabilities, so `!isShipping` already covers: + // - session.enableShipping = false + // - line items have no SHIP fulfillment (PICKUP / PURCHASE / all-NONE) + // The remaining case is the user opting out of "use shipping for billing". + const billingIsSeparateFromShipping = !isShipping || !useShippingAddress; const shouldShowBillingNamesOnly = paymentMethod === PaymentMethodType.ACH && diff --git a/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx b/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx index c51d3120..76394eba 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx @@ -19,12 +19,13 @@ export function CreditCardContainer({ children }: { children?: ReactNode }) { const deliveryMethod = form.watch('deliveryMethod'); const isShipping = deliveryMethod === DeliveryMethods.SHIP; - // Billing is separate from shipping when: - // - shipping is disabled at the session level, or - // - the order has no shipping fulfillment (e.g. PICKUP, PURCHASE / all-NONE items), or - // - the user opted out of "use shipping address as billing". - const billingIsSeparateFromShipping = - !session?.enableShipping || !isShipping || !useShippingAddress; + // Billing is separate from shipping when there is no shipping address to + // copy from. `mapOrderToFormValues` canonicalizes deliveryMethod against + // session capabilities, so `!isShipping` already covers: + // - session.enableShipping = false + // - line items have no SHIP fulfillment (PICKUP / PURCHASE / all-NONE) + // The remaining case is the user opting out of "use shipping for billing". + const billingIsSeparateFromShipping = !isShipping || !useShippingAddress; const shouldShowBillingNamesOnly = paymentMethod === PaymentMethodType.CREDIT_CARD && diff --git a/packages/react/src/components/checkout/utils/checkout-transformers.ts b/packages/react/src/components/checkout/utils/checkout-transformers.ts index d7d36314..7a0d5223 100644 --- a/packages/react/src/components/checkout/utils/checkout-transformers.ts +++ b/packages/react/src/components/checkout/utils/checkout-transformers.ts @@ -43,31 +43,56 @@ export function mapOrderToFormValues({ order, defaultValues, defaultCountryCode, + enableShipping, + enableLocalPickup, }: { order?: DraftOrder | null; defaultValues?: Pick; defaultCountryCode?: string | null; + /** + * Session-level capability flags. When provided, the derived + * `deliveryMethod` will fall back to `PURCHASE` for any item-level + * fulfillment mode that the session has disabled. This avoids the + * contradictory state where line items declare SHIP/PICKUP but the + * session disables those flows (so `DeliveryMethodForm` is not even + * rendered to correct the value). Pass `undefined` to skip the gate. + */ + enableShipping?: boolean | null; + enableLocalPickup?: boolean | null; }): CheckoutFormData { const orderShippingAddress = order?.shipping?.address; const orderBillingAddress = order?.billing?.address; const paymentShouldUseShippingAddress = Boolean( orderShippingAddress?.addressLine1 === orderBillingAddress?.addressLine1 ); - const isPickup = order?.lineItems?.some( - lineItem => lineItem.fulfillmentMode === DeliveryMethods.PICKUP - ); - - const isShipping = order?.lineItems?.some( - lineItem => lineItem.fulfillmentMode === DeliveryMethods.SHIP - ); - - const isMixedFulfillment = isPickup && isShipping; + // Purchase mode = the session has neither shipping nor pickup enabled, so + // this checkout is payment-only regardless of what fulfillment modes the + // line items declare. Costs are assumed to be hardcoded on the order. + const isPurchaseMode = + enableShipping === false && enableLocalPickup === false; let deliveryMethod = DeliveryMethods.PURCHASE; - if (!isMixedFulfillment && isPickup) { - deliveryMethod = DeliveryMethods.PICKUP; - } else if (!isMixedFulfillment && isShipping) { - deliveryMethod = DeliveryMethods.SHIP; + + if (!isPurchaseMode) { + const hasPickupItem = order?.lineItems?.some( + lineItem => lineItem.fulfillmentMode === DeliveryMethods.PICKUP + ); + const hasShipItem = order?.lineItems?.some( + lineItem => lineItem.fulfillmentMode === DeliveryMethods.SHIP + ); + + // Only treat item-level fulfillment as actionable if the session allows + // that flow. When a flag is undefined we treat it as enabled so existing + // callers retain their previous behavior. + const isPickup = hasPickupItem && enableLocalPickup !== false; + const isShipping = hasShipItem && enableShipping !== false; + const isMixedFulfillment = isPickup && isShipping; + + if (!isMixedFulfillment && isPickup) { + deliveryMethod = DeliveryMethods.PICKUP; + } else if (!isMixedFulfillment && isShipping) { + deliveryMethod = DeliveryMethods.SHIP; + } } return {