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
diff --git a/examples/nextjs/app/page.tsx b/examples/nextjs/app/page.tsx
index ba3079a8..12ff2de4 100644
--- a/examples/nextjs/app/page.tsx
+++ b/examples/nextjs/app/page.tsx
@@ -58,6 +58,14 @@ export default async function Home() {
processor: 'godaddy',
checkoutTypes: ['standard'],
},
+ ach: {
+ processor: 'godaddy',
+ checkoutTypes: ['standard'],
+ },
+ express: {
+ processor: 'godaddy',
+ checkoutTypes: ['express'],
+ },
mercadopago: {
processor: 'mercadopago',
checkoutTypes: ['standard'],
diff --git a/packages/localizations/src/deDe.ts b/packages/localizations/src/deDe.ts
index 11d375a8..170119bd 100644
--- a/packages/localizations/src/deDe.ts
+++ b/packages/localizations/src/deDe.ts
@@ -108,6 +108,7 @@ export const deDe = {
offline: 'Offline-Zahlungen',
mercadopago: 'Mercado Pago',
ccavenue: 'Mit CCAvenue bezahlen',
+ ach: 'Bankkonto',
},
descriptions: {
creditCard: '',
@@ -118,6 +119,7 @@ export const deDe = {
offline: '',
mercadopago:
'Verwende das MercadoPago-Formular unten, um deinen Kauf sicher abzuschließen.',
+ ach: '',
ccavenue: '',
},
noMethodsAvailable: 'Keine Zahlungsmethoden verfügbar',
@@ -359,6 +361,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 450a28d1..9423ba31 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Pay with CCAvenue',
},
@@ -116,6 +117,7 @@ export const enIe = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Use the MercadoPago form below to complete your purchase securely.',
ccavenue: '',
@@ -335,6 +337,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 db18689c..398b6358 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Pay with CCAvenue',
},
@@ -116,6 +117,7 @@ export const enUs = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Use the MercadoPago form below to complete your purchase securely.',
ccavenue: '',
@@ -335,6 +337,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 ae62fa8e..917f77c2 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',
mercadopago: 'Mercado Pago',
ccavenue: 'الدفع عبر CCAvenue',
},
@@ -117,6 +118,7 @@ export const esAr = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.',
ccavenue: '',
@@ -342,6 +344,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 6dd7c8fa..e381d156 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Pagar con CCAvenue',
},
@@ -117,6 +118,7 @@ export const esCl = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.',
ccavenue: '',
@@ -344,6 +346,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 6fc6b6d8..3d228544 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Pagar con CCAvenue',
},
@@ -117,6 +118,7 @@ export const esCo = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.',
ccavenue: '',
@@ -342,6 +344,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 ff845f89..fdceff01 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Pagar con CCAvenue',
},
@@ -117,6 +118,7 @@ export const esEs = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.',
ccavenue: '',
@@ -347,6 +349,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 06f4698d..f4e4f045 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Pagar con CCAvenue',
},
@@ -117,6 +118,7 @@ export const esMx = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.',
ccavenue: '',
@@ -343,6 +345,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 b8c82659..0d3c9047 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Pagar con CCAvenue',
},
@@ -117,6 +118,7 @@ export const esPe = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.',
ccavenue: '',
@@ -342,6 +344,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 e4ab2386..e74068f1 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Pagar con CCAvenue',
},
@@ -117,6 +118,7 @@ export const esUs = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.',
ccavenue: '',
@@ -342,6 +344,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 d139fb4f..79d65050 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Payer avec CCAvenue',
},
@@ -117,6 +118,7 @@ export const frCa = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Utilisez le formulaire MercadoPago ci-dessous pour finaliser votre achat en toute sécurité.',
ccavenue: '',
@@ -359,6 +361,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 27702e17..b6d7aba0 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Payer avec CCAvenue',
},
@@ -117,6 +118,7 @@ export const frFr = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Utilisez le formulaire MercadoPago ci-dessous pour finaliser votre achat en toute sécurité.',
ccavenue: '',
@@ -360,6 +362,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 ebbe647d..1f75ab10 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Bayar dengan CCAvenue',
},
@@ -116,6 +117,7 @@ export const idId = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Gunakan formulir MercadoPago di bawah untuk menyelesaikan pembelian Anda dengan aman.',
ccavenue: '',
@@ -334,6 +336,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 fd98b6d3..e50584ea 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Paga con CCAvenue',
},
@@ -117,6 +118,7 @@ export const itIt = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Usa il modulo MercadoPago qui sotto per completare l’acquisto in modo sicuro.',
ccavenue: '',
@@ -358,6 +360,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 f4cb7c27..cfede5d9 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Pagar com CCAvenue',
},
@@ -116,6 +117,7 @@ export const ptBr = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Use o formulário do MercadoPago abaixo para concluir sua compra com segurança.',
ccavenue: '',
@@ -340,6 +342,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 91ad583f..0e2467f5 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ëñţ]',
mercadopago: 'Mercado Pago',
ccavenue: '[Þâÿ ïñ ÇÇÂvëñûë]',
},
@@ -117,6 +118,7 @@ export const qaPs = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'[Üšë ţhë MërçâðöÞâgö förm këlöw ţö çömþlëţë ÿöür þürçhâšë šëçürëlÿ.]',
ccavenue: '',
@@ -344,6 +346,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 339867c4..793dc9ae 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ı',
mercadopago: 'Mercado Pago',
ccavenue: 'CCAvenue ile öde',
},
@@ -116,6 +117,7 @@ export const trTr = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Satın alımınızı güvenle tamamlamak için aşağıdaki MercadoPago formunu kullanın.',
ccavenue: '',
@@ -335,6 +337,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 a496cdc0..f3bcf6fa 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',
mercadopago: 'Mercado Pago',
ccavenue: 'Thanh toán bằng CCAvenue',
},
@@ -116,6 +117,7 @@ export const viVn = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago:
'Hãy sử dụng biểu mẫu MercadoPago bên dưới để hoàn tất mua hàng một cách an toàn.',
ccavenue: '',
@@ -335,6 +337,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 d5c0e2a2..c17bb453 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: '银行账户',
mercadopago: 'Mercado Pago',
ccavenue: '使用 CCAvenue 支付',
},
@@ -112,6 +113,7 @@ export const zhCn = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago: '请使用下方的 MercadoPago 表单安全完成购买。',
ccavenue: '',
},
@@ -323,6 +325,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 de14b637..2fc8a914 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: '银行账户',
mercadopago: 'Mercado Pago',
ccavenue: '使用 CCAvenue 支付',
},
@@ -112,6 +113,7 @@ export const zhSg = {
googlePay: '',
paze: '',
offline: '',
+ ach: '',
mercadopago: '请使用下方的 MercadoPago 表单安全完成购买。',
ccavenue: '',
},
@@ -323,6 +325,7 @@ export const zhSg = {
BILLING_ADDRESS_VERIFICATION_FAILED: '账单地址验证失败',
MISSING_SHIPPING_INFO: '配送地址或方式应用失败',
DEPENDENCY_ERROR: '我們目前無法處理您的訂單。請稍等片刻再試',
+ AUTHORIZATION_FAILED: '付款授权失败',
},
storefront: {
product: '产品',
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/payment/checkout-buttons/ach/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/ach/godaddy.tsx
new file mode 100644
index 00000000..f70b7cf9
--- /dev/null
+++ b/packages/react/src/components/checkout/payment/checkout-buttons/ach/godaddy.tsx
@@ -0,0 +1,44 @@
+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 () => {
+ 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/express/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx
index a5c823f1..15b29965 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,
@@ -63,6 +64,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
);
@@ -510,7 +512,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',
@@ -1456,7 +1458,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);
// }
// };
}, [
@@ -1486,7 +1488,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/lazy-payment-loader.tsx b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx
index 6edadc0c..eb270c94 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,24 @@ 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,
+ })
+ )
+ ),
+
MercadoPagoCheckoutButton: lazy(() =>
import(
'@/components/checkout/payment/checkout-buttons/mercadopago/mercadopago'
@@ -190,6 +208,12 @@ type PaymentComponentRegistry = {
button: PaymentComponentKey;
};
};
+ [PaymentMethodType.ACH]?: {
+ [PaymentProvider.GODADDY]: {
+ form: PaymentComponentKey;
+ button: PaymentComponentKey;
+ };
+ };
[PaymentMethodType.MERCADOPAGO]?: {
[PaymentProvider.MERCADOPAGO]: {
button: PaymentComponentKey;
@@ -254,6 +278,12 @@ export const lazyPaymentComponentRegistry: PaymentComponentRegistry = {
button: 'MercadoPagoCheckoutButton',
},
},
+ [PaymentMethodType.ACH]: {
+ [PaymentProvider.GODADDY]: {
+ form: 'GoDaddyACHForm',
+ button: 'ACHCheckoutButton',
+ },
+ },
[PaymentMethodType.CCAVENUE]: {
[PaymentProvider.CCAVENUE]: {
button: 'CCAvenueCheckoutButton',
diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx
index 23fa3ca4..75b58cf3 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,
@@ -64,6 +70,7 @@ import {
// UI config for payment methods (labels will be resolved from translations)
const PAYMENT_METHOD_ICONS: Record
= {
card: ,
+ ach: ,
paypal: ,
applePay: ,
googlePay: ,
@@ -108,6 +115,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:
@@ -135,6 +144,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..bf8c0472
--- /dev/null
+++ b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx
@@ -0,0 +1,220 @@
+import { useId, 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 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"';
+
+ 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(elementId, 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/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}
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 2d381b1a..65c42898 100644
--- a/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx
+++ b/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx
@@ -2,6 +2,7 @@ import { PayPalScriptProvider } from '@paypal/react-paypal-js';
import { useCheckoutContext } from '@/components/checkout/checkout';
import { CCAvenueReturnProvider } from './ccavenue-return-provider';
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';
@@ -24,6 +25,7 @@ export function ConditionalPaymentProviders({
squareConfig,
paypalConfig,
ccavenueConfig,
+ session,
} = useCheckoutContext();
const { payPalRequest } = useBuildPaymentRequest();
@@ -35,6 +37,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/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/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 (
+ );
+}
+
+/**
+ * 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 = (
+
+ {symbol}
+
+ );
+
+ 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));
}
diff --git a/packages/react/src/lib/godaddy/checkout-env.ts b/packages/react/src/lib/godaddy/checkout-env.ts
index c0c4afc2..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: {
@@ -3072,6 +3081,13 @@ const introspection = {
kind: 'INPUT_OBJECT',
name: 'CheckoutSessionPaymentMethodsInput',
inputFields: [
+ {
+ name: 'ach',
+ type: {
+ kind: 'INPUT_OBJECT',
+ name: 'CheckoutSessionPaymentMethodConfigInput',
+ },
+ },
{
name: 'applePay',
type: {
@@ -3108,28 +3124,28 @@ const introspection = {
},
},
{
- name: 'offline',
+ name: 'mercadopago',
type: {
kind: 'INPUT_OBJECT',
name: 'CheckoutSessionPaymentMethodConfigInput',
},
},
{
- name: 'paypal',
+ name: 'offline',
type: {
kind: 'INPUT_OBJECT',
name: 'CheckoutSessionPaymentMethodConfigInput',
},
},
{
- name: 'paze',
+ name: 'paypal',
type: {
kind: 'INPUT_OBJECT',
name: 'CheckoutSessionPaymentMethodConfigInput',
},
},
{
- name: 'mercadopago',
+ name: 'paze',
type: {
kind: 'INPUT_OBJECT',
name: 'CheckoutSessionPaymentMethodConfigInput',
diff --git a/packages/react/src/lib/godaddy/checkout-mutations.ts b/packages/react/src/lib/godaddy/checkout-mutations.ts
index e3e77f19..4c6bb6a9 100644
--- a/packages/react/src/lib/godaddy/checkout-mutations.ts
+++ b/packages/react/src/lib/godaddy/checkout-mutations.ts
@@ -113,6 +113,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 e463a8c3..f6f3bb54 100644
--- a/packages/react/src/lib/godaddy/checkout-queries.ts
+++ b/packages/react/src/lib/godaddy/checkout-queries.ts
@@ -109,6 +109,10 @@ export const GetCheckoutSessionQuery = graphql(`
processor
checkoutTypes
}
+ ach {
+ processor
+ checkoutTypes
+ }
mercadopago {
processor
checkoutTypes
diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts
index 999d2266..a2a06376 100644
--- a/packages/react/src/types.ts
+++ b/packages/react/src/types.ts
@@ -58,6 +58,7 @@ export type AvailablePaymentProviders =
export const PaymentMethodType = {
CREDIT_CARD: 'card',
+ ACH: 'ach',
EXPRESS: 'express',
PAYPAL: 'paypal',
APPLE_PAY: 'applePay',