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
Donations, metadata and gift options
Building the checkoutPage 12 of 31Updated
The three most requested checkout additions, each with a working example: order metadata fields for extra answers, gift options built from metadata fields, and a donation added as a product in the cart.
Who this is for. Developers adding questions, gift details or a donation step to a checkout page. After this page you can render and store order metadata fields, build gift options from them, and add an optional donation without losing the shopper's place.
Order metadata fields
Order metadata fields are the platform's way to collect extra answers on the checkout and keep them with the order: a delivery instruction, a gift message, a "how did you hear about us". Each field is a record defined in the admin, with a name, an identifier and a data type, which is one of three:
Data type
Stored as
Input
text
Text, up to 190 characters
A text input with maxlength="190"
number
A decimal number
A number input
boolean
Yes or no
A choice of yes, no or no answer
The checkout page receives every metadata field defined for the site as metadataFields, not only the ones that suit the current cart, so choosing which to show is the template's job. A metadata field has no required setting: when an answer is required, mark the input required on the page and check it before the purchase call.
Rendering the fields
A metadata field is a plain record with no layout areas, so read it with its getters: getId() and getDataType(). The template below names the fields it shows, by id, with the wording for each, and renders the right input for each data type:
Template codecheckouts/single-page.html
{* the fields this page asks for, by metadata field id, with the wording to show *}
{$questions = [4 => 'Gift message', 5 => 'This is a gift', 6 => 'Gift wrap this order']}
{foreach $metadataFields as $field}
{if isset($questions[$field->getId()])}
<label for="metadata-{$field->getId()}">{$questions[$field->getId()]}</label>
{if $field->getDataType() === 'boolean'}
<select id="metadata-{$field->getId()}" data-metadata-field="{$field->getId()}">
<option value=""></option>
<option value="1">Yes</option>
<option value="0">No</option>
</select>
{elseif $field->getDataType() === 'number'}
<input type="number" id="metadata-{$field->getId()}" data-metadata-field="{$field->getId()}">
{else}
<input type="text" id="metadata-{$field->getId()}" data-metadata-field="{$field->getId()}" maxlength="190">
{/if}
{/if}
{/foreach}
{# the fields this page asks for, by metadata field id, with the wording to show #}
{% set questions = {4: 'Gift message', 5: 'This is a gift', 6: 'Gift wrap this order'} %}
{% for field in metadataFields %}
{% if questions[field.getId()] is defined %}
<label for="metadata-{{ field.getId() }}">{{ questions[field.getId()] }}</label>
{% if field.getDataType() == 'boolean' %}
<select id="metadata-{{ field.getId() }}" data-metadata-field="{{ field.getId() }}">
<option value=""></option>
<option value="1">Yes</option>
<option value="0">No</option>
</select>
{% elseif field.getDataType() == 'number' %}
<input type="number" id="metadata-{{ field.getId() }}" data-metadata-field="{{ field.getId() }}">
{% else %}
<input type="text" id="metadata-{{ field.getId() }}" data-metadata-field="{{ field.getId() }}" maxlength="190">
{% endif %}
{% endif %}
{% endfor %}
On a multilingual site, put the wording in the shopper's language the same way you translate the rest of the page's copy; the field's admin name is for the people who define it.
Storing the values
Values are written with PUT /cdna-api/checkout/<id>, in the checkout's metadataFieldValues field, one of the four fields a page can write. A new value names its field under metadataField:
To change a value already stored, send the value's own id with the new value instead. The values are kept with the order once the checkout is paid.
Write them with one generic component, not one per field. It collects every metadata input on the page and sends them all in one PUT through the page's API layer whenever one changes:
Checkout is the page's API layer (One API layer). A field added in the admin later needs only a line in the template's list; the component picks it up unchanged.
Gift options are metadata fields
A gift message, a "this is a gift" flag and a gift-wrap choice are not a separate feature: they are ordinary metadata fields, defined in the admin like any other. The example above is the whole of it:
Gift message: a text field. The input's maxlength="190" matches what a text value holds, so the shopper sees the limit while typing.
This is a gift: a boolean field.
Gift wrap: another boolean field. If wrapping costs extra, sell it as a product in the cart, the way a donation is added below, so the charge is part of the total.
Show the message field only when the shopper has said the order is a gift, with a few lines of script on the flag's change event; the values arrive with the order either way.
A donation is a product
A donation added during checkout is not a separate feature either: it is a product. The production store models each donation amount as its own product (DONATE-5, DONATE-10 and so on) and adds the chosen one to the cart like any other item, with POST /cdna-api/cart/item/<code> and a quantity of 1. Its price comes from the product's price row in the cart's currency, like any item (Prices and currencies). The current choice is read back by checking which of those product codes is among the checkout's items.
A change to the cart ends the checkout attached to it (From cart to order), so changing the amount takes four steps: save what the shopper has typed, remove the donation line already in the cart, add the new one (or none), then create the checkout again with POST /checkout and let the page reload.
JavaScriptcheckout/components/donation-handler.js
const DONATION_CODES = ['DONATE-5', 'DONATE-10', 'DONATE-25'];
async function setDonation(newCode) {
saveFormData(); // keep the shopper's typing through the reload
const current = CheckoutState.getItems()
.find((item) => DONATION_CODES.includes(item.productCode));
if (current) await removeCartItem(current); // the cart's own remove call
if (newCode) await addCartItem(newCode, 1); // POST /cdna-api/cart/item/<code>
recreateCheckout(); // POST /checkout again; the page reloads
}
To show the donation apart from the other items in the summary, a template can recognise its line by the product's category instead of a list of codes. The production store files the donation products in a category named "Donation" and checks each line's primary category:
Template code
{$category = $item->getProduct()->getPrimaryCategory()}
{if $category && $category->getName()|lower == 'donation'}
{* this line goes in the donation section of the summary *}
{else}
{* this line goes with the other items *}
{/if}
{% set category = item.getProduct().getPrimaryCategory() %}
{% if category and category.getName()|lower == 'donation' %}
{# this line goes in the donation section of the summary #}
{% else %}
{# this line goes with the other items #}
{% endif %}
Either way of marking a donation line, a list of product codes or a category, works; choose one and use it everywhere. The line's stored name is the donation product's name in the client's default language, so on a multilingual site print it from the product's description in the shopper's language (Its currency, and the language of its lines).
A donation with no purchase around it, where the shopper only gives, is a payment of its own rather than a checkout addition; see Payment methods and how a payment runs.