Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/address-telephone-required-from-magento.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@graphcommerce/magento-cart-shipping-address': patch
'@graphcommerce/magento-customer': patch
'@graphcommerce/magento-cart': patch
---

Address forms now honor Magento's `customer/address/telephone_show` configuration instead of silently submitting a `000 - 000 0000` placeholder 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` and the field rendered as optional, after which `onBeforeSubmit` substituted the placeholder. Every checkout that skipped the field therefore stored a fake phone number on the order.

Magento does expose whether a telephone is required — `attributesForm(formCode: "customer_address_edit")` returns `is_required` per address attribute and reflects `customer/address/telephone_show`. `ShippingAddressForm`, `EditBillingAddressForm` and `EditAddressForm` now read that metadata through the existing `useAttributesForm` hook and mark the field required accordingly. The placeholder is gone: an empty telephone is submitted as an empty string, which Magento validates against the very same `is_required` (`Magento\Customer\Model\Address\Validator\General::checkOptionalFields`).

This is a deliberate behavior change. Where a customer who bypassed the client-side validation previously got a successful order carrying a fake phone number, a shop that requires a telephone now gets a proper `"telephone" is required. Enter and try again.` validation error. Addresses that still carry the old placeholder are cleared when they are loaded into a form, so the customer fills in a real number instead of being shown zeroes.

`attributesForm` only exists since Magento 2.4.7, so all of this is version-gated in `stripLegacyPlaceholderTelephone()` / `applyLegacyPlaceholderTelephone()` (`@graphcommerce/magento-customer`): below 2.4.7 the placeholder is still submitted and no longer stripped, exactly as before.

Pages that render these forms should preload the metadata in `getStaticProps` with `await preloadAttributesForm(client, 'customer_address_edit')`, the same way the account pages already preload `customer_account_create` / `customer_account_edit`. The examples do this for the checkout and address pages, which also closes the window where a customer could submit before the metadata resolved.
5 changes: 4 additions & 1 deletion examples/magento-graphcms/pages/account/addresses/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
AccountDashboardAddressesDocument,
getCustomerAccountIsDisabled,
} from '@graphcommerce/magento-customer'
import { PageMeta, StoreConfigDocument } from '@graphcommerce/magento-store'
import { PageMeta, preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import {
GetStaticProps,
iconAddresses,
Expand Down Expand Up @@ -82,6 +83,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
const up = { href: '/account/addresses', title: t`Addresses` }
const conf = client.query({ query: StoreConfigDocument })

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

return {
props: {
apolloState: await conf.then(() => client.cache.extract()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
AccountDashboardAddressesDocument,
getCustomerAccountIsDisabled,
} from '@graphcommerce/magento-customer'
import { PageMeta, StoreConfigDocument } from '@graphcommerce/magento-store'
import { PageMeta, preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import {
GetStaticProps,
iconAddresses,
Expand Down Expand Up @@ -98,6 +99,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
const up = { href: '/checkout', title: t`Shipping` }
const conf = client.query({ query: StoreConfigDocument })

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

return {
props: {
apolloState: await conf.then(() => client.cache.extract()),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { PageOptions } from '@graphcommerce/framer-next-pages'
import { cacheFirst } from '@graphcommerce/graphql'
import { getCheckoutIsDisabled, EditBillingAddressForm } from '@graphcommerce/magento-cart'
import { StoreConfigDocument } from '@graphcommerce/magento-store'
import { preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import { GetStaticProps, PageMeta, LayoutOverlayHeader, LayoutTitle } from '@graphcommerce/next-ui'
import { t } from '@lingui/core/macro'
import { Trans } from '@lingui/react/macro'
Expand Down Expand Up @@ -61,6 +62,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
fetchPolicy: cacheFirst(staticClient),
})

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

return {
props: {
...(await layout).data,
Expand Down
5 changes: 4 additions & 1 deletion examples/magento-graphcms/pages/checkout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
} from '@graphcommerce/magento-cart-shipping-address'
import { ShippingMethodForm } from '@graphcommerce/magento-cart-shipping-method'
import { CustomerDocument, useCustomerQuery } from '@graphcommerce/magento-customer'
import { PageMeta, StoreConfigDocument } from '@graphcommerce/magento-store'
import { PageMeta, preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import {
FormActions,
GetStaticProps,
Expand Down Expand Up @@ -163,6 +164,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
fetchPolicy: cacheFirst(staticClient),
})

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

return {
props: {
...(await layout).data,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
useCustomerQuery,
WaitForCustomer,
} from '@graphcommerce/magento-customer'
import { PageMeta, StoreConfigDocument } from '@graphcommerce/magento-store'
import { PageMeta, preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import type { GetStaticProps } from '@graphcommerce/next-ui'
import {
iconAddresses,
Expand Down Expand Up @@ -83,6 +84,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
const up = { href: '/account/addresses', title: t`Addresses` }
const conf = client.query({ query: StoreConfigDocument })

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

return {
props: {
apolloState: await conf.then(() => client.cache.extract()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
getCustomerAccountIsDisabled,
useCustomerQuery,
} from '@graphcommerce/magento-customer'
import { PageMeta, StoreConfigDocument } from '@graphcommerce/magento-store'
import { PageMeta, preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import type { GetStaticProps } from '@graphcommerce/next-ui'
import {
FullPageMessage,
Expand Down Expand Up @@ -99,6 +100,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
const up = { href: '/checkout', title: t`Shipping` }
const conf = client.query({ query: StoreConfigDocument })

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

return {
props: {
apolloState: await conf.then(() => client.cache.extract()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { PageOptions } from '@graphcommerce/framer-next-pages'
import { cacheFirst } from '@graphcommerce/graphql'
import { EditBillingAddressForm, getCheckoutIsDisabled } from '@graphcommerce/magento-cart'
import { getBillingAddressPermission } from '@graphcommerce/magento-customer'
import { StoreConfigDocument } from '@graphcommerce/magento-store'
import { preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import type { GetStaticProps } from '@graphcommerce/next-ui'
import { LayoutOverlayHeader, LayoutTitle, PageMeta } from '@graphcommerce/next-ui'
import { t } from '@lingui/core/macro'
Expand Down Expand Up @@ -63,6 +64,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
fetchPolicy: cacheFirst(staticClient),
})

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

return {
props: {
...(await layout).data,
Expand Down
5 changes: 4 additions & 1 deletion examples/magento-open-source/pages/checkout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
} from '@graphcommerce/magento-cart-shipping-address'
import { ShippingMethodForm } from '@graphcommerce/magento-cart-shipping-method'
import { CustomerDocument, useCustomerQuery } from '@graphcommerce/magento-customer'
import { PageMeta, StoreConfigDocument } from '@graphcommerce/magento-store'
import { PageMeta, preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import type { GetStaticProps } from '@graphcommerce/next-ui'
import {
FormActions,
Expand Down Expand Up @@ -164,6 +165,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
fetchPolicy: cacheFirst(staticClient),
})

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

return {
props: {
...(await layout).data,
Expand Down
5 changes: 4 additions & 1 deletion examples/magento-storyblok/pages/account/addresses/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
useCustomerQuery,
WaitForCustomer,
} from '@graphcommerce/magento-customer'
import { PageMeta, StoreConfigDocument } from '@graphcommerce/magento-store'
import { PageMeta, preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import type { GetStaticProps } from '@graphcommerce/next-ui'
import {
iconAddresses,
Expand Down Expand Up @@ -83,6 +84,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
const up = { href: '/account/addresses', title: t`Addresses` }
const conf = client.query({ query: StoreConfigDocument })

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

return {
props: {
apolloState: await conf.then(() => client.cache.extract()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
getCustomerAccountIsDisabled,
useCustomerQuery,
} from '@graphcommerce/magento-customer'
import { PageMeta, StoreConfigDocument } from '@graphcommerce/magento-store'
import { PageMeta, preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import type { GetStaticProps } from '@graphcommerce/next-ui'
import {
FullPageMessage,
Expand Down Expand Up @@ -99,6 +100,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
const up = { href: '/checkout', title: t`Shipping` }
const conf = client.query({ query: StoreConfigDocument })

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

return {
props: {
apolloState: await conf.then(() => client.cache.extract()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { PageOptions } from '@graphcommerce/framer-next-pages'
import { cacheFirst } from '@graphcommerce/graphql'
import { EditBillingAddressForm, getCheckoutIsDisabled } from '@graphcommerce/magento-cart'
import { getBillingAddressPermission } from '@graphcommerce/magento-customer'
import { StoreConfigDocument } from '@graphcommerce/magento-store'
import { preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import type { GetStaticProps } from '@graphcommerce/next-ui'
import { LayoutOverlayHeader, LayoutTitle, PageMeta } from '@graphcommerce/next-ui'
import { t } from '@lingui/core/macro'
Expand Down Expand Up @@ -63,6 +64,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
fetchPolicy: cacheFirst(staticClient),
})

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

return {
props: {
...(await layout).data,
Expand Down
5 changes: 4 additions & 1 deletion examples/magento-storyblok/pages/checkout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
} from '@graphcommerce/magento-cart-shipping-address'
import { ShippingMethodForm } from '@graphcommerce/magento-cart-shipping-method'
import { CustomerDocument, useCustomerQuery } from '@graphcommerce/magento-customer'
import { PageMeta, StoreConfigDocument } from '@graphcommerce/magento-store'
import { PageMeta, preloadAttributesForm, StoreConfigDocument } from '@graphcommerce/magento-store'
import { magentoVersion } from '@graphcommerce/next-config/config'
import type { GetStaticProps } from '@graphcommerce/next-ui'
import {
FormActions,
Expand Down Expand Up @@ -166,6 +167,8 @@ export const getStaticProps: GetPageStaticProps = async (context) => {
})
const globalConfig = fetchGlobalConfig(context)

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

return {
props: {
...(await layout).data,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ import {
import type { CartAddressFragment } from '@graphcommerce/magento-cart'
import {
AddressFields,
applyLegacyPlaceholderTelephone,
CompanyFields,
CustomerDocument,
NameFields,
stripLegacyPlaceholderTelephone,
useBillingAddressPermission,
useCustomerQuery,
} from '@graphcommerce/magento-customer'
import { CountryRegionsDocument, StoreConfigDocument } from '@graphcommerce/magento-store'
import {
CountryRegionsDocument,
StoreConfigDocument,
useAttributesForm,
} from '@graphcommerce/magento-store'
import { customerAddressNoteEnable } from '@graphcommerce/next-config/config'
import { Form, FormRow } from '@graphcommerce/next-ui'
import { Trans } from '@lingui/react/macro'
Expand Down Expand Up @@ -57,6 +63,11 @@ export const ShippingAddressForm = React.memo<ShippingAddressFormProps>((props)

const billingAddressReadonly = useBillingAddressPermission() === 'READONLY'

// Magento's address attribute metadata tells us whether a telephone is required, as configured by
// `customer/address/telephone_show`.
const addressAttributes = useAttributesForm({ formCode: 'customer_address_edit' })
const telephoneRequired = addressAttributes.find((a) => a.code === 'telephone')?.is_required

const shopCountry = config?.storeConfig?.locale?.split('_')?.[1].toUpperCase()

const shippingAddress = cartQuery?.cart?.shipping_addresses?.[0]
Expand Down Expand Up @@ -102,8 +113,7 @@ export const ShippingAddressForm = React.memo<ShippingAddressFormProps>((props)
// todo(paales): change to something more sustainable
firstname: currentAddress?.firstname ?? customerQuery?.customer?.firstname ?? '',
lastname: currentAddress?.lastname ?? customerQuery?.customer?.lastname ?? '',
telephone:
currentAddress?.telephone !== '000 - 000 0000' ? currentAddress?.telephone : '',
telephone: stripLegacyPlaceholderTelephone(currentAddress?.telephone),
city: currentAddress?.city ?? '',
company: currentAddress?.company ?? '',
vatId: currentAddress?.vat_id ?? '',
Expand All @@ -130,7 +140,11 @@ export const ShippingAddressForm = React.memo<ShippingAddressFormProps>((props)

return {
...variables,
telephone: variables.telephone || '000 - 000 0000',
// `CartAddressInput.telephone` is non-nullable, so an empty string is sent rather than
// nothing. Magento validates it against the same `is_required` we render the field with, so
// a shop that requires a telephone returns a proper validation error instead of silently
// accepting a fake number.
telephone: applyLegacyPlaceholderTelephone(variables.telephone),
region: regionId ? variables.region : '',
regionId,
addition: variables.addition ?? '',
Expand Down Expand Up @@ -159,7 +173,7 @@ export const ShippingAddressForm = React.memo<ShippingAddressFormProps>((props)
control={form.control}
name='telephone'
variant='outlined'
required={required.telephone}
required={required.telephone || telephoneRequired === true}
showValid
/>
</FormRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
import {
AddressFields,
ApolloCustomerErrorAlert,
applyLegacyPlaceholderTelephone,
CompanyFields,
NameFields,
stripLegacyPlaceholderTelephone,
} from '@graphcommerce/magento-customer'
import { CountryRegionsDocument } from '@graphcommerce/magento-store'
import { CountryRegionsDocument, useAttributesForm } from '@graphcommerce/magento-store'
import { Button, Form, FormActions, FormDivider, FormRow } from '@graphcommerce/next-ui'
import { Trans } from '@lingui/react/macro'
import type { SxProps, Theme } from '@mui/material'
Expand All @@ -28,6 +30,11 @@ export function EditBillingAddressForm(props: EditBillingAddressFormProps) {

const goToCheckout = useHistoryGo({ href: '/checkout/payment' })

// Magento's address attribute metadata tells us whether a telephone is required, as configured by
// `customer/address/telephone_show`.
const addressAttributes = useAttributesForm({ formCode: 'customer_address_edit' })
const telephoneRequired = addressAttributes.find((a) => a.code === 'telephone')?.is_required

const form = useFormGqlMutationCart(SetBillingAddressDocument, {
defaultValues: {
firstname: address?.firstname,
Expand All @@ -36,7 +43,7 @@ export function EditBillingAddressForm(props: EditBillingAddressFormProps) {
city: address?.city,
countryCode: address?.country.code,
street: address?.street?.[0] ?? '',
telephone: address?.telephone,
telephone: stripLegacyPlaceholderTelephone(address?.telephone),
houseNumber: address?.street?.[1] ?? '',
addition: address?.street?.[2] ?? '',
company: address?.company ?? '',
Expand All @@ -56,7 +63,9 @@ export function EditBillingAddressForm(props: EditBillingAddressFormProps) {

return {
...variables,
telephone: variables.telephone || '000 - 000 0000',
// See ShippingAddressForm: `CartAddressInput.telephone` is non-nullable, so send an empty
// string rather than a fake number and let Magento validate it.
telephone: applyLegacyPlaceholderTelephone(variables.telephone),
regionId,
}
},
Expand All @@ -77,7 +86,7 @@ export function EditBillingAddressForm(props: EditBillingAddressFormProps) {
<FormRow>
<TelephoneElement
variant='outlined'
required={required.telephone}
required={required.telephone || telephoneRequired === true}
control={control}
name='telephone'
disabled={formState.isSubmitting}
Expand Down
Loading
Loading