A Core dna store is a set of records and an API that moves a purchase from cart to order: products described in every language and priced in every currency the store sells in, a cart, a checkout that becomes an order, invoices, payments, tax and shipping. The site builds the storefront on top: the cart and checkout pages, their scripts, and the customer emails.
L2–L3
Unit 03 · Building the checkout
Addresses and shipping at checkout
Building the checkoutPage 10 of 31Updated
Write the shopper's email and addresses to the checkout, add a places autocomplete without taking manual entry away, then create shipping, show its rates, and handle an address that no shipping method covers.
Who this is for. Developers building the address and delivery steps of a checkout page. After this page you can collect both addresses, pre-fill them for a member, offer address suggestions that keep the form accessible, and present shipping rates, including when there are none.
Writing the email and addresses
The person's email and both addresses are written with PUT /cdna-api/checkout/<id>, as nested objects:
The other address fields are addressLine2, companyName, unit and city. countryName is matched against the country list by its full name, so send "Australia", not "AU". stateName is matched within that country, and kept as plain text when it matches none of its states.
Billing, delivery and the address book
A billing address is one of the four conditions for payment; a delivery address is not (What "ready for payment" requires). For delivery, the shipping call takes the address in its own body (below), so send it the same values you saved as deliveryAddress. A "same as delivery" switch on the page copies the delivery fields into the billing fields before the PUT.
Checkout addresses are records of the checkout's own. They do not point at a member's address book, and address types (billing or shipping) belong to the address book, not to the checkout. A member's saved address can pre-fill the checkout's fields: read the entry in the checkout template and print its values into the inputs, as Addresses and the address book shows. The production store this unit draws on does this when a signed-in member has no delivery address on the checkout yet: it takes the member's primary saved address, or the first one, and copies it into both the delivery and the billing fields.
The email address
A signed-in member checks out with their own email only. Any other is refused: "The checkout email must match your profile email."
A guest who enters the email of an existing account is refused with "This email is already associated with another account" (error.email.already_in_use). Put a sign-in link beside the email field for this case.
A member who enters their own email again changes nothing.
What guest and account checkout each create, and a check for an existing account before the shopper pays, are on Guest checkout and the captcha.
Autofill tokens and a places autocomplete
Every address input carries the HTML autocomplete token for what it collects, prefixed with the section it belongs to: shipping for the delivery address, billing for the billing address. This is WCAG 2.1 success criterion 1.3.5, Identify Input Purpose: browsers and assistive technology use the token to fill the field and to tell the shopper what it is for.
Field
Token
firstName
given-name
addressLine1
address-line1
addressLine2
address-line2
suburb
address-level2
stateName
address-level1
postcode
postal-code
phone
tel
One include renders both addresses, so the section and the field-name prefix are parameters:
{* included twice: namePrefix='deliveryAddress' with autocompleteSection='shipping',
and namePrefix='billingAddress' with autocompleteSection='billing';
address is the address to pre-fill, or null *}
<label for="{$namePrefix}-addressLine1">Address</label>
<input type="text" id="{$namePrefix}-addressLine1" name="{$namePrefix}.addressLine1"
value="{if $address}{$address->getAddressLine1()|escape}{/if}"
autocomplete="{$autocompleteSection} address-line1" required>
{# included twice: namePrefix 'deliveryAddress' with autocompleteSection 'shipping',
and namePrefix 'billingAddress' with autocompleteSection 'billing';
address is the address to pre-fill, or null #}
<label for="{{ namePrefix }}-addressLine1">Address</label>
<input type="text" id="{{ namePrefix }}-addressLine1" name="{{ namePrefix }}.addressLine1"
value="{{ address ? address.getAddressLine1()|e : '' }}"
autocomplete="{{ autocompleteSection }} address-line1" required>
A places autocomplete on top of these fields assists the shopper; it does not replace the fields. The input stays a plain text field the shopper can type into, its autocomplete token stays, and an address the service does not know can still be entered by hand. The pattern the production store follows:
Attach the suggestions to the existing addressLine1 input.
Wait until typing pauses (300 ms) and the input holds at least three characters before asking the service.
Show the suggestions in a list that Escape or a click outside dismisses, without ever blocking typing.
On a chosen suggestion, fill the sibling fields by name (addressLine2, suburb, stateName, postcode, countryName) and fire a bubbling input event on each, so that validation, the "same as delivery" copy and the saved-typing component all see the change.
// fill the named fields from a chosen suggestion
function fillAddress(prefix, parts) {
for (const [field, value] of Object.entries(parts)) {
const input = document.querySelector(`[name="${prefix}.${field}"]`);
if (!input) continue;
input.value = value;
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}
fillAddress('deliveryAddress', {
addressLine1: '1 High Street', suburb: 'Fitzroy',
stateName: 'Victoria', postcode: '3065', countryName: 'Australia',
});
Which places service you use, and its terms, are your choice; the checkout only ever receives the field values.
Creating shipping and showing the rates
POST /cdna-api/checkout/<id>/shipping/simple-delivery takes the delivery address (countryName, addressLine1 and postcode are required) and builds one fulfilment for all available items, with a rate from each shipping method that applies. For collection, simple-pickup takes a warehouse instead.
A method applies when it is active and its shipping categories cover every item's shipping category: the product's own, or Shipping.defaultShippingCategory when the product has none, with a variant using its product's. For pickup, the method's warehouses must also include the one chosen. Methods and categories are set up as in Shipping methods and their templates.
Ask for only the rates with a properties selector:
HTTP
POST /cdna-api/checkout/<id>/shipping/simple-delivery?properties={ fulfillments { shippingRates { id, name, total } } }
{"result":"success","message":"Shipping created",
"data":{"fulfillments":[{"shippingRates":[{"id":55,"name":"Standard","total":"10.0000"}]}]}}
Then select one with PUT /cdna-api/checkout/<id>/shipping/55. Build the selector string once in the API layer and use it on both calls. Show each rate's name and total; like every amount on the checkout, the total is in the checkout's currency (Its currency, and the language of its lines), so print it with the currency code the page was seeded with.
A rate lasts an hour
A rate expires an hour after it is created (Shipping.rates.lifetime, 3,600 seconds), and a checkout whose selected rate has expired is not ready for payment: its warning is "A fulfillment's selected shipping rate has expired". When a shopper comes back after an hour, create shipping again and let them choose from the new rates. Select only from the latest list: a rate id from an earlier list, or from another fulfilment, is refused.
When no method covers the address
An address that no shipping method covers is not an error. The call succeeds ("result":"success") with an empty shippingRates list. The checkout cannot be paid until a rate is selected, so the page has to say why, in words, with a next step:
JavaScript
const rates = response.data.fulfillments.flatMap((fulfillment) => fulfillment.shippingRates);
if (rates.length === 0) {
showShippingMessage(
"We can't deliver to this address yet. Check the postcode and country, or contact us."
);
return; // the pay button stays disabled, with this message beside it
}
renderRates(rates);
Never leave an empty list or a disabled control without an explanation: to a shopper, that looks like a broken page. While testing, an empty list usually means that no active method covers the test products' shipping categories or the test address; check the methods as described in Shipping methods and their templates. A store that sells only goods that are not shipped needs a method that returns a free rate for every address, so that an empty list is never a shopper's normal path.