Order form, cart and accounts
Build the cash-on-delivery order form, an add-to-cart flow and optional customer accounts.
The order form
Section titled “The order form”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 itsmethodandaction; 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.
Fields
Section titled “Fields”| 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
Section titled “The cart”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.
Customer accounts
Section titled “Customer accounts”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>}