From 6a07e7ca57aec1afb22902cd72273da340d5c360 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Fri, 13 Feb 2026 10:38:05 -0600 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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