Skip to content

Add ach payment method and restructure delivery method logic #1313

Open
pbennett1-godaddy wants to merge 30 commits into
mainfrom
add-ach-paymentMethod
Open

Add ach payment method and restructure delivery method logic #1313
pbennett1-godaddy wants to merge 30 commits into
mainfrom
add-ach-paymentMethod

Conversation

@pbennett1-godaddy
Copy link
Copy Markdown
Collaborator

@pbennett1-godaddy pbennett1-godaddy commented Mar 6, 2026

Summary

Adds ACH (bank account) as a new payment method for GoDaddy Payments, surfaces tips, fees, shipping, and taxes correctly in the order summary, rewrites the custom tip input for better
multi-currency UX, hardens billing & delivery-method resolution for no-fulfillment / all-NONE / shipping-disabled orders, and ships several checkout bug fixes (inline billing for ACH & pickup,
stuck nonce loading state, $0 line items, duplicate DOM IDs).

Packages affected: @godaddy/react (patch), @godaddy/localizations (patch)


🆕 ACH Payment Method

Adds full support for ACH (bank account) payments via GoDaddy Payments:

  • New components
    • GoDaddyACHForm — Collect iframe for bank account input (account holder name/type, routing number, account number) with custom CSS matching the checkout theme
    • ACHCheckoutButton — submit button that triggers nonce collection and form validation
  • New provider: PoyntACHCollectProvider — dedicated React context for the ACH collect instance and nonce loading state, separate from the credit card collect provider
  • Payment registry: ACH registered in lazyPaymentComponentRegistry with lazy-loaded form and button components
  • Payment form UI: Added Landmark icon and label/description resolution for the ACH tab
  • Conditional wiring: ConditionalPaymentProviders wraps children in the ACH provider only when GoDaddy Payments is configured and session.paymentMethods.ach.processor === 'godaddy'
  • Lazy renderer guard: LazyPaymentMethodRenderer returns null for the ACH method unless GoDaddy Payments has an appId and the session lists ach with the godaddy processor (and similarly
    hides the GoDaddy CC form when no appId is configured)
  • GraphQL: Updated CreateCheckoutSessionMutation, GetCheckoutSessionQuery, and the introspection schema to include ach { processor, checkoutTypes }
  • Types: Added PaymentMethodType.ACH = 'ach'
  • Example: Next.js example app updated with ach and express payment method config

🧾 Inline Billing for ACH & Pickup Fix for Credit Card

Refactored billing-address logic in payment-form.tsx and payment-methods/credit-card/container.tsx:

  • Introduced an INLINE_BILLING_PAYMENT_METHODS list (card, ach) — these collect billing inline, so the standalone billing block / address toggle is suppressed for both
  • PaymentAddressToggle now only renders for non-inline-billing methods
  • For credit card with local pickup, shouldShowBillingNamesOnly now correctly evaluates (!useShippingAddress || isPickup) so the names-only billing block appears when needed
  • GoDaddyACHForm mirrors the same logic, including names-only fallback when enableBillingAddressCollection === false

🚚 Billing & Delivery-Method Resolution for No-Fulfillment Orders

Fixes several related defects that surfaced when an order had no actionable shipping/pickup fulfillment. This includes:

  1. All line items have fulfillmentMode: NONE (with shipping/pickup either enabled or not at the session level) — deliveryMethod previously fell through to PURCHASE but downstream billing
    logic didn't recognize that state, so the full billing-address form rendered even with enableBillingAddressCollection: false.
  2. Session has enableShipping: false while line items still declare SHIP — the shipping form wasn't rendered (correctly), but the schema still required shipping fields and billing rendered as
    if shipping were available, leaving the form unsubmittable.
  3. Session has enableShipping: false AND enableLocalPickup: false (purchase-only checkout) regardless of what fulfillment modes the line items declare — the order is payment-only and costs
    are assumed hardcoded on the order; no dynamic shipping or pickup logic should run, but mapOrderToFormValues was still resolving deliveryMethod from items and downstream code mis-treated it as a
    shipping flow.
  4. Asymmetric session config (e.g. enableShipping: true, enableLocalPickup: false, items have only PICKUP fulfillment) — the form would resolve to PICKUP even though the session disables
    it, with no UI path to correct it.

In all of these the full billing-address form was being rendered even with enableBillingAddressCollection: false, schema validation could either over- or under-validate billing/shipping fields, and
the form could land in an unrecoverable state for contradictory item-vs-session configurations.

  • mapOrderToFormValues is now session-aware. Accepts enableShipping / enableLocalPickup and canonicalizes deliveryMethod against session capabilities, with an explicit isPurchaseMode = enableShipping === false && enableLocalPickup === false short-circuit to PURCHASE (skips line-item scans entirely — line items with SHIP/PICKUP/NONE/mixed fulfillment all resolve to PURCHASE
    when the session is payment-only). Item-level fulfillment is also gated per-flag so asymmetric configs (e.g. SHIP items with enableShipping: false but enableLocalPickup: true) fall back correctly
    to PICKUP/PURCHASE instead of producing an unreachable SHIP state.

  • Callers updated to pass the new flags: checkout-form-container.tsx and payment/checkout-buttons/express/godaddy.tsx.

  • Unified billingIsSeparateFromShipping predicate (!isShipping || !useShippingAddress) replaces the buggy (!useShippingAddress || isPickup) gate in:

    • payment/payment-methods/credit-card/container.tsx
    • payment/payment-methods/ach/godaddy.tsx
    • payment/payment-form.tsx
    • checkout.tsx schema superRefine

    Result: enableBillingAddressCollection: false correctly renders names-only (and validates only first/last name) instead of falling through to the full address form when there's no shipping
    address to copy from.

  • Schema requireShippingAddress gated on enableShipping as defense-in-depth so contradictory configs (SHIP items + enableShipping: false) don't block submission with errors for fields the
    form refuses to render.

  • Field-filter validation in custom-form-provider.tsx changed from paymentUseShippingAddress && !isPickup to paymentUseShippingAddress && isShipping, so billing field errors actually
    surface in PURCHASE / no-shipping flows instead of being silently stripped from the validation pass.

  • Removed dead _isPickup locals in CreditCardContainer and ACH godaddy form after the refactor.

Net behavior for a payment-only checkout (enableShipping: false, enableLocalPickup: false, enableBillingAddressCollection: false, line items with any mix of fulfillment modes including
all-NONE):

  • deliveryMethod resolves to PURCHASE
  • <DeliveryMethodForm /> is not mounted (its parent gates on enableShipping || enableLocalPickup)
  • No shipping or pickup sections render
  • Billing block renders names-only and validates first/last name
  • Contact + payment + totals render as usual

💰 Order Summary: Tips, Fees, Shipping, Taxes

  • Tips included in total: displayed order total now includes the tip amount (total + tip) so customers see the true amount they'll be charged
  • New fees line item: added a Fees line in DraftOrderTotals (with skeleton + checkout.summary.totals.fees.before extension target). Wired through feeTotal from useDraftOrderTotals and
    an update-draft-order-fees mutation-key loading state
  • Show lines when there's a value, even if collection is disabled: introduced showShippingLine, showTaxesLine, showFeesLine in CheckoutForm. Merchants who disable collection but pre-apply
    a value still see it reflected in the summary:
    • Shipping: (isShipping && enableShipping) || shipping > 0
    • Taxes: enableTaxCollection || taxTotal > 0
    • Fees: feeTotal > 0
  • Shipping source: shipping is now read directly from totals.shippingTotal.value instead of summing order.shippingLines (removes the unused useDraftOrder call)

✏️ Custom Tip Input Rewrite

Replaced <input type="number"> with a polished currency input using the "format on blur" pattern (same UX as Stripe, Square, Shopify):

  • While focused: raw text editing with sanitization (digits + single decimal only); intermediate states like "10." or "" are preserved for natural editing
  • On blur: parses input, converts to minor units, syncs to form state, and reformats display (e.g. "10.5""10.50")
  • Debounced sync: after 1.5s of inactivity while focused, form state and totals update automatically (via @tanstack/react-pacer's useDebouncedValue)
  • Multi-currency: respects per-currency precision (0 decimals for JPY/KRW, 3 for KWD/BHD, etc.) and symbol placement (prefix $ vs suffix )
  • convertMajorToMinorUnits hardened: now returns 0 for NaN, negative, or otherwise invalid input
  • Responsive: tip button grids stack on small screens (grid-cols-1 sm:grid-cols-3 / grid-cols-1 sm:grid-cols-2)

🐛 Bug Fixes

  • Stuck nonce loading state: credit card and ACH Pay Now buttons now flip isLoadingNonce to true only after validation succeeds, and reset it correctly on error, on failed validated
    events, and inside the confirmCheckout catch block. The button is also short-circuited when already disabled or when collect isn't ready
  • $0 line items: changed item.originalPrice && …item.originalPrice != null && … so items with a 0 original price correctly render the price column instead of being falsy-skipped
  • Duplicate DOM IDs: GoDaddy credit card and ACH forms now generate their mount-target IDs with React useId() (gdpay-card-element-…, gdpay-ach-element-…), preventing collisions when
    multiple checkouts mount on the same page
  • Stable applicationId deps: ApplePay, GooglePay, Paze, Express, credit card, and ACH forms now compute applicationId once at the component level and include it in their useEffect deps
    instead of re-deriving inline (previously caused subtle re-mount churn)

🌐 Localizations

  • Added ach payment method name ("Bank Account" / localized equivalents) and description across all 20 locales
  • Added a fees totals label across all locales
  • Added AUTHORIZATION_FAILED error message ("Failed to authorize payment" / localized equivalents) across all 20 locales

Changeset

  • Changeset added (docs)

Test Plan

…-ach-paymentMethod

# Conflicts:
#	examples/nextjs/app/page.tsx
#	packages/localizations/src/deDe.ts
#	packages/localizations/src/enIe.ts
#	packages/localizations/src/enUs.ts
#	packages/localizations/src/esAr.ts
#	packages/localizations/src/esCl.ts
#	packages/localizations/src/esCo.ts
#	packages/localizations/src/esEs.ts
#	packages/localizations/src/esMx.ts
#	packages/localizations/src/esPe.ts
#	packages/localizations/src/esUs.ts
#	packages/localizations/src/frCa.ts
#	packages/localizations/src/frFr.ts
#	packages/localizations/src/idId.ts
#	packages/localizations/src/itIt.ts
#	packages/localizations/src/ptBr.ts
#	packages/localizations/src/qaPs.ts
#	packages/localizations/src/trTr.ts
#	packages/localizations/src/viVn.ts
#	packages/localizations/src/zhCn.ts
#	packages/localizations/src/zhSg.ts
#	packages/react/src/components/checkout/payment/lazy-payment-loader.tsx
#	packages/react/src/lib/godaddy/checkout-mutations.ts
#	packages/react/src/lib/godaddy/checkout-queries.ts
…-ach-paymentMethod

# Conflicts:
#	packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx
…-ach-paymentMethod

# Conflicts:
#	packages/localizations/src/deDe.ts
#	packages/react/src/components/checkout/payment/lazy-payment-loader.tsx
#	packages/react/src/components/checkout/payment/utils/conditional-providers.tsx
@pbennett1-godaddy pbennett1-godaddy self-assigned this Mar 6, 2026
@pbennett1-godaddy pbennett1-godaddy requested a review from a team as a code owner March 6, 2026 15:28
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 6, 2026

🦋 Changeset detected

Latest commit: 7c63c31

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@godaddy/localizations Patch
@godaddy/react Patch
nextjs Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 6, 2026

'This PR is stale as it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'

@github-actions github-actions Bot added the stale label Apr 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

'This PR was closed because it has been stalled for 5 days with no activity.'

@github-actions github-actions Bot closed this Apr 12, 2026
# Conflicts:
#	packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx
#	packages/react/src/components/checkout/payment/checkout-buttons/paze/godaddy.tsx
#	packages/react/src/components/checkout/pickup/local-pickup.tsx
@pbennett1-godaddy pbennett1-godaddy changed the title Add ach payment method Add ach payment method and restructure delivery method logic May 22, 2026
@wcole1-godaddy
Copy link
Copy Markdown
Contributor

I reviewed the PR and found a few issues worth addressing before merge.

  1. High: tips are displayed in totals but do not appear to be submitted or charged

    • DraftOrderTotals displays total + tip, but the tip only lives in React Hook Form state.
    • Payment requests still use totals.total.value, and confirm payloads only send token/type/provider.
    • Result: customers can see a total due that includes tip while the payment/confirm flow appears to charge only the draft order total. The tip should either be persisted to the order before confirm or included in the confirm/payment request path.
  2. High: the new PURCHASE delivery fallback can be overwritten by DeliveryMethodForm

    • mapOrderToFormValues can now resolve deliveryMethod to PURCHASE when item fulfillment is not allowed by session flags.
    • But if either shipping or pickup is enabled, DeliveryMethodForm mounts and replaces any invalid current value with the first session-enabled method.
    • Example: enableShipping=false, enableLocalPickup=true, line items are SHIP only. The transformer resolves PURCHASE, then DeliveryMethodForm changes it to PICKUP, reintroducing an unreachable fulfillment state. Available delivery methods should account for item-level fulfillment too, or preserve PURCHASE when no enabled session method is actually supported by the order.
  3. Medium: collapsed mobile order-summary totals still omit tips

    • The expanded totals include tip, but collapsed mobile summary headers still format only the base total.
    • A customer who selects a tip can still see the pre-tip amount in the collapsed summary header. These should use the same calculated total as DraftOrderTotals.
  4. Medium: GoDaddy card/ACH can remain selectable when the app ID is unavailable

    • Payment methods are included based on session checkoutTypes, but the GoDaddy app ID guard happens later inside the lazy renderer.
    • This can leave the accordion showing/selecting Card or ACH while rendering no form/button.
    • Also, getApplicationId can resolve a session override, but ConditionalPaymentProviders still wraps providers only when godaddyPaymentsConfig?.appId exists. Availability, provider wrapping, and rendering should use the same app-id predicate.
  5. Question / possible tracking regression: standard GDP wallets confirm as card

    • Standard Paze/Apple Pay/Google Pay confirm payloads now use PaymentMethodType.CREDIT_CARD.
    • useConfirmCheckout only emits wallet-specific completion events for apple_pay, google_pay, and paze.
    • If the backend requires card, tracking should be updated. If the backend supports wallet sources, these should probably pass the wallet source as before.

Process note: the PR includes generated package/changelog version bumps while also keeping a changeset. Unless this is intentionally a release PR, I would remove generated package/changelog versioning and let Changesets handle the release bump on merge.

@pbennett1-godaddy
Copy link
Copy Markdown
Collaborator Author

I reviewed the PR and found a few issues worth addressing before merge.

  1. High: tips are displayed in totals but do not appear to be submitted or charged

    • DraftOrderTotals displays total + tip, but the tip only lives in React Hook Form state.
    • Payment requests still use totals.total.value, and confirm payloads only send token/type/provider.
    • Result: customers can see a total due that includes tip while the payment/confirm flow appears to charge only the draft order total. The tip should either be persisted to the order before confirm or included in the confirm/payment request path.
  2. High: the new PURCHASE delivery fallback can be overwritten by DeliveryMethodForm

    • mapOrderToFormValues can now resolve deliveryMethod to PURCHASE when item fulfillment is not allowed by session flags.
    • But if either shipping or pickup is enabled, DeliveryMethodForm mounts and replaces any invalid current value with the first session-enabled method.
    • Example: enableShipping=false, enableLocalPickup=true, line items are SHIP only. The transformer resolves PURCHASE, then DeliveryMethodForm changes it to PICKUP, reintroducing an unreachable fulfillment state. Available delivery methods should account for item-level fulfillment too, or preserve PURCHASE when no enabled session method is actually supported by the order.
  3. Medium: collapsed mobile order-summary totals still omit tips

    • The expanded totals include tip, but collapsed mobile summary headers still format only the base total.
    • A customer who selects a tip can still see the pre-tip amount in the collapsed summary header. These should use the same calculated total as DraftOrderTotals.
  4. Medium: GoDaddy card/ACH can remain selectable when the app ID is unavailable

    • Payment methods are included based on session checkoutTypes, but the GoDaddy app ID guard happens later inside the lazy renderer.
    • This can leave the accordion showing/selecting Card or ACH while rendering no form/button.
    • Also, getApplicationId can resolve a session override, but ConditionalPaymentProviders still wraps providers only when godaddyPaymentsConfig?.appId exists. Availability, provider wrapping, and rendering should use the same app-id predicate.
  5. Question / possible tracking regression: standard GDP wallets confirm as card

    • Standard Paze/Apple Pay/Google Pay confirm payloads now use PaymentMethodType.CREDIT_CARD.
    • useConfirmCheckout only emits wallet-specific completion events for apple_pay, google_pay, and paze.
    • If the backend requires card, tracking should be updated. If the backend supports wallet sources, these should probably pass the wallet source as before.

Process note: the PR includes generated package/changelog version bumps while also keeping a changeset. Unless this is intentionally a release PR, I would remove generated package/changelog versioning and let Changesets handle the release bump on merge.

1. Tips not submitted/charged — can't address in this PR

You're right that the displayed total includes the tip while the confirm payload doesn't carry it, but MutationConfirmCheckoutSessionInput has no tip field today (only enableTips exists in the schema
as a session-level flag). Wiring this end-to-end requires a checkout-api change to accept a tip on confirm (and/or a way to persist it to the draft order beforehand). I'll open a follow-up to track the
API + client work together. Happy to drop a TODO near the total + tip calculation in the meantime so the next reader doesn't assume it's wired through.

2. PURCHASE fallback overwritten by DeliveryMethodForm — working as intended

This is by design. DeliveryMethodForm only mounts when enableShipping || enableLocalPickup, and availableMethods is built strictly from those session flags. The PURCHASE fallback in
mapOrderToFormValues is for the payment-only case (enableShipping === false && enableLocalPickup === false) — when one of them is on, the merchant has explicitly opted into that fulfillment path, so
the form correctly steers the customer to a session-supported method even if line items declare otherwise.

For your specific example (enableShipping=false, enableLocalPickup=true, items SHIP-only): the merchant has enabled pickup at the session level, so resolving to PICKUP is the intended outcome. The
session config is the source of truth for "what flows can this checkout actually run"; line-item fulfillment is a content/data signal that the session may override. If items truly can't be picked up,
that's a merchant data-integrity issue, not something the form should silently degrade to PURCHASE for (which would render no actionable UI). Open to revisiting if you've seen a real case where this
misleads customers.

3. Collapsed mobile summary omits tip — fixed ✅

Confirmed the bug: mobile accordion header used totals?.total?.value directly while the expanded DraftOrderTotals uses total + tip. Updated the header in checkout-form.tsx to use orderTotal + tipTotal
so both views stay in sync.

4. GoDaddy CC/ACH selectable without app id — fixed ✅

Good catch on the predicate split. Unified everything around getApplicationId(session, godaddyPaymentsConfig?.appId):

  • payment-form.tsx: availablePaymentMethods now filters out GoDaddy CREDIT_CARD / ACH when no app id resolves, so the tab is no longer selectable when the lazy renderer would return null.
  • conditional-providers.tsx: PoyntCollectProvider and PoyntACHCollectProvider wrapping now uses the same getApplicationId resolution instead of godaddyPaymentsConfig?.appId directly, which also fixes
    the experimental_rules.gopay_override-only case (previously the renderer would mount the form but no provider context existed).
  • LazyPaymentMethodRenderer keeps its existing guard as a safety net.

Same predicate now drives selector inclusion, provider wrapping, and renderer fallback.

5. Standard GDP wallets confirming as card

Expects card for all of these and caterogizes on the gopay side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants