Skip to content

Order form, cart and accounts

Build the cash-on-delivery order form, an add-to-cart flow and optional customer accounts.

The order form on the product page is Sworen’s main checkout: the shopper fills in their name, phone and city and orders, with no account and no payment up front.

<form {...formAttrs('order', { class: 'order-form' })}>
{formFields('order')}
<If condition={errors && errors.length > 0}>
<ul class="form-errors" role="alert">{errors.map(e => <li>{e}</li>)}</ul>
</If>
<input type="text" name="customer_name" required autocomplete="name" placeholder={'Full name' | t} />
<input type="tel" name="phone" required autocomplete="tel" placeholder={'Phone' | t} />
<input type="text" name="city" required placeholder={'City' | t} />
<input type="text" name="address" placeholder={'Address' | t} />
<If condition={product.has_variants}>
<select name="variant_id" required>
{product.variants.map(v => <option value={v.id} disabled={!v.available}>{v.title}</option>)}
</select>
</If>
<input type="number" name="quantity" value="1" min="1" />
<If condition={formData('order').payments.length > 1}>
<fieldset>
{formData('order').payments.map((p, i) => (
<label><input type="radio" name="payment_method" value={p.key} checked={i === 0} /> {p.label}</label>
))}
</fieldset>
</If>
<button type="submit">{'Order now' | t}</button>
</form>
  • formAttrs('order', { … }) gives the form its method and action; your extra attributes are added, but they can never replace those two.
  • formFields('order') adds the hidden fields every order form needs, including the anti-spam field. Always include it.
  • On success the shopper lands on the thank-you page. If the order is refused, the product page is shown again with errors, a list of messages.
Field
customer_name Required
phone Required: the customer’s identity in Sworen
city Required. If the merchant set delivery cities, it must be one of shop.cities
address, note Optional
variant_id Required when the product has variants
quantity Defaults to 1. A quantity with a bundle offer is priced as that bundle
upsells[] The ids of the add-ons the shopper ticked
coupon_code A discount code
payment_method A key from formData('order').payments. Leave it out for cash on delivery

Prices always come from the catalog, never from the form.

Show the payment choice only when there is more than one method: a cash-only store needs nothing, and the theme keeps working when the merchant adds card payments later.

The cart is a JSON API you call from JavaScript (the visitor’s session identifies their cart):

Action Request Body
Read GET {routes.cart_api_url} —
Add POST {routes.cart_items_url} { product_id, quantity, variant_id }
Change quantity POST {routes.cart_change_url} { line, quantity } or { product_id, variant_id, quantity }
Remove DELETE /api/v1/cart/items/{product_id} —
Apply / remove a coupon POST / DELETE /api/v1/cart/coupon { code }
Note POST /api/v1/cart/note { note }
Empty POST {routes.cart_clear_url} —

Without JavaScript, an add-to-cart form can post to routes.cart_add_url: it adds the line and shows the cart.

<form action={routes.cart_add_url} method="post">
<input type="hidden" name="product_id" value={product.id} />
<input type="hidden" name="variant_id" value={product.first_available_variant?.id} />
<input type="hidden" name="csrf_token" value={customer.csrf_token} />
<input type="number" name="quantity" value="1" min="1" />
<button type="submit">{'Add to cart' | t}</button>
</form>

csrf_token is only filled when a customer is signed in, and is then required.

Pass the URLs to your script from the layout:

<script>
window.Shop = { cartItems: '{routes.cart_items_url}', cartApi: '{routes.cart_api_url}' };
</script>

The cart page (cart template) checks out with an order form, like the product page.

Accounts are optional: shoppers can always order without one. An account shows a customer their own orders, matched by phone number.

Page Form
account-login POST {routes.account_login_url} with email and password
account-register POST {routes.account_register_url} with email, password (8 characters or more), and optional name and phone
account Shows orders; visitors who aren’t signed in are sent to the login page

These are plain <form method="post"> forms. Rejected attempts come back with errors.

Signing out needs the customer’s token as a hidden field:

<form method="post" action={routes.account_logout_url}>
<input type="hidden" name="csrf_token" value={customer.csrf_token} />
<button type="submit">{'Sign out' | t}</button>
</form>

In the header:

{customer.logged_in
? <a href={routes.account_url}>{'My account' | t}</a>
: <a href={routes.account_login_url}>{'Sign in' | t}</a>}