Skip to content

fix(magento-customer): honor Magento's required-status for the address telephone - #2663

Open
paales wants to merge 4 commits into
canaryfrom
feature/address-telephone-required
Open

fix(magento-customer): honor Magento's required-status for the address telephone#2663
paales wants to merge 4 commits into
canaryfrom
feature/address-telephone-required

Conversation

@paales

@paales paales commented Aug 6, 2026

Copy link
Copy Markdown
Member

Written by Claude Code:

The problem

CartAddressInput.telephone is a non-nullable String! in Magento's schema, so the address forms always have to send something. The mutation variables however declare it as an optional String with a placeholder default:

mutation SetShippingBillingAddress($telephone: String = "000 - 000 0000", ...)

handlerFactory derives required from variable.type.kind === NON_NULL_TYPE, so required.telephone resolves to false, the field renders as optional, and onBeforeSubmit fills the gap:

telephone: variables.telephone || '000 - 000 0000',

Every checkout where the customer skipped the field therefore stores a fake phone number on the order — it shows up in the Magento admin, in order confirmation mails and in any address template that renders telephone.

Magento already tells us

attributesForm(formCode: "customer_address_edit") returns is_required per address attribute, and it reflects the customer/address/telephone_show store configuration (required / optional / hidden). Against a stock Magento 2.4.9:

firstname   | is_required = true
lastname    | is_required = true
company     | is_required = false
street      | is_required = true
city        | is_required = true
country_id  | is_required = true
postcode    | is_required = false
telephone   | is_required = true
vat_id      | is_required = false

GraphCommerce already queries this — useAttributesForm in @graphcommerce/magento-store — but only for the customer account create/edit forms. The address forms never looked at it.

The change

ShippingAddressForm, EditBillingAddressForm and EditAddressForm each call the hook directly:

const addressAttributes = useAttributesForm({ formCode: 'customer_address_edit' })
const telephoneRequired = addressAttributes.find((a) => a.code === 'telephone')?.is_required
  • The telephone field is marked required when Magento says it is required.
  • The placeholder is gone. An empty telephone is submitted as an empty string, which satisfies the non-nullable input and is validated by Magento against the very same flag: Magento\Customer\Model\Address\Validator\General::checkOptionalFields() checks eavConfig->getAttribute('customer_address', 'telephone')->getIsRequired(), and customer/address/telephone_show is what writes that flag (Show\AddressOnly: req → 1, opt → 0, hidden → 0).
  • The stored placeholder is stripped from the form default values in all three forms via a local legacyPlaceholderTelephone constant, so a customer editing an address that still carries it gets an empty required field rather than a prefilled fake number. Nothing writes that string anymore.

Behavior change

Where a customer who bypassed the client-side validation previously ended up with a successful order carrying 000 - 000 0000, a shop that requires a telephone now returns a proper validation error. Verified against Magento 2.4.9 with telephone_show = req:

telephone sent to setShippingAddressesOnCart result
"" "telephone" is required. Enter and try again.
"0612345678" accepted

That window is small — the preload below puts the metadata in the Apollo cache before first render — and failing loudly beats silently storing junk.

Version gate

attributesForm only exists since Magento 2.4.7, so all of the above is gated in one helper, packages/magento-customer/utils/legacyPlaceholderTelephone.ts:

  • stripLegacyPlaceholderTelephone(telephone) — clears a stored placeholder when an address is loaded into a form; returns the value untouched below 2.4.7.
  • applyLegacyPlaceholderTelephone(telephone) — submits '' for an empty field; falls back to the placeholder below 2.4.7.

The required-status derivation needs no extra gate: below 2.4.7 attributesForm yields nothing, so telephoneRequired stays undefined and the field renders exactly as before. Net effect on < 2.4.7 is identical to today.

CreateCustomerAddressForm needs no change — its mutation declares $telephone: String!, so it was already required.

Preloading

The metadata is preloaded in getStaticProps, the same way account/signin.tsx and account/name/index.tsx already preload customer_account_create / customer_account_edit:

if (magentoVersion >= 247) await preloadAttributesForm(client, 'customer_address_edit')

Applied to checkout/index.tsx, checkout/edit/billing-address.tsx, checkout/customer/addresses/edit.tsx and account/addresses/edit.tsx in all three examples. Besides saving a client-side round trip, this closes the window where a customer could submit the form before the metadata resolved and still end up with the placeholder.

Notes / deliberately out of scope

  • The $telephone: String = "000 - 000 0000" defaults in SetShippingAddress.graphql, SetShippingBillingAddress.graphql and SetBillingAddress.graphql are left as-is. They are unreachable through the forms (useFormGql always sends the variable) and removing the default would break any consumer that calls the mutation directly without a telephone.
  • Only telephone is wired up. The same metadata can drive company / vat_id / postcode required-status later, but postcode in particular is country-dependent in Magento (is_required = false while the actual requirement comes from the zip-optional country list), so that deserves its own change.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
graphcommerce-246 Ready Ready Preview Aug 10, 2026 10:37am
graphcommerce-247 Ready Ready Preview Aug 10, 2026 10:37am
graphcommerce-hygraph-dynamic-rows-ui Ready Ready Preview Aug 10, 2026 10:37am
graphcommerce-open-source Ready Ready Preview Aug 10, 2026 10:37am
graphcommerce-storyblok Ready Ready Preview Aug 10, 2026 10:37am

Request Review

@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ce9f531

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

This PR includes changesets to release 87 packages
Name Type
@graphcommerce/magento-cart-shipping-address Patch
@graphcommerce/magento-customer Patch
@graphcommerce/magento-cart Patch
@graphcommerce/magento-graphcms Patch
@graphcommerce/magento-open-source Patch
@graphcommerce/magento-storyblok Patch
@graphcommerce/docs Patch
@graphcommerce/browserslist-config-pwa Patch
@graphcommerce/changeset-changelog Patch
@graphcommerce/eslint-config-pwa Patch
@graphcommerce/graphql-codegen-markdown-docs Patch
@graphcommerce/graphql-codegen-near-operation-file Patch
@graphcommerce/graphql-codegen-relay-optimizer-plugin Patch
@graphcommerce/misc Patch
@graphcommerce/next-config Patch
@graphcommerce/prettier-config-pwa Patch
@graphcommerce/typescript-config-pwa Patch
@graphcommerce/address-autocomplete Patch
@graphcommerce/address-fields-nl Patch
@graphcommerce/algolia-categories Patch
@graphcommerce/algolia-insights Patch
@graphcommerce/algolia-personalization Patch
@graphcommerce/algolia-products Patch
@graphcommerce/algolia-recommend Patch
@graphcommerce/algolia-search Patch
@graphcommerce/cli Patch
@graphcommerce/demo-magento-graphcommerce Patch
@graphcommerce/ecommerce-ui Patch
@graphcommerce/framer-next-pages Patch
@graphcommerce/framer-scroller Patch
@graphcommerce/framer-utils Patch
@graphcommerce/google-datalayer Patch
@graphcommerce/google-playstore Patch
@graphcommerce/googleanalytics Patch
@graphcommerce/googlerecaptcha Patch
@graphcommerce/googletagmanager Patch
@graphcommerce/graphcms-ui Patch
@graphcommerce/graphql-mesh Patch
@graphcommerce/graphql Patch
@graphcommerce/hygraph-cli Patch
@graphcommerce/hygraph-dynamic-rows-ui Patch
@graphcommerce/hygraph-dynamic-rows Patch
@graphcommerce/hygraph-ui Patch
@graphcommerce/image Patch
@graphcommerce/lingui-next Patch
@graphcommerce/magento-cart-checkout Patch
@graphcommerce/magento-cart-coupon Patch
@graphcommerce/magento-cart-email Patch
@graphcommerce/magento-cart-items Patch
@graphcommerce/magento-cart-payment-method Patch
@graphcommerce/magento-cart-pickup Patch
@graphcommerce/magento-cart-shipping-method Patch
@graphcommerce/magento-category Patch
@graphcommerce/magento-cms Patch
@graphcommerce/magento-compare Patch
@graphcommerce/magento-graphql-rest Patch
@graphcommerce/magento-graphql Patch
@graphcommerce/magento-newsletter Patch
@graphcommerce/magento-payment-adyen Patch
@graphcommerce/magento-payment-afterpay Patch
@graphcommerce/magento-payment-braintree Patch
@graphcommerce/magento-payment-included Patch
@graphcommerce/magento-payment-klarna Patch
@graphcommerce/magento-payment-multisafepay Patch
@graphcommerce/magento-payment-paypal Patch
@graphcommerce/magento-payment-tokens Patch
@graphcommerce/magento-product-bundle Patch
@graphcommerce/magento-product-configurable Patch
@graphcommerce/magento-product-downloadable Patch
@graphcommerce/magento-product-grouped Patch
@graphcommerce/magento-product-simple Patch
@graphcommerce/magento-product-virtual Patch
@graphcommerce/magento-product Patch
@graphcommerce/magento-recently-viewed-products Patch
@graphcommerce/magento-review Patch
@graphcommerce/magento-search-overlay Patch
@graphcommerce/magento-search Patch
@graphcommerce/magento-store Patch
@graphcommerce/magento-wishlist Patch
@graphcommerce/mollie-magento-payment Patch
@graphcommerce/next-ui Patch
@graphcommerce/react-hook-form Patch
@graphcommerce/service-worker Patch
@graphcommerce/storyblok-ui Patch
@graphcommerce/framer-next-pages-example Patch
@graphcommerce/framer-scroller-example Patch
@graphcommerce/image-example 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

Comment thread packages/magento-customer/hooks/useAddressAttributes.ts Outdated
paales and others added 4 commits August 10, 2026 12:32
…s telephone

`CartAddressInput.telephone` is a non-nullable `String!`, so the address forms
always had to send a value. Because the mutation variable is declared as an
optional `String`, `required.telephone` resolved to `false`, the field rendered
as optional and `onBeforeSubmit` substituted a `000 - 000 0000` placeholder for
an empty value — storing a fake phone number on the order.

Magento does expose whether an address requires a telephone:
`attributesForm(formCode: "customer_address_edit")` returns `is_required` per
address attribute and reflects `customer/address/telephone_show`.

- Add `useAddressAttributesRequired()` / `useTelephoneRequired()` to
  `@graphcommerce/magento-customer`, built on the existing `useAttributesForm`
  query, plus a shared `placeholderTelephone` constant.
- `ShippingAddressForm`, `EditBillingAddressForm` and `EditAddressForm` mark the
  telephone field required when Magento says so, and submit an empty string
  instead of the placeholder when Magento says it is optional. The placeholder
  is only used while the metadata hasn't loaded, so shops that require a
  telephone keep working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d it

Review feedback: drop the `useAddressAttributes` indirection.

- Remove `hooks/useAddressAttributes.ts` (`useAddressAttributesRequired`,
  `useTelephoneRequired`, `placeholderTelephone`). Every address form now calls
  `useAttributesForm({ formCode: 'customer_address_edit' })` itself and reads
  `is_required` off the telephone attribute; the placeholder string is inline
  again, matching how it was already written in the surrounding code.
- Preload the metadata in `getStaticProps` the same way the account pages
  preload `customer_account_create` / `customer_account_edit`:
  `if (magentoVersion >= 247) await preloadAttributesForm(client, 'customer_address_edit')`
  on the checkout, checkout-address-edit, billing-address-edit and account
  address-edit pages of all three examples. That also closes the window where a
  customer could submit the form before the attribute metadata resolved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback: the `000 - 000 0000` fallback only existed because the forms
couldn't tell whether the telephone was conditionally required. Now that they
read `is_required` from the address attribute metadata, it has no reason to
exist.

`ShippingAddressForm`, `EditBillingAddressForm` and `EditAddressForm` submit
`variables.telephone || ''` — an empty string satisfies the non-nullable
`CartAddressInput.telephone`, and Magento validates it against the very same
`is_required` the form renders with
(`Magento\Customer\Model\Address\Validator\General::checkOptionalFields` reads
`eavConfig->getAttribute('customer_address', 'telephone')->getIsRequired()`).

Behavior change: a customer who bypasses the client-side validation on a shop
that requires a telephone now gets `"telephone" is required. Enter and try
again.` instead of an order carrying a fake number. Verified against Magento
2.4.9 with `customer/address/telephone_show = req`.

The placeholder string survives only as a `legacyPlaceholderTelephone` constant
per form, used to clear it out of addresses that were saved with it before this
change so the customer is asked for a real number.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`attributesForm` — and with it any way to know whether a shop requires a
telephone — only exists since Magento 2.4.7. Below that the placeholder flow is
still the active behavior, so neither stripping a stored placeholder nor
submitting an empty string is correct there.

Both directions now live in one helper,
`packages/magento-customer/utils/legacyPlaceholderTelephone.ts`:

- `stripLegacyPlaceholderTelephone(telephone)` — clears a stored
  `000 - 000 0000` when loading an address into a form; returns the value
  untouched below 2.4.7.
- `applyLegacyPlaceholderTelephone(telephone)` — submits an empty string for an
  empty field so Magento validates it; falls back to the placeholder below
  2.4.7, where the form can never know the field is required.

`ShippingAddressForm`, `EditBillingAddressForm` and `EditAddressForm` import the
helper instead of each carrying a local constant. The required-status derivation
needs no extra gate: below 2.4.7 `attributesForm` yields nothing, so
`telephoneRequired` stays `undefined` and the field renders exactly as it did
before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant