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 fef69ee0..d9871810 100644 --- a/packages/localizations/src/deDe.ts +++ b/packages/localizations/src/deDe.ts @@ -112,6 +112,7 @@ export const deDe = { offline: 'Offline-Zahlungen', mercadopago: 'Mercado Pago', ccavenue: 'Mit CCAvenue bezahlen', + ach: 'Bankkonto', }, descriptions: { creditCard: '', @@ -122,6 +123,7 @@ export const deDe = { offline: '', mercadopago: 'Verwende das MercadoPago-Formular unten, um deinen Kauf sicher abzuschließen.', + ach: '', ccavenue: '', }, noMethodsAvailable: 'Keine Zahlungsmethoden verfügbar', @@ -166,6 +168,7 @@ export const deDe = { shipping: 'Versand', tip: 'Trinkgeld', estimatedTaxes: 'Geschätzte Steuern', + fees: 'Gebühren', totalDue: 'Gesamtbetrag', orderSummary: 'Bestellübersicht', itemCount: 'Artikel', @@ -363,6 +366,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/enAu.ts b/packages/localizations/src/enAu.ts index 68fbe932..cf205cb9 100644 --- a/packages/localizations/src/enAu.ts +++ b/packages/localizations/src/enAu.ts @@ -110,6 +110,7 @@ export const enAu = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Offline payments', + ach: 'Bank Account', mercadopago: 'Mercado Pago', ccavenue: 'Pay with CCAvenue', }, @@ -120,6 +121,7 @@ export const enAu = { googlePay: '', paze: '', offline: '', + ach: '', mercadopago: 'Use the MercadoPago form below to complete your purchase securely.', ccavenue: '', @@ -166,6 +168,7 @@ export const enAu = { shipping: 'Shipping', tip: 'Tip', estimatedTaxes: 'Estimated GST', + fees: 'Fees', totalDue: 'Total Due', orderSummary: 'Order Summary', itemCount: 'items', @@ -339,6 +342,7 @@ export const enAu = { MISSING_SHIPPING_INFO: 'Shipping address or method failed to apply', DEPENDENCY_ERROR: "We're unable to process your order right now. Please wait a moment and try again", + AUTHORIZATION_FAILED: 'Failed to authorise payment', }, storefront: { product: 'Product', diff --git a/packages/localizations/src/enIe.ts b/packages/localizations/src/enIe.ts index 68ba1989..881fd736 100644 --- a/packages/localizations/src/enIe.ts +++ b/packages/localizations/src/enIe.ts @@ -110,6 +110,7 @@ export const enIe = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Offline payments', + ach: 'Bank Account', mercadopago: 'Mercado Pago', ccavenue: 'Pay with CCAvenue', }, @@ -120,6 +121,7 @@ export const enIe = { googlePay: '', paze: '', offline: '', + ach: '', mercadopago: 'Use the MercadoPago form below to complete your purchase securely.', ccavenue: '', @@ -166,6 +168,7 @@ export const enIe = { shipping: 'Shipping', tip: 'Tip', estimatedTaxes: 'Estimated VAT', + fees: 'Fees', totalDue: 'Total Due', orderSummary: 'Order Summary', itemCount: 'items', @@ -339,6 +342,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 e277395d..f485b21c 100644 --- a/packages/localizations/src/enUs.ts +++ b/packages/localizations/src/enUs.ts @@ -110,6 +110,7 @@ export const enUs = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Offline payments', + ach: 'Bank Account', mercadopago: 'Mercado Pago', ccavenue: 'Pay with CCAvenue', }, @@ -120,6 +121,7 @@ export const enUs = { googlePay: '', paze: '', offline: '', + ach: '', mercadopago: 'Use the MercadoPago form below to complete your purchase securely.', ccavenue: '', @@ -166,6 +168,7 @@ export const enUs = { shipping: 'Shipping', tip: 'Tip', estimatedTaxes: 'Estimated taxes', + fees: 'Fees', totalDue: 'Total Due', orderSummary: 'Order Summary', itemCount: 'items', @@ -339,6 +342,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 f33b6b42..9cfd4cdd 100644 --- a/packages/localizations/src/esAr.ts +++ b/packages/localizations/src/esAr.ts @@ -111,6 +111,7 @@ export const esAr = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos en efectivo', + ach: 'Cuenta Bancaria', mercadopago: 'Mercado Pago', ccavenue: 'الدفع عبر CCAvenue', }, @@ -121,6 +122,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: '', @@ -167,6 +169,7 @@ export const esAr = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a Pagar', orderSummary: 'Resumen del Pedido', itemCount: 'productos', @@ -346,6 +349,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 63f0e714..d7defa1b 100644 --- a/packages/localizations/src/esCl.ts +++ b/packages/localizations/src/esCl.ts @@ -111,6 +111,7 @@ export const esCl = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos offline', + ach: 'Cuenta Bancaria', mercadopago: 'Mercado Pago', ccavenue: 'Pagar con CCAvenue', }, @@ -121,6 +122,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: '', @@ -167,6 +169,7 @@ export const esCl = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a Pagar', orderSummary: 'Resumen del Pedido', itemCount: 'artículos', @@ -348,6 +351,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 e2600cac..ac8d83a7 100644 --- a/packages/localizations/src/esCo.ts +++ b/packages/localizations/src/esCo.ts @@ -111,6 +111,7 @@ export const esCo = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos sin conexión', + ach: 'Cuenta bancaria', mercadopago: 'Mercado Pago', ccavenue: 'Pagar con CCAvenue', }, @@ -121,6 +122,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: '', @@ -167,6 +169,7 @@ export const esCo = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a pagar', orderSummary: 'Resumen del pedido', itemCount: 'artículos', @@ -346,6 +349,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 7a69fddd..07804024 100644 --- a/packages/localizations/src/esEs.ts +++ b/packages/localizations/src/esEs.ts @@ -111,6 +111,7 @@ export const esEs = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos sin conexión', + ach: 'Cuenta bancaria', mercadopago: 'Mercado Pago', ccavenue: 'Pagar con CCAvenue', }, @@ -121,6 +122,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: '', @@ -167,6 +169,7 @@ export const esEs = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a pagar', orderSummary: 'Resumen del pedido', itemCount: 'artículos', @@ -351,6 +354,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 6921355f..a1d49236 100644 --- a/packages/localizations/src/esMx.ts +++ b/packages/localizations/src/esMx.ts @@ -111,6 +111,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', }, @@ -121,6 +122,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: '', @@ -167,6 +169,7 @@ export const esMx = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a Pagar', orderSummary: 'Resumen del Pedido', itemCount: 'artículos', @@ -347,6 +350,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 db814e89..88dfcfcb 100644 --- a/packages/localizations/src/esPe.ts +++ b/packages/localizations/src/esPe.ts @@ -111,6 +111,7 @@ export const esPe = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos en efectivo', + ach: 'Cuenta Bancaria', mercadopago: 'Mercado Pago', ccavenue: 'Pagar con CCAvenue', }, @@ -121,6 +122,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: '', @@ -167,6 +169,7 @@ export const esPe = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a Pagar', orderSummary: 'Resumen del Pedido', itemCount: 'productos', @@ -346,6 +349,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 ec9e146f..a951a673 100644 --- a/packages/localizations/src/esUs.ts +++ b/packages/localizations/src/esUs.ts @@ -111,6 +111,7 @@ export const esUs = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagos offline', + ach: 'Cuenta Bancaria', mercadopago: 'Mercado Pago', ccavenue: 'Pagar con CCAvenue', }, @@ -121,6 +122,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: '', @@ -167,6 +169,7 @@ export const esUs = { shipping: 'Envío', tip: 'Propina', estimatedTaxes: 'Impuestos estimados', + fees: 'Cargos', totalDue: 'Total a Pagar', orderSummary: 'Resumen del Pedido', itemCount: 'artículos', @@ -346,6 +349,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 a1f95e85..5268a2d9 100644 --- a/packages/localizations/src/frCa.ts +++ b/packages/localizations/src/frCa.ts @@ -111,6 +111,7 @@ export const frCa = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Paiements hors ligne', + ach: 'Compte bancaire', mercadopago: 'Mercado Pago', ccavenue: 'Payer avec CCAvenue', }, @@ -121,6 +122,7 @@ export const frCa = { googlePay: '', paze: '', offline: '', + ach: '', mercadopago: 'Utilisez le formulaire MercadoPago ci-dessous pour finaliser votre achat en toute sécurité.', ccavenue: '', @@ -167,6 +169,7 @@ export const frCa = { shipping: 'Expédition', tip: 'Pourboire', estimatedTaxes: 'Taxes estimées', + fees: 'Frais', totalDue: 'Total à payer', orderSummary: 'Résumé de la commande', itemCount: 'articles', @@ -363,6 +366,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 ad94eef2..c59d74a4 100644 --- a/packages/localizations/src/frFr.ts +++ b/packages/localizations/src/frFr.ts @@ -111,6 +111,7 @@ export const frFr = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Paiements hors ligne', + ach: 'Compte bancaire', mercadopago: 'Mercado Pago', ccavenue: 'Payer avec CCAvenue', }, @@ -121,6 +122,7 @@ export const frFr = { googlePay: '', paze: '', offline: '', + ach: '', mercadopago: 'Utilisez le formulaire MercadoPago ci-dessous pour finaliser votre achat en toute sécurité.', ccavenue: '', @@ -167,6 +169,7 @@ export const frFr = { shipping: 'Expédition', tip: 'Pourboire', estimatedTaxes: 'Taxes estimées', + fees: 'Frais', totalDue: 'Total à payer', orderSummary: 'Récapitulatif de la commande', itemCount: 'articles', @@ -364,6 +367,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 09b61197..948417cb 100644 --- a/packages/localizations/src/idId.ts +++ b/packages/localizations/src/idId.ts @@ -110,6 +110,7 @@ export const idId = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pembayaran offline', + ach: 'Rekening Bank', mercadopago: 'Mercado Pago', ccavenue: 'Bayar dengan CCAvenue', }, @@ -120,6 +121,7 @@ export const idId = { googlePay: '', paze: '', offline: '', + ach: '', mercadopago: 'Gunakan formulir MercadoPago di bawah untuk menyelesaikan pembelian Anda dengan aman.', ccavenue: '', @@ -166,6 +168,7 @@ export const idId = { shipping: 'Pengiriman', tip: 'Tip', estimatedTaxes: 'Perkiraan pajak', + fees: 'Biaya', totalDue: 'Total Pembayaran', orderSummary: 'Ringkasan Pesanan', itemCount: 'item', @@ -338,6 +341,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 85d5e0a2..0e735589 100644 --- a/packages/localizations/src/itIt.ts +++ b/packages/localizations/src/itIt.ts @@ -111,6 +111,7 @@ export const itIt = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagamenti offline', + ach: 'Conto Bancario', mercadopago: 'Mercado Pago', ccavenue: 'Paga con CCAvenue', }, @@ -121,6 +122,7 @@ export const itIt = { googlePay: '', paze: '', offline: '', + ach: '', mercadopago: 'Usa il modulo MercadoPago qui sotto per completare l’acquisto in modo sicuro.', ccavenue: '', @@ -167,6 +169,7 @@ export const itIt = { shipping: 'Spedizione', tip: 'Mancia', estimatedTaxes: 'Tasse stimate', + fees: 'Commissioni', totalDue: 'Totale Dovuto', orderSummary: 'Riepilogo Ordine', itemCount: 'articoli', @@ -362,6 +365,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 57048a4a..dcf0eb34 100644 --- a/packages/localizations/src/ptBr.ts +++ b/packages/localizations/src/ptBr.ts @@ -110,6 +110,7 @@ export const ptBr = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Pagamentos offline', + ach: 'Conta Bancária', mercadopago: 'Mercado Pago', ccavenue: 'Pagar com CCAvenue', }, @@ -120,6 +121,7 @@ export const ptBr = { googlePay: '', paze: '', offline: '', + ach: '', mercadopago: 'Use o formulário do MercadoPago abaixo para concluir sua compra com segurança.', ccavenue: '', @@ -166,6 +168,7 @@ export const ptBr = { shipping: 'Envio', tip: 'Gorjeta', estimatedTaxes: 'Impostos estimados', + fees: 'Taxas', totalDue: 'Total a Pagar', orderSummary: 'Resumo do Pedido', itemCount: 'itens', @@ -344,6 +347,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 6e99184c..28e5a478 100644 --- a/packages/localizations/src/qaPs.ts +++ b/packages/localizations/src/qaPs.ts @@ -111,6 +111,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ëñûë]', }, @@ -121,6 +122,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: '', @@ -167,6 +169,7 @@ export const qaPs = { shipping: '[Šhîþþîñg Çöšţš]', tip: '[Ţîþ Âmöüñţ]', estimatedTaxes: '[Ëšţîmâţëd ţâxëš çâlçülâţîöñ]', + fees: '[Fëëš Çhârgëd]', totalDue: '[Tötâl Âmöüñţ Düë]', orderSummary: '[Ördër Šümmârÿ Dëţâîlš]', itemCount: '[îţëmš îñ çârţ]', @@ -348,6 +351,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 fed8b55e..97427426 100644 --- a/packages/localizations/src/trTr.ts +++ b/packages/localizations/src/trTr.ts @@ -110,6 +110,7 @@ export const trTr = { googlePay: 'Google Pay', paze: 'Paze', offline: 'Çevrimdışı ödemeler', + ach: 'Banka Hesabı', mercadopago: 'Mercado Pago', ccavenue: 'CCAvenue ile öde', }, @@ -120,6 +121,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: '', @@ -166,6 +168,7 @@ export const trTr = { shipping: 'Kargo', tip: 'Bahşiş', estimatedTaxes: 'Tahmini vergiler', + fees: 'Ücretler', totalDue: 'Ödenecek Toplam', orderSummary: 'Sipariş Özeti', itemCount: 'ürün', @@ -339,6 +342,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 474b874b..b838a6cd 100644 --- a/packages/localizations/src/viVn.ts +++ b/packages/localizations/src/viVn.ts @@ -110,6 +110,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', }, @@ -120,6 +121,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: '', @@ -166,6 +168,7 @@ export const viVn = { shipping: 'Vận chuyển', tip: 'Tip', estimatedTaxes: 'Thuế ước tính', + fees: 'Phí', totalDue: 'Tổng cộng', orderSummary: 'Tóm tắt đơn hàng', itemCount: 'sản phẩm', @@ -339,6 +342,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 d7c2403b..8feb4056 100644 --- a/packages/localizations/src/zhCn.ts +++ b/packages/localizations/src/zhCn.ts @@ -106,6 +106,7 @@ export const zhCn = { googlePay: 'Google Pay', paze: 'Paze', offline: '线下付款', + ach: '银行账户', mercadopago: 'Mercado Pago', ccavenue: '使用 CCAvenue 支付', }, @@ -116,6 +117,7 @@ export const zhCn = { googlePay: '', paze: '', offline: '', + ach: '', mercadopago: '请使用下方的 MercadoPago 表单安全完成购买。', ccavenue: '', }, @@ -161,6 +163,7 @@ export const zhCn = { shipping: '配送费', tip: '小费', estimatedTaxes: '预估税费', + fees: '费用', totalDue: '应付总计', orderSummary: '订单摘要', itemCount: '件商品', @@ -327,6 +330,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 bb855e68..dbe6bb16 100644 --- a/packages/localizations/src/zhSg.ts +++ b/packages/localizations/src/zhSg.ts @@ -106,6 +106,7 @@ export const zhSg = { googlePay: 'Google Pay', paze: 'Paze', offline: '线下付款', + ach: '银行账户', mercadopago: 'Mercado Pago', ccavenue: '使用 CCAvenue 支付', }, @@ -116,6 +117,7 @@ export const zhSg = { googlePay: '', paze: '', offline: '', + ach: '', mercadopago: '请使用下方的 MercadoPago 表单安全完成购买。', ccavenue: '', }, @@ -161,6 +163,7 @@ export const zhSg = { shipping: '运费', tip: '小费', estimatedTaxes: '预估税费', + fees: '费用', totalDue: '应付总额', orderSummary: '订单摘要', itemCount: '件商品', @@ -327,6 +330,7 @@ export const zhSg = { BILLING_ADDRESS_VERIFICATION_FAILED: '账单地址验证失败', MISSING_SHIPPING_INFO: '配送地址或方式应用失败', DEPENDENCY_ERROR: '我們目前無法處理您的訂單。請稍等片刻再試', + AUTHORIZATION_FAILED: '付款授权失败', }, storefront: { product: '产品', diff --git a/packages/react/README.md b/packages/react/README.md index 2047b067..c8f578df 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -184,6 +184,23 @@ All fields are optional strings. Pass any subset to override the defaults. | `secondaryBackground` | Secondary background | | `secondaryForeground` | Text on secondary backgrounds | +### Loading State + +The `` component manages its own loading state internally — it shows a skeleton while it fetches its JWT and draft order, then reveals the form. If you need to coordinate with your own preload work (user profile, theme, feature flags, etc.), the following props let you keep the skeleton up while your data loads. + +| Prop | Type | Description | +|------|------|-------------| +| `isLoading` | `boolean` | When `true`, the checkout keeps showing its loading state even after its own internal data has finished loading. Internal queries still run in the background, so the checkout is warm and ready to render the moment you flip this back to `false`. Mount the component early and reveal late — you avoid both the layout flicker of a late-mount and the wasted time of gating the mount entirely. | +| `loadingFallback` | `ReactNode` | Optional override for the loading UI. Defaults to the built-in ``. Rendered whenever `isLoading` is `true` or the checkout is loading its own internal data. | + +```tsx +} +/> +``` + ## AI Agent Skills This package ships a [TanStack Intent](https://tanstack.com/intent/latest) skill that teaches AI coding agents how to authenticate with the GoDaddy Commerce Platform using OAuth2 client credentials and create checkout sessions. For API discovery and testing, the skill directs agents to use [`@godaddy/cli`](https://www.npmjs.com/package/@godaddy/cli). diff --git a/packages/react/src/components/checkout/address/address-form.tsx b/packages/react/src/components/checkout/address/address-form.tsx index ef8e9258..03d98d1a 100644 --- a/packages/react/src/components/checkout/address/address-form.tsx +++ b/packages/react/src/components/checkout/address/address-form.tsx @@ -58,7 +58,10 @@ interface AddressFormProps { onlyNames?: boolean; } -export function AddressForm({ sectionKey, onlyNames = false }: AddressFormProps) { +export function AddressForm({ + sectionKey, + onlyNames = false, +}: AddressFormProps) { const form = useFormContext(); const { session } = useCheckoutContext(); const { t } = useGoDaddyContext(); @@ -442,8 +445,7 @@ export function AddressForm({ sectionKey, onlyNames = false }: AddressFormProps) render={({ field, fieldState }) => ( - {t.shipping.firstName}{' '} - {!onlyNames && `(${t.general.optional})`} + {t.shipping.firstName} {!onlyNames && `(${t.general.optional})`} ReactNode>>; + targets?: Partial< + Record ReactNode> + >; checkoutFormSchema?: CheckoutFormSchema; defaultValues?: Pick; + isLoading?: boolean; + loadingFallback?: ReactNode; } export function Checkout(props: CheckoutProps) { @@ -260,6 +264,7 @@ export function Checkout(props: CheckoutProps) { const enableBillingAddressCollection = session?.enableBillingAddressCollection !== false; + const enableShipping = session?.enableShipping !== false; return extendedSchema.superRefine((data, ctx) => { if (data.billingPhone) { @@ -286,11 +291,19 @@ export function Checkout(props: CheckoutProps) { // BUT skip for free orders (paymentMethod === 'offline') const isFreeOrder = data.paymentMethod === PaymentMethodType.OFFLINE; const isPickup = data.deliveryMethod === DeliveryMethods.PICKUP; + const isShipping = data.deliveryMethod === DeliveryMethods.SHIP; const isFreePickup = isFreeOrder && isPickup; + // Billing is separate from shipping when there is no shipping address + // to copy from. `mapOrderToFormValues` canonicalizes deliveryMethod + // against session capabilities, so `!isShipping` already covers both + // session.enableShipping=false and orders with no SHIP fulfillment. + // The remaining case is the user opting out of "use shipping for billing". + const billingIsSeparateFromShipping = + !isShipping || !data.paymentUseShippingAddress; + const requireBillingNamesOnly = - (!enableBillingAddressCollection && - (!data.paymentUseShippingAddress || isPickup)) || + (!enableBillingAddressCollection && billingIsSeparateFromShipping) || isFreePickup; if (requireBillingNamesOnly) { @@ -313,7 +326,7 @@ export function Checkout(props: CheckoutProps) { const requireBillingAddress = enableBillingAddressCollection && !isFreePickup && - (!data.paymentUseShippingAddress || isPickup); + billingIsSeparateFromShipping; if (requireBillingAddress) { // Basic billing fields required for all countries @@ -348,8 +361,12 @@ export function Checkout(props: CheckoutProps) { } // Shipping address validation - only required if delivery method is SHIP + // AND shipping is enabled at the session level. This guards against the + // contradictory case where line items declare SHIP fulfillment but the + // session has enableShipping: false (the shipping form is not rendered + // in that case, so requiring the fields would block the user). const requireShippingAddress = - data.deliveryMethod === DeliveryMethods.SHIP; + data.deliveryMethod === DeliveryMethods.SHIP && enableShipping; if (requireShippingAddress) { // Basic shipping fields required for all countries @@ -383,13 +400,18 @@ export function Checkout(props: CheckoutProps) { } } }); - }, [checkoutFormSchema, session?.enableBillingAddressCollection, t]); + }, [ + checkoutFormSchema, + session?.enableBillingAddressCollection, + session?.enableShipping, + t, + ]); const requiredFields = React.useMemo(() => { return getRequiredFieldsFromSchema(formSchema); }, [formSchema]); - if (!isLoadingJWT && !session) { + if (!props.isLoading && !isLoadingJWT && !session) { return (
diff --git a/packages/react/src/components/checkout/form/checkout-form-container.tsx b/packages/react/src/components/checkout/form/checkout-form-container.tsx index bc17109d..3fb5271b 100644 --- a/packages/react/src/components/checkout/form/checkout-form-container.tsx +++ b/packages/react/src/components/checkout/form/checkout-form-container.tsx @@ -46,9 +46,17 @@ export function CheckoutFormContainer({ order, defaultValues: props.defaultValues, defaultCountryCode: session?.shipping?.originAddress?.countryCode, + enableShipping: session?.enableShipping, + enableLocalPickup: session?.enableLocalPickup, }), }), - [order, props.defaultValues, session?.shipping?.originAddress?.countryCode] + [ + order, + props.defaultValues, + session?.shipping?.originAddress?.countryCode, + session?.enableShipping, + session?.enableLocalPickup, + ] ); if (!isConfirmingCheckout && !draftOrderQuery.isLoading && !order) { @@ -59,8 +67,10 @@ export function CheckoutFormContainer({ } } - if (draftOrderQuery.isLoading || isLoadingJWT) { - return ; + if (props.isLoading || draftOrderQuery.isLoading || isLoadingJWT) { + return ( + props.loadingFallback ?? + ); } return ( diff --git a/packages/react/src/components/checkout/form/checkout-form.tsx b/packages/react/src/components/checkout/form/checkout-form.tsx index 1655cf4b..3e5fa9f2 100644 --- a/packages/react/src/components/checkout/form/checkout-form.tsx +++ b/packages/react/src/components/checkout/form/checkout-form.tsx @@ -24,10 +24,7 @@ import { type Product, } from '@/components/checkout/line-items/line-items'; import { NotesForm } from '@/components/checkout/notes/notes-form'; -import { - useDraftOrder, - useDraftOrderTotals, -} from '@/components/checkout/order/use-draft-order'; +import { useDraftOrderTotals } from '@/components/checkout/order/use-draft-order'; import { PaymentForm } from '@/components/checkout/payment/payment-form'; import { ConditionalExpressProviders, @@ -103,21 +100,21 @@ export function CheckoutForm({ mutationKey: ['update-draft-order-taxes', { id: session?.id }], }) > 0; + const isUpdatingFees = + useIsMutating({ + mutationKey: ['update-draft-order-fees', { id: session?.id }], + }) > 0; + const draftOrderTotalsQuery = useDraftOrderTotals(); - const draftOrder = useDraftOrder(); const { data: totals, isLoading: totalsLoading } = draftOrderTotalsQuery; - const { data: order } = draftOrder; // Order summary calculations - keep all values in minor units const subtotal = totals?.subTotal?.value || 0; const orderDiscount = totals?.discountTotal?.value || 0; - const shipping = - order?.shippingLines?.reduce( - (sum, line) => sum + (line?.amount?.value || 0), - 0 - ) || 0; + const shipping = totals?.shippingTotal?.value || 0; const taxTotal = totals?.taxTotal?.value || 0; + const feeTotal = totals?.feeTotal?.value || 0; const orderTotal = totals?.total?.value || 0; const tipTotal = tipAmount || 0; const currencyCode = totals?.total?.currencyCode || 'USD'; @@ -126,6 +123,14 @@ export function CheckoutForm({ const isFree = orderTotal <= 0; const showExpressButtons = subtotal > 0; + // Show shipping/taxes/fees lines if collection is enabled OR if there's + // a preset amount on the order. This way merchants who disable collection + // but pre-apply a value still see it reflected in the summary. + const showShippingLine = + (isShipping && !!session?.enableShipping) || shipping > 0; + const showTaxesLine = !!session?.enableTaxCollection || taxTotal > 0; + const showFeesLine = feeTotal > 0; + useEffect(() => { if (!totalsLoading && isFree) { form.setValue('paymentMethod', PaymentMethodType.OFFLINE); @@ -394,7 +399,9 @@ export function CheckoutForm({ currencyCode={currencyCode} tip={tipTotal} taxes={taxTotal} + fees={feeTotal} isTaxLoading={isUpdatingTaxes} + isFeeLoading={isUpdatingFees} isShippingLoading={isUpdatingShipping} isDiscountLoading={isDiscountApplying} subtotal={subtotal} @@ -403,9 +410,9 @@ export function CheckoutForm({ totalSavings={totalSavings} itemCount={itemCount} total={orderTotal} - enableShipping={ - isShipping && session?.enableShipping - } + enableShipping={showShippingLine} + enableTaxes={showTaxesLine} + enableFees={showFeesLine} /> ) : ( @@ -440,7 +447,7 @@ export function CheckoutForm({ {formatCurrency({ - amount: totals?.total?.value || 0, + amount: orderTotal + tipTotal, currencyCode, inputInMinorUnits: true, })} @@ -460,7 +467,9 @@ export function CheckoutForm({ currencyCode={currencyCode} tip={tipTotal} taxes={taxTotal} + fees={feeTotal} isTaxLoading={isUpdatingTaxes} + isFeeLoading={isUpdatingFees} isShippingLoading={isUpdatingShipping} subtotal={subtotal} discount={orderDiscount} @@ -470,8 +479,9 @@ export function CheckoutForm({ itemCount={itemCount} total={orderTotal} enableDiscounts={session?.enablePromotionCodes} - enableTaxes={session?.enableTaxCollection} - enableShipping={isShipping && session?.enableShipping} + enableTaxes={showTaxesLine} + enableFees={showFeesLine} + enableShipping={showShippingLine} />
@@ -491,7 +501,9 @@ export function CheckoutForm({ currencyCode={currencyCode} tip={tipTotal} taxes={taxTotal} + fees={feeTotal} isTaxLoading={isUpdatingTaxes} + isFeeLoading={isUpdatingFees} isShippingLoading={isUpdatingShipping} subtotal={subtotal} discount={orderDiscount} @@ -501,8 +513,9 @@ export function CheckoutForm({ itemCount={itemCount} total={orderTotal} enableDiscounts={session?.enablePromotionCodes} - enableTaxes={session?.enableTaxCollection} - enableShipping={isShipping && session?.enableShipping} + enableTaxes={showTaxesLine} + enableFees={showFeesLine} + enableShipping={showShippingLine} />
diff --git a/packages/react/src/components/checkout/form/custom-form-provider.tsx b/packages/react/src/components/checkout/form/custom-form-provider.tsx index 4602126c..7e0ead41 100644 --- a/packages/react/src/components/checkout/form/custom-form-provider.tsx +++ b/packages/react/src/components/checkout/form/custom-form-provider.tsx @@ -71,8 +71,11 @@ export function CustomFormProvider< fieldName === 'billingFirstName' || fieldName === 'billingLastName' ); - } else if (paymentUseShippingAddress && !isPickup) { - /* If using shipping address for billing (and not pickup), filter out billing-related field validations */ + } else if (paymentUseShippingAddress && isShipping) { + /* If using shipping address for billing, filter out billing-related field validations. + * We require isShipping (not just !isPickup) so that PURCHASE / all-NONE + * fulfillment orders, or sessions with enableShipping: false, still validate + * billing fields — there's no shipping address to copy from in those cases. */ fieldNames = fieldNames.filter( fieldName => !fieldName.startsWith('billing') ); diff --git a/packages/react/src/components/checkout/line-items/line-items.tsx b/packages/react/src/components/checkout/line-items/line-items.tsx index 9bd26b95..07ec2d12 100644 --- a/packages/react/src/components/checkout/line-items/line-items.tsx +++ b/packages/react/src/components/checkout/line-items/line-items.tsx @@ -125,14 +125,6 @@ export function DraftOrderLineItems({ ))} ))} - {item.notes?.length ? ( - <> - {t.lineItems.note} - {item.notes?.map(note => ( - {note.content} - ))} - - ) : null} {t.general.quantity}: {item.quantity} @@ -157,8 +149,16 @@ export function DraftOrderLineItems({ ) : null} + {item.notes?.length ? ( + + {t.lineItems.note} + {item.notes?.map(note => ( + {note.content} + ))} + + ) : 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..11e83c5d --- /dev/null +++ b/packages/react/src/components/checkout/payment/checkout-buttons/ach/godaddy.tsx @@ -0,0 +1,50 @@ +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, setIsLoadingNonce } = usePoyntACHCollect(); + const { isConfirmingCheckout } = useCheckoutContext(); + const isPaymentDisabled = useIsPaymentDisabled(); + const form = useFormContext(); + const buttonRef = useRef(null); + const { t } = useGoDaddyContext(); + + const isDisabled = + isLoadingNonce || isConfirmingCheckout || isPaymentDisabled; + + const handleSubmit = useCallback(async () => { + if (isDisabled || !collect) return; + + const valid = await form.trigger(); + if (!valid) { + const firstError = Object.keys(form.formState.errors)[0]; + if (firstError) { + form.setFocus(firstError); + } + return; + } + + setIsLoadingNonce(true); + collect.getNonce({}); + }, [collect, form, isDisabled, setIsLoadingNonce]); + + if (!collect) return null; + + return ( + + ); +} diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/applePay/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/applePay/godaddy.tsx index 06155e81..62cde7bc 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/applePay/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/applePay/godaddy.tsx @@ -36,6 +36,7 @@ export function GoDaddyApplePayCheckoutButton() { const currencyCode = totals?.total?.currencyCode || 'USD'; const countryCode = session?.shipping?.originAddress?.countryCode || 'US'; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); const confirmCheckout = useConfirmCheckout(); const collect = useRef(null); @@ -91,10 +92,7 @@ export function GoDaddyApplePayCheckoutButton() { businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId( - session, - godaddyPaymentsConfig?.appId - ), + applicationId, }, { country: countryCode, @@ -113,6 +111,7 @@ export function GoDaddyApplePayCheckoutButton() { session?.businessId, session?.storeId, session?.channelId, + applicationId, isPoyntLoaded, isCollectLoading, ]); diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx index 72aafb79..08124af9 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/godaddy.tsx @@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button'; import { useGoDaddyContext } from '@/godaddy-provider'; export function CreditCardCheckoutButton() { - const { collect, isLoadingNonce } = usePoyntCollect(); + const { collect, isLoadingNonce, setIsLoadingNonce } = usePoyntCollect(); const { isConfirmingCheckout } = useCheckoutContext(); const isPaymentDisabled = useIsPaymentDisabled(); const form = useFormContext(); @@ -16,7 +16,12 @@ export function CreditCardCheckoutButton() { const buttonRef = useRef(null); const { t } = useGoDaddyContext(); + const isDisabled = + isLoadingNonce || isConfirmingCheckout || isPaymentDisabled; + const handleSubmit = useCallback(async () => { + if (isDisabled || !collect) return; + const valid = await form.trigger(); if (!valid) { const firstError = Object.keys(form.formState.errors)[0]; @@ -26,8 +31,9 @@ export function CreditCardCheckoutButton() { return; } - collect?.getNonce(poyntCardRequest); - }, [poyntCardRequest, form, collect]); + setIsLoadingNonce(true); + collect.getNonce(poyntCardRequest); + }, [collect, form, isDisabled, poyntCardRequest, setIsLoadingNonce]); if (!collect) return null; @@ -38,7 +44,7 @@ export function CreditCardCheckoutButton() { type='button' onClick={handleSubmit} ref={buttonRef} - disabled={isLoadingNonce || isConfirmingCheckout || isPaymentDisabled} + disabled={isDisabled} > {t.payment.payNow} diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx index 9b3e3d65..1fad71e2 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx @@ -85,6 +85,7 @@ export function ExpressCheckoutButton() { const updateTaxes = useUpdateTaxes(); const countryCode = session?.shipping?.originAddress?.countryCode || 'US'; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); const confirmCheckout = useConfirmCheckout(); const collect = useRef(null); @@ -439,10 +440,7 @@ export function ExpressCheckoutButton() { businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId( - session, - godaddyPaymentsConfig?.appId - ), + applicationId, }, { country: countryCode, @@ -512,6 +510,7 @@ export function ExpressCheckoutButton() { session?.businessId, session?.storeId, session?.channelId, + applicationId, session?.enablePromotionCodes, session?.enableShippingAddressCollection, session?.storeName, @@ -576,6 +575,8 @@ export function ExpressCheckoutButton() { mapOrderToFormValues({ order: draftOrder, defaultCountryCode: session?.shipping?.originAddress?.countryCode, + enableShipping: session?.enableShipping, + enableLocalPickup: session?.enableLocalPickup, }) ); diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/googlePay/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/googlePay/godaddy.tsx index 1b2b08b4..a7bfa20f 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/googlePay/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/googlePay/godaddy.tsx @@ -36,6 +36,7 @@ export function GoDaddyGooglePayCheckoutButton() { const currencyCode = totals?.total?.currencyCode || 'USD'; const countryCode = session?.shipping?.originAddress?.countryCode || 'US'; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); const confirmCheckout = useConfirmCheckout(); const collect = useRef(null); @@ -91,10 +92,7 @@ export function GoDaddyGooglePayCheckoutButton() { businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId( - session, - godaddyPaymentsConfig?.appId - ), + applicationId, }, { country: countryCode, @@ -113,6 +111,7 @@ export function GoDaddyGooglePayCheckoutButton() { session?.businessId, session?.storeId, session?.channelId, + applicationId, isPoyntLoaded, isCollectLoading, ]); diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx index 3d508f8d..ffd2b83c 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx @@ -36,6 +36,7 @@ export function PazeCheckoutButton() { const currencyCode = totals?.total?.currencyCode || 'USD'; const countryCode = session?.shipping?.originAddress?.countryCode || 'US'; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); const confirmCheckout = useConfirmCheckout(); const collect = useRef(null); @@ -90,10 +91,7 @@ export function PazeCheckoutButton() { businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId( - session, - godaddyPaymentsConfig?.appId - ), + applicationId, }, { country: countryCode, @@ -112,6 +110,7 @@ export function PazeCheckoutButton() { session?.businessId, session?.storeId, session?.channelId, + applicationId, isPoyntLoaded, isCollectLoading, ]); diff --git a/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx index 3087222d..7d0f6c32 100644 --- a/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx +++ b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx @@ -1,6 +1,8 @@ 'use client'; import { type ComponentType, lazy, Suspense } from 'react'; +import { useCheckoutContext } from '@/components/checkout/checkout'; +import { getApplicationId } from '@/components/checkout/payment/utils/get-application-id'; import { Skeleton } from '@/components/ui/skeleton'; import { type AvailablePaymentProviders, @@ -70,6 +72,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' @@ -202,6 +222,12 @@ type PaymentComponentRegistry = { button: PaymentComponentKey; }; }; + [PaymentMethodType.ACH]?: { + [PaymentProvider.GODADDY]: { + form: PaymentComponentKey; + button: PaymentComponentKey; + }; + }; [PaymentMethodType.MERCADOPAGO]?: { [PaymentProvider.MERCADOPAGO]: { button: PaymentComponentKey; @@ -271,6 +297,12 @@ export const lazyPaymentComponentRegistry: PaymentComponentRegistry = { button: 'MercadoPagoCheckoutButton', }, }, + [PaymentMethodType.ACH]: { + [PaymentProvider.GODADDY]: { + form: 'GoDaddyACHForm', + button: 'ACHCheckoutButton', + }, + }, [PaymentMethodType.CCAVENUE]: { [PaymentProvider.CCAVENUE]: { button: 'CCAvenueCheckoutButton', @@ -309,6 +341,27 @@ export function LazyPaymentMethodRenderer({ provider, isExpress, }: LazyPaymentMethodRendererProps) { + const { godaddyPaymentsConfig, session } = useCheckoutContext(); + + if (provider === PaymentProvider.GODADDY) { + const hasGoDaddyAppId = !!getApplicationId( + session, + godaddyPaymentsConfig?.appId + )?.trim(); + + if (method === PaymentMethodType.CREDIT_CARD && !hasGoDaddyAppId) { + return null; + } + + if ( + method === PaymentMethodType.ACH && + (!hasGoDaddyAppId || + session?.paymentMethods?.ach?.processor !== PaymentProvider.GODADDY) + ) { + return null; + } + } + const methodRegistry = lazyPaymentComponentRegistry[ method as keyof typeof lazyPaymentComponentRegistry diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index a6474d08..4ff85bea 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, @@ -65,6 +71,7 @@ import { // UI config for payment methods (labels will be resolved from translations) const PAYMENT_METHOD_ICONS: Record = { card: , + ach: , paypal: , applePay: , googlePay: , @@ -74,6 +81,11 @@ const PAYMENT_METHOD_ICONS: Record = { ccavenue: , }; +const INLINE_BILLING_PAYMENT_METHODS: PaymentMethodValue[] = [ + PaymentMethodType.CREDIT_CARD, + PaymentMethodType.ACH, +]; + export function PaymentForm( props: DraftOrderTotalsProps & { items: Product[] } ) { @@ -92,6 +104,11 @@ export function PaymentForm( const useShippingAddress = form.watch('paymentUseShippingAddress'); const isPickup = deliveryMethod === DeliveryMethods.PICKUP; const isShipping = deliveryMethod === DeliveryMethods.SHIP; + const isPaymentMethodWithInlineBilling = paymentMethod + ? INLINE_BILLING_PAYMENT_METHODS.includes( + paymentMethod as PaymentMethodValue + ) + : false; const methodConfig = useGetSelectedPaymentMethod( paymentMethod as PaymentMethodValue ); @@ -108,6 +125,7 @@ export function PaymentForm( const currencyCode = props.currencyCode || 'USD'; const countryCode = session?.shipping?.originAddress?.countryCode || 'US'; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); // Helper function to get translated payment method labels const getPaymentMethodLabel = useCallback( @@ -115,6 +133,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: @@ -142,6 +162,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: @@ -185,10 +207,7 @@ export function PaymentForm( businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId( - session, - godaddyPaymentsConfig?.appId - ), + applicationId, }, { country: countryCode, @@ -212,9 +231,12 @@ export function PaymentForm( session?.businessId, session?.storeId, session?.channelId, + applicationId, isPoyntLoaded, ]); + const hasGoDaddyAppId = !!applicationId?.trim(); + const availablePaymentMethods = React.useMemo(() => { if (!session?.paymentMethods) return []; return Object.keys(session.paymentMethods).filter(key => { @@ -226,6 +248,23 @@ export function PaymentForm( Array.isArray(method.checkoutTypes) && method.checkoutTypes.includes(CheckoutType.STANDARD); + // GoDaddy CC/ACH require a resolvable application id (default config or + // gopay_override). Without it, LazyPaymentMethodRenderer returns null + // and the tab would render an empty form/button area. + if ( + key === PaymentMethodType.CREDIT_CARD && + method?.processor === PaymentProvider.GODADDY + ) { + return baseCheck && hasGoDaddyAppId; + } + + if ( + key === PaymentMethodType.ACH && + method?.processor === PaymentProvider.GODADDY + ) { + return baseCheck && hasGoDaddyAppId; + } + // Special handling for GoDaddy wallet payments — only show when device supports them if ( key === PaymentMethodType.PAZE && @@ -250,18 +289,32 @@ export function PaymentForm( return baseCheck; }); - }, [session, pazeSupported, applePaySupported, googlePaySupported]); + }, [ + session, + hasGoDaddyAppId, + pazeSupported, + applePaySupported, + googlePaySupported, + ]); + + // Billing is separate from shipping when there is no shipping address to + // copy from. `mapOrderToFormValues` canonicalizes deliveryMethod against + // session capabilities, so `!isShipping` already covers: + // - session.enableShipping = false + // - line items have no SHIP fulfillment (PICKUP / PURCHASE / all-NONE) + // The remaining case is the user opting out of "use shipping for billing". + const billingIsSeparateFromShipping = !isShipping || !useShippingAddress; const shouldShowBillingNamesOnly = - paymentMethod !== PaymentMethodType.CREDIT_CARD && + !isPaymentMethodWithInlineBilling && session?.enableBillingAddressCollection === false && - !useShippingAddress; + billingIsSeparateFromShipping; const isBillingAddressRequired = - shouldShowBillingNamesOnly || - (session?.enableBillingAddressCollection && - (!useShippingAddress || isPickup) && - paymentMethod !== PaymentMethodType.CREDIT_CARD); + !isPaymentMethodWithInlineBilling && + billingIsSeparateFromShipping && + (shouldShowBillingNamesOnly || + session?.enableBillingAddressCollection !== false); const billingCopy = shouldShowBillingNamesOnly && t.payment.billingInformation @@ -481,7 +534,7 @@ export function PaymentForm( {isShipping && session?.enableShipping && - paymentMethod !== PaymentMethodType.CREDIT_CARD ? ( + !isPaymentMethodWithInlineBilling ? ( ) : null} {isBillingAddressRequired ? ( @@ -534,7 +587,9 @@ export function PaymentForm( currencyCode={props.currencyCode} tip={props.tip} taxes={props.taxes} + fees={props.fees} isTaxLoading={props.isTaxLoading} + isFeeLoading={props.isFeeLoading} isShippingLoading={props.isShippingLoading} subtotal={props.subtotal} discount={props.discount} @@ -544,7 +599,8 @@ export function PaymentForm( total={props.total} enableShipping={props.enableShipping} enableDiscounts={session?.enablePromotionCodes} - enableTaxes={session?.enableTaxCollection} + enableTaxes={props.enableTaxes ?? session?.enableTaxCollection} + enableFees={props.enableFees} />
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..812081e8 --- /dev/null +++ b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx @@ -0,0 +1,283 @@ +import { useId, useLayoutEffect, useRef, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { AddressForm } from '@/components/checkout/address'; +import { useCheckoutContext } from '@/components/checkout/checkout'; +import { CheckoutSection } from '@/components/checkout/checkout-section'; +import { CheckoutSectionHeader } from '@/components/checkout/checkout-section-header'; +import { DeliveryMethods } from '@/components/checkout/delivery/delivery-method'; +import type { + TokenizeJs, + TokenizeJsEvent, +} from '@/components/checkout/payment/types'; +import { getApplicationId } from '@/components/checkout/payment/utils/get-application-id'; +import { PaymentAddressToggle } from '@/components/checkout/payment/utils/payment-address-toggle'; +import { usePoyntACHCollect } from '@/components/checkout/payment/utils/poynt-ach-provider'; +import { + 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 form = useFormContext(); + const paymentMethod = form.watch('paymentMethod'); + const useShippingAddress = form.watch('paymentUseShippingAddress'); + const deliveryMethod = form.watch('deliveryMethod'); + const isShipping = deliveryMethod === DeliveryMethods.SHIP; + + // Billing is separate from shipping when there is no shipping address to + // copy from. `mapOrderToFormValues` canonicalizes deliveryMethod against + // session capabilities, so `!isShipping` already covers: + // - session.enableShipping = false + // - line items have no SHIP fulfillment (PICKUP / PURCHASE / all-NONE) + // The remaining case is the user opting out of "use shipping for billing". + const billingIsSeparateFromShipping = !isShipping || !useShippingAddress; + + const shouldShowBillingNamesOnly = + paymentMethod === PaymentMethodType.ACH && + session?.enableBillingAddressCollection === false && + billingIsSeparateFromShipping; + + const isBillingAddressRequired = + paymentMethod === PaymentMethodType.ACH && + billingIsSeparateFromShipping && + (shouldShowBillingNamesOnly || + session?.enableBillingAddressCollection !== false); + + const billingCopy = + shouldShowBillingNamesOnly && t.payment.billingInformation + ? t.payment.billingInformation + : t.payment.billingAddress; + + const description = t.payment.descriptions?.ach; + + const confirmCheckout = useConfirmCheckout(); + + const elementId = `gdpay-ach-element-${useId()}`; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); + + const fontFamily = + '"GD Sherpa", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'; + + 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, + }); + + 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); + } + setIsLoadingNonce(false); + } + } 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(''); + } else { + setIsLoadingNonce(false); + } + }); + }, [ + isPoyntLoaded, + godaddyPaymentsConfig, + confirmCheckout.mutateAsync, + setCollect, + setCheckoutErrors, + t, + setIsLoadingNonce, + session?.businessId, + session?.storeId, + session?.channelId, + applicationId, + elementId, + ]); + + return ( + <> + {description ?
{description}
: null} +
+ {error ? ( +

{error}

+ ) : null} + {session?.enableShipping && + isShipping && + paymentMethod === PaymentMethodType.ACH ? ( + + ) : null} + {isBillingAddressRequired ? ( + + + + + ) : null} + + ); +} diff --git a/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx b/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx index c7d3eaa0..76394eba 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx @@ -18,18 +18,25 @@ export function CreditCardContainer({ children }: { children?: ReactNode }) { const useShippingAddress = form.watch('paymentUseShippingAddress'); const deliveryMethod = form.watch('deliveryMethod'); const isShipping = deliveryMethod === DeliveryMethods.SHIP; - const isPickup = deliveryMethod === DeliveryMethods.PICKUP; + + // Billing is separate from shipping when there is no shipping address to + // copy from. `mapOrderToFormValues` canonicalizes deliveryMethod against + // session capabilities, so `!isShipping` already covers: + // - session.enableShipping = false + // - line items have no SHIP fulfillment (PICKUP / PURCHASE / all-NONE) + // The remaining case is the user opting out of "use shipping for billing". + const billingIsSeparateFromShipping = !isShipping || !useShippingAddress; + const shouldShowBillingNamesOnly = paymentMethod === PaymentMethodType.CREDIT_CARD && session?.enableBillingAddressCollection === false && - !useShippingAddress; + billingIsSeparateFromShipping; const isBillingAddressRequired = - !session?.enableShipping || - shouldShowBillingNamesOnly || - (session?.enableBillingAddressCollection && - (!useShippingAddress || isPickup) && - paymentMethod === PaymentMethodType.CREDIT_CARD); + paymentMethod === PaymentMethodType.CREDIT_CARD && + billingIsSeparateFromShipping && + (shouldShowBillingNamesOnly || + session?.enableBillingAddressCollection !== false); const billingCopy = shouldShowBillingNamesOnly && t.payment.billingInformation 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 06dc2783..8f3e6573 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/credit-card/godaddy.tsx @@ -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, @@ -25,6 +25,9 @@ export function GoDaddyCreditCardForm() { const confirmCheckout = useConfirmCheckout(); + const elementId = `gdpay-card-element-${useId()}`; + const applicationId = getApplicationId(session, godaddyPaymentsConfig?.appId); + const options = { iFrame: { width: '100%', @@ -200,14 +203,14 @@ export function GoDaddyCreditCardForm() { businessId: godaddyPaymentsConfig?.businessId || session?.businessId, storeId: session?.storeId, channelId: session?.channelId, - applicationId: getApplicationId(session, godaddyPaymentsConfig?.appId), + applicationId, }); collect?.current?.on('ready', () => { 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; @@ -225,6 +228,7 @@ export function GoDaddyCreditCardForm() { if (err instanceof GraphQLErrorWithCodes) { setCheckoutErrors(err.codes); } + setIsLoadingNonce(false); } } else { setCheckoutErrors(['TRANSACTION_PROCESSING_FAILED']); @@ -240,6 +244,8 @@ export function GoDaddyCreditCardForm() { collect?.current?.on('validated', event => { if (event?.data?.validated) { setError(''); + } else { + setIsLoadingNonce(false); } }); }, [ @@ -253,11 +259,13 @@ export function GoDaddyCreditCardForm() { session?.businessId, session?.storeId, session?.channelId, + applicationId, + elementId, ]); 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..6dfda4b0 100644 --- a/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx +++ b/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx @@ -1,7 +1,9 @@ import { PayPalScriptProvider } from '@paypal/react-paypal-js'; import { useCheckoutContext } from '@/components/checkout/checkout'; import { CCAvenueReturnProvider } from './ccavenue-return-provider'; +import { getApplicationId } from './get-application-id'; 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 +26,7 @@ export function ConditionalPaymentProviders({ squareConfig, paypalConfig, ccavenueConfig, + session, } = useCheckoutContext(); const { payPalRequest } = useBuildPaymentRequest(); @@ -35,8 +38,26 @@ export function ConditionalPaymentProviders({ wrappedChildren = {wrappedChildren}; } + // Resolve the GoDaddy application id the same way the lazy renderer does, + // so provider wrapping, availability gating, and form rendering all agree + // (e.g. when only `experimental_rules.gopay_override` supplies an app id). + const hasGoDaddyAppId = !!getApplicationId( + session, + godaddyPaymentsConfig?.appId + )?.trim(); + + // Only wrap with PoyntACHCollectProvider if GoDaddy ACH is configured + if ( + hasGoDaddyAppId && + session?.paymentMethods?.ach?.processor === 'godaddy' + ) { + wrappedChildren = ( + {wrappedChildren} + ); + } + // Only wrap with PoyntCollectProvider (GoDaddy Payments) if configured - if (godaddyPaymentsConfig?.appId?.trim()) { + if (hasGoDaddyAppId) { wrappedChildren = ( {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/target/target.tsx b/packages/react/src/components/checkout/target/target.tsx index 682571cc..b3956809 100644 --- a/packages/react/src/components/checkout/target/target.tsx +++ b/packages/react/src/components/checkout/target/target.tsx @@ -34,6 +34,7 @@ export type Target = | 'checkout.summary.totals.shipping.before' | 'checkout.summary.totals.tip.before' | 'checkout.summary.totals.taxes.before' + | 'checkout.summary.totals.fees.before' | 'checkout.summary.totals.total-due.before' | 'checkout.summary.totals.total-due.after' | 'checkout.summary.totals.after' diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index 4d015870..0f6b00cb 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -1,7 +1,13 @@ -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, + type FormatCurrencyOptions, + useFormatCurrency, +} from '@/components/checkout/utils/format-currency'; import { Button } from '@/components/ui/button'; import { FormControl, @@ -23,7 +29,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 +96,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { return (
@@ -122,7 +127,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) {
@@ -150,49 +155,210 @@ 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: FormatCurrencyOptions) => 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 1.5s of inactivity, + // even if the user hasn't blurred the input yet. This keeps the order + // summary / totals up-to-date while the input stays focused. + const [debouncedLocal] = useDebouncedValue(localValue, { wait: 1500 }); + + /** + * 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 1.5s of inactivity. This keeps the order summary + // up-to-date and gives the user visual confirmation of their amount. + useEffect(() => { + if (!isFocused.current || debouncedLocal === null) return; + 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, code, form]); + + 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, @@ -200,20 +366,21 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { properties: { tipAmount: tipAmount, totalBeforeTip: total, - tipPercentage: Number( - ((tipAmount / total) * 100).toFixed(2) - ), + tipPercentage: + total > 0 + ? Number(((tipAmount / total) * 100).toFixed(2)) + : 0, currencyCode, }, }); }} /> - - - - )} - /> - )} - +
+
+ +
+ ); + }} + /> ); } diff --git a/packages/react/src/components/checkout/totals/totals.tsx b/packages/react/src/components/checkout/totals/totals.tsx index 2a559d19..ffee47e2 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 { Target } from '@/components/checkout/target/target'; import { TotalLineItemSkeleton } from '@/components/checkout/totals/totals-skeleton'; @@ -17,6 +18,9 @@ export interface DraftOrderTotalsProps { tip?: number; taxes?: number; isTaxLoading?: boolean; + fees?: number; + isFeeLoading?: boolean; + enableFees?: boolean | null; enableTaxes?: boolean | null; enableDiscounts?: boolean | null; enableShipping?: boolean | null; @@ -68,11 +72,14 @@ export function DraftOrderTotals({ total = 0, tip = 0, taxes = 0, + fees = 0, enableDiscounts = false, enableTaxes = false, + enableFees = false, isTaxLoading = false, isShippingLoading = false, isDiscountLoading = false, + isFeeLoading = false, enableShipping = true, inputInMinorUnits = false, }: DraftOrderTotalsProps) { @@ -82,6 +89,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 (
@@ -143,6 +155,18 @@ export function DraftOrderTotals({ inputInMinorUnits={inputInMinorUnits} /> ))} + + {enableFees && + (isFeeLoading ? ( + + ) : ( + + ))}
@@ -168,7 +192,7 @@ export function DraftOrderTotals({ {formatCurrency({ - amount: total, + amount: calculatedTotal, currencyCode, inputInMinorUnits, })} diff --git a/packages/react/src/components/checkout/utils/checkout-transformers.ts b/packages/react/src/components/checkout/utils/checkout-transformers.ts index d7d36314..6aaf5922 100644 --- a/packages/react/src/components/checkout/utils/checkout-transformers.ts +++ b/packages/react/src/components/checkout/utils/checkout-transformers.ts @@ -43,31 +43,56 @@ export function mapOrderToFormValues({ order, defaultValues, defaultCountryCode, + enableShipping, + enableLocalPickup, }: { order?: DraftOrder | null; defaultValues?: Pick; defaultCountryCode?: string | null; + /** + * Session-level capability flags. When provided, the derived + * `deliveryMethod` will fall back to `PURCHASE` for any item-level + * fulfillment mode that the session has disabled. This avoids the + * contradictory state where line items declare SHIP/PICKUP but the + * session disables those flows (so `DeliveryMethodForm` is not even + * rendered to correct the value). Pass `undefined` to skip the gate. + */ + enableShipping?: boolean | null; + enableLocalPickup?: boolean | null; }): CheckoutFormData { const orderShippingAddress = order?.shipping?.address; const orderBillingAddress = order?.billing?.address; const paymentShouldUseShippingAddress = Boolean( orderShippingAddress?.addressLine1 === orderBillingAddress?.addressLine1 ); - const isPickup = order?.lineItems?.some( - lineItem => lineItem.fulfillmentMode === DeliveryMethods.PICKUP - ); + // Purchase mode = the session has neither shipping nor pickup enabled, so + // this checkout is payment-only regardless of what fulfillment modes the + // line items declare. Costs are assumed to be hardcoded on the order. + const isPurchaseMode = + enableShipping === false && enableLocalPickup === false; - const isShipping = order?.lineItems?.some( - lineItem => lineItem.fulfillmentMode === DeliveryMethods.SHIP - ); + let deliveryMethod = DeliveryMethods.PURCHASE; - const isMixedFulfillment = isPickup && isShipping; + if (!isPurchaseMode) { + const hasPickupItem = order?.lineItems?.some( + lineItem => lineItem.fulfillmentMode === DeliveryMethods.PICKUP + ); + const hasShipItem = order?.lineItems?.some( + lineItem => lineItem.fulfillmentMode === DeliveryMethods.SHIP + ); - let deliveryMethod = DeliveryMethods.PURCHASE; - if (!isMixedFulfillment && isPickup) { - deliveryMethod = DeliveryMethods.PICKUP; - } else if (!isMixedFulfillment && isShipping) { - deliveryMethod = DeliveryMethods.SHIP; + // Only treat item-level fulfillment as actionable if the session allows + // that flow. When a flag is undefined we treat it as enabled so existing + // callers retain their previous behavior. + const isPickup = hasPickupItem && enableLocalPickup !== false; + const isShipping = hasShipItem && enableShipping !== false; + const isMixedFulfillment = isPickup && isShipping; + + if (!isMixedFulfillment && isPickup) { + deliveryMethod = DeliveryMethods.PICKUP; + } else if (!isMixedFulfillment && isShipping) { + deliveryMethod = DeliveryMethods.SHIP; + } } return { @@ -128,7 +153,10 @@ export function mapOrderToFormValues({ paymentYear: '', // Notes - notes: order?.notes?.[0]?.content || '', + notes: + order?.notes?.find( + note => note.authorType === 'CUSTOMER' && note.content?.trim() !== '' + )?.content || '', pickupDate: '', pickupTime: '', @@ -142,6 +170,24 @@ export function mapOrderToFormValues({ }; } +/** + * Reads the `lineItemOrder` metafield from a line item and returns it as a + * number. Returns Infinity when the metafield is missing or unparseable so + * those items sort to the end while preserving their relative order via a + * stable sort. + */ +function getLineItemOrder( + lineItem: NonNullable[number] +): number { + const metafield = lineItem?.metafields?.find(m => m?.key === 'lineItemOrder'); + if (!metafield?.value) return Number.POSITIVE_INFINITY; + + // Value is stored as a string; for JSON-typed numeric values we still want + // to parse the underlying number. + const parsed = Number(metafield.value); + return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY; +} + /** * Maps order line items and SKUs to displayable items */ @@ -149,8 +195,12 @@ export function mapSkusToItemsDisplay( orderItems?: DraftOrder['lineItems'], skusMap: Record = {} ): Product[] { + const sortedOrderItems = orderItems + ? [...orderItems].sort((a, b) => getLineItemOrder(a) - getLineItemOrder(b)) + : orderItems; + return ( - orderItems?.map(orderItem => { + sortedOrderItems?.map(orderItem => { const sku = orderItem?.details?.sku; const skuDetails = sku ? skusMap[sku] : undefined; @@ -167,10 +217,7 @@ export function mapSkusToItemsDisplay( // (orderItem.totals?.taxTotal?.value ?? 0) - // do we need taxTotal here? (orderItem.totals?.discountTotal?.value ?? 0), notes: orderItem.notes - ?.filter( - note => - note.authorType !== 'CUSTOMER' && note.content?.trim() !== '' - ) + ?.filter(note => !!note.content && note.content.trim() !== '') .map(note => ({ id: note.id, content: note.content, diff --git a/packages/react/src/components/checkout/utils/format-currency.ts b/packages/react/src/components/checkout/utils/format-currency.ts index e72ab591..910d0887 100644 --- a/packages/react/src/components/checkout/utils/format-currency.ts +++ b/packages/react/src/components/checkout/utils/format-currency.ts @@ -53,9 +53,21 @@ export interface FormatCurrencyOptions { */ inputInMinorUnits?: boolean; /** - * Return raw numeric value without currency symbol. - * - true → returns "10.00" instead of "$10.00" - * - false → returns full currency string (default) + * Return a machine-parseable major-unit string instead of a localized + * currency string. + * + * - `true` → always uses `en-US` formatting (`.` decimal separator, no + * thousands grouping, no currency symbol) so the result is + * safe to pass to `Number.parseFloat`. The `locale` option + * is intentionally ignored in this mode — using a localized + * separator (e.g. `"10,50"` for `fr-FR`) would silently + * corrupt the value when parsed back to a number. + * Example: `{ amount: 1050, currencyCode: 'USD', returnRaw: true }` → `"10.50"` + * - `false` → returns the full localized currency string (default). + * Example: `{ amount: 1050, currencyCode: 'USD' }` → `"$10.50"` + * + * Use `returnRaw: true` when feeding the value into a payment SDK or a + * numeric input. For human-readable display, leave it `false`. */ returnRaw?: boolean; } @@ -65,7 +77,8 @@ export interface FormatCurrencyOptions { * * - When `inputInMinorUnits = true` (default): converts from minor units (cents) and returns formatted string like "$123.45" * - When `inputInMinorUnits = false`: formats major units (dollars) directly and returns formatted string like "$123.45" - * - When `returnRaw = true`: returns numeric value without currency symbol like "123.45" + * - When `returnRaw = true`: returns a parseable major-unit string in en-US + * format (e.g. "123.45"), regardless of `locale`. See {@link FormatCurrencyOptions.returnRaw}. */ export function formatCurrency({ amount, @@ -114,9 +127,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 +140,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/index.ts b/packages/react/src/index.ts index 98658647..3e0953ac 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -19,4 +19,11 @@ export { } from './components/checkout/totals/totals'; export * from './components/storefront'; export * from './godaddy-provider'; +export { + convertMajorToMinorUnits, + type FormatCurrencyOptions, + formatCurrency, + useConvertMajorToMinorUnits, + useFormatCurrency, +} from './lib/format-currency'; export * from './types'; diff --git a/packages/react/src/lib/format-currency.ts b/packages/react/src/lib/format-currency.ts new file mode 100644 index 00000000..599ed54b --- /dev/null +++ b/packages/react/src/lib/format-currency.ts @@ -0,0 +1,16 @@ +/** + * Public re-exports of currency formatting utilities. + * + * The implementation lives in `components/checkout/utils/format-currency.ts` + * for historical reasons; this module is the stable public entry point. + * + * `currencyConfigs` is intentionally not re-exported — keep the symbol/precision + * table internal so it can be extended without semver impact. + */ +export { + convertMajorToMinorUnits, + type FormatCurrencyOptions, + formatCurrency, + useConvertMajorToMinorUnits, + useFormatCurrency, +} from '@/components/checkout/utils/format-currency'; diff --git a/packages/react/src/lib/godaddy/checkout-env.ts b/packages/react/src/lib/godaddy/checkout-env.ts index ee36d32c..48bc6d84 100644 --- a/packages/react/src/lib/godaddy/checkout-env.ts +++ b/packages/react/src/lib/godaddy/checkout-env.ts @@ -3198,6 +3198,15 @@ const introspection = { kind: 'OBJECT', name: 'CheckoutSessionPaymentMethods', fields: [ + { + name: 'ach', + type: { + kind: 'OBJECT', + name: 'CheckoutSessionPaymentMethodConfig', + }, + args: [], + isDeprecated: false, + }, { name: 'applePay', type: { @@ -3286,6 +3295,13 @@ const introspection = { kind: 'INPUT_OBJECT', name: 'CheckoutSessionPaymentMethodsInput', inputFields: [ + { + name: 'ach', + type: { + kind: 'INPUT_OBJECT', + name: 'CheckoutSessionPaymentMethodConfigInput', + }, + }, { name: 'applePay', type: { diff --git a/packages/react/src/lib/godaddy/checkout-mutations.ts b/packages/react/src/lib/godaddy/checkout-mutations.ts index 898f22cd..b5af9be9 100644 --- a/packages/react/src/lib/godaddy/checkout-mutations.ts +++ b/packages/react/src/lib/godaddy/checkout-mutations.ts @@ -117,6 +117,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 25462336..d9625c32 100644 --- a/packages/react/src/lib/godaddy/checkout-queries.ts +++ b/packages/react/src/lib/godaddy/checkout-queries.ts @@ -113,6 +113,10 @@ export const GetCheckoutSessionQuery = graphql(` processor checkoutTypes } + ach { + processor + checkoutTypes + } mercadopago { processor checkoutTypes @@ -299,6 +303,11 @@ export const DraftOrderQuery = graphql(` status tags type + metafields { + key + type + value + } details { productAssetUrl selectedAddons { 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',