import type {CustomerAddressInput} from '@shopify/hydrogen/customer-account-api-types'; import type { AddressFragment, CustomerFragment, } from 'customer-accountapi.generated'; import { data, Form, useActionData, useNavigation, useOutletContext, type Fetcher, } from 'react-router'; import type {Route} from './+types/account.addresses'; import { UPDATE_ADDRESS_MUTATION, DELETE_ADDRESS_MUTATION, CREATE_ADDRESS_MUTATION, } from '~/graphql/customer-account/CustomerAddressMutations'; export type ActionResponse = { addressId?: string | null; createdAddress?: AddressFragment; defaultAddress?: string | null; deletedAddress?: string | null; error: Record | null; updatedAddress?: AddressFragment; }; export const meta: Route.MetaFunction = () => { return [{title: 'Addresses'}]; }; export async function loader({context}: Route.LoaderArgs) { await context.customerAccount.handleAuthStatus(); return {}; } export async function action({request, context}: Route.ActionArgs) { const {customerAccount} = context; try { const form = await request.formData(); const addressId = form.has('addressId') ? String(form.get('addressId')) : null; if (!addressId) { throw new Error('You must provide an address id.'); } // this will ensure redirecting to login never happen for mutatation const isLoggedIn = await customerAccount.isLoggedIn(); if (!isLoggedIn) { return data( {error: {[addressId]: 'Unauthorized'}}, { status: 401, }, ); } const defaultAddress = form.has('defaultAddress') ? String(form.get('defaultAddress')) === 'on' : false; const address: CustomerAddressInput = {}; const keys: (keyof CustomerAddressInput)[] = [ 'address1', 'address2', 'city', 'company', 'territoryCode', 'firstName', 'lastName', 'phoneNumber', 'zoneCode', 'zip', ]; for (const key of keys) { const value = form.get(key); if (typeof value === 'string') { address[key] = value; } } switch (request.method) { case 'POST': { // handle new address creation try { const {data, errors} = await customerAccount.mutate( CREATE_ADDRESS_MUTATION, { variables: { address, defaultAddress, language: customerAccount.i18n.language, }, }, ); if (errors?.length) { throw new Error(errors[0].message); } if (data?.customerAddressCreate?.userErrors?.length) { throw new Error(data?.customerAddressCreate?.userErrors[0].message); } if (!data?.customerAddressCreate?.customerAddress) { throw new Error('Customer address create failed.'); } return { error: null, createdAddress: data?.customerAddressCreate?.customerAddress, defaultAddress, }; } catch (error: unknown) { if (error instanceof Error) { return data( {error: {[addressId]: error.message}}, { status: 400, }, ); } return data( {error: {[addressId]: error}}, { status: 400, }, ); } } case 'PUT': { // handle address updates try { const {data, errors} = await customerAccount.mutate( UPDATE_ADDRESS_MUTATION, { variables: { address, addressId: decodeURIComponent(addressId), defaultAddress, language: customerAccount.i18n.language, }, }, ); if (errors?.length) { throw new Error(errors[0].message); } if (data?.customerAddressUpdate?.userErrors?.length) { throw new Error(data?.customerAddressUpdate?.userErrors[0].message); } if (!data?.customerAddressUpdate?.customerAddress) { throw new Error('Customer address update failed.'); } return { error: null, updatedAddress: address, defaultAddress, }; } catch (error: unknown) { if (error instanceof Error) { return data( {error: {[addressId]: error.message}}, { status: 400, }, ); } return data( {error: {[addressId]: error}}, { status: 400, }, ); } } case 'DELETE': { // handles address deletion try { const {data, errors} = await customerAccount.mutate( DELETE_ADDRESS_MUTATION, { variables: { addressId: decodeURIComponent(addressId), language: customerAccount.i18n.language, }, }, ); if (errors?.length) { throw new Error(errors[0].message); } if (data?.customerAddressDelete?.userErrors?.length) { throw new Error(data?.customerAddressDelete?.userErrors[0].message); } if (!data?.customerAddressDelete?.deletedAddressId) { throw new Error('Customer address delete failed.'); } return {error: null, deletedAddress: addressId}; } catch (error: unknown) { if (error instanceof Error) { return data( {error: {[addressId]: error.message}}, { status: 400, }, ); } return data( {error: {[addressId]: error}}, { status: 400, }, ); } } default: { return data( {error: {[addressId]: 'Method not allowed'}}, { status: 405, }, ); } } } catch (error: unknown) { if (error instanceof Error) { return data( {error: error.message}, { status: 400, }, ); } return data( {error}, { status: 400, }, ); } } export default function Addresses() { const {customer} = useOutletContext<{customer: CustomerFragment}>(); const {defaultAddress, addresses} = customer; return (

Addresses


Create address



{!addresses.nodes.length ? (

You have no addresses saved.

) : ( )}
); } function NewAddressForm() { const newAddress = { address1: '', address2: '', city: '', company: '', territoryCode: '', firstName: '', id: 'new', lastName: '', phoneNumber: '', zoneCode: '', zip: '', } as CustomerAddressInput; return ( {({stateForMethod}) => (
)}
); } function ExistingAddresses({ addresses, defaultAddress, }: Pick) { return (
Existing addresses {addresses.nodes.map((address) => ( {({stateForMethod}) => (
)}
))}
); } export function AddressForm({ addressId, address, defaultAddress, children, }: { addressId: AddressFragment['id']; address: CustomerAddressInput; defaultAddress: CustomerFragment['defaultAddress']; children: (props: { stateForMethod: (method: 'PUT' | 'POST' | 'DELETE') => Fetcher['state']; }) => React.ReactNode; }) { const {state, formMethod} = useNavigation(); const action = useActionData(); const error = action?.error?.[addressId]; const isDefaultAddress = defaultAddress?.id === addressId; return (
{error ? (

{error}

) : (
)} {children({ stateForMethod: (method) => (formMethod === method ? state : 'idle'), })}
); }