I'm finding a guest checkout flow in Commerce 3.4.14 insanely confusing, docs aren't much help. I'm using Craft Solo and Commerce Lite.
TL;DR
I realize the code below is probably too complex, so even an example of a guest checkout process in Commerce would be extremely helpful (how do docs not provide this?!). Requirements:
- 3 page checkout process Contact > Shipping > Payment
- Contact page has email, billing address, shipping address
- Shipping page has shipping method (looked up by region) and notes
- Payment page has Stripe payment, ideally paypal as well (but not absolutely required)
- No javascript. All php/forms submitted to commerce
Overview (if you're still here)
I don't want to register customers or manage accounts. I want a guest checkout where they can enter shipping address > billing address > pay (Stripe or Paypal). I'm trying to set this up without javascript/JSON responses as suggested here. I want to use forms commerce/cart/update-cart
and commerce/payments/pay
as much as possible to update the cart addresses and pay.
I keep losing the shipping address because my commerce/cart/update-cart
creates a whole new shipping address or the billing address overrides it (research: 1, 2 and 3). I've tried adding <input type="hidden" name="shippingAddress[id]" value="{{cart.shippingAddress.id}}"/>
suggested in those links, but I'm still losing my shipping address somewhere along the line no matter how I try it.
I know billingAddress
overrides shippingAddress
updates when they have the same id but I can't figure out how to control that without registering them as a customer. I couldn't get the code from this issue to work with my template flow.
With the current code I get a new address when I submit store/checkout/shipping
that only has the shipping method data.
Current code
store/cart
{% set customer = craft.commerce.customers ? craft.commerce.customers.customer : null %}
{% set addresses = customer ? customer.addresses : null %}
<form method="POST" data-cart>
{{ csrfInput() }}
{{ actionInput('commerce/cart/update-cart') }}
{{ hiddenInput('successMessage', 'Cart has been updated.'|hash) }}
{% include '_partials/_cart-items' with {items: cart.lineItems} %}
<div class="actions">
<a href="/store" class="button actions__action">← Back to Store</a>
<button class="button actions__action"
type="submit"
name="redirect"
value="{{ '/store/cart' | hash }}">Update Cart</button>
<button class="button actions__action"
type="submit"
name="redirect"
value="{{ '/store/checkout/contact' | hash }}">Checkout →</button>
</div>
</form>
store/checkout/contact
{% set cart = craft.commerce.carts.cart %}
{% set address = cart.shippingAddress %}
{% set hasCountry = address and address.countryId %}
<form method="post">
{{ csrfInput() }}
{{ actionInput('commerce/cart/update-cart') }}
{{ redirectInput(hasCountry ? '/store/checkout/shipping' : '/store/checkout/contact') }}
<fieldset>
<legend>Shipping Address</legend>
{% if not hasCountry %}
<p>Please select the country that you want me to ship this order to.</p>
{% endif %}
{% include "_partials/_forms/_cartShippingFields" %}
</fieldset>
{% if hasCountry %}
{% if address.id %}
{# use shipping address id created when country was submitted #}
{{ hiddenInput('shippingAddress[id]', address.id) }}
{% endif %}
<fieldset>
<legend>Contact Info</legend>
<label class="field">
<span class="field__label">email:</span>
<div class="field__inputs">
<input
class="text-input"
name="email"
required
type="email"
value="{{ cart.email }}"
/>
</div>
<div class="field__errors">{{ cart.getErrors('email') | join }}</div>
</label>
</fieldset>
{% endif %}
<div class="actions">
<a href="/store/cart" class="button actions__action">← Back to Cart</a>
<button type="submit" class="button actions__action">
{% if hasCountry %}Shipping Method →{% else %}Set Country{% endif %}
</button>
</div>
</form>
store/checkout/shipping
{% set cart = craft.commerce.carts.cart %}
{% set address = cart.shippingAddress %}
{% set hasCountry = address and address.countryId %}
<form method="post">
<input type="hidden" name="action" value="commerce/cart/update-cart" />
{{ csrfInput() }}
{{ redirectInput('/store/checkout/payment') }}
{% if address.id %}
{{ hiddenInput('shippingAddress[id]', address.id) }}
{% endif %}
<fieldset>
<legend>Shipping Method</legend>
{% for handle, method in cart.availableShippingMethodOptions %}
{% set isCurrentSelection = handle == cart.shippingMethodHandle %}
{% set formattedPrice = method.priceForOrder(cart)|currency(cart.currency) %}
<label>
<input type="radio"
name="shippingMethodHandle"
value="{{ handle }}"
{{ isCurrentSelection ? ' checked' : '' }}
/>
<strong>{{ method.name }}</strong> {{ formattedPrice }}
</label>
{% endfor %}
<label>
<input name="billingAddressSameAsShipping" type="checkbox" checked="" value="1" />
My billing address is the same as my shipping address.
</label>
<legend>Order Notes</legend>
<label class="field">
<span class="field__label">Order Notes:</span>
<div class="field__inputs">
<textarea name="shippingAddress[notes]" cols="30" rows="4">{{ address ? address.notes : '' }}</textarea>
</div>
</label>
</fieldset>
<div class="actions">
<a href="/store/checkout/contact" class="button actions__action">← Back to Contact</a>
<button type="submit" class="button actions__action">Payment →</button>
</div>
</form>
store/checkout/payment
{% set cart = craft.commerce.carts.cart %}
{% set address = cart.shippingAddress %}
{# update the payment method and add billing address if needed #}
<form method="post">
{{ csrfInput() }}
{{ actionInput('commerce/cart/update-cart') }}
{{ hiddenInput('successMessage', 'Payment method has been updated.'|hash) }}
{{ redirectInput('store/checkout/payment') }}
{% if address.id %}
{{ hiddenInput('shippingAddress[id]', address.id) }}
{% endif %}
{% if cart.billingAddress.id %}
{{ hiddenInput('billingAddress[id]', address.id) }}
{% endif %}
<label for="gatewayId">Payment Method</label>
<select name="gatewayId" id="gatewayId">
{% for id,name in enabledGateways %}
<option value="{{ id }}"{% if id == cart.gatewayId %} selected{% endif %}>
{{- name -}}
</option>
{% endfor %}
</select>
{# check if they checked billing same as shipping #}
{% if cart.billingAddress is null %}
{% include "_partials/_forms/_cartBillingFields" %}
{% endif %}
<div class="actions">
<button type="submit" class="button actions__action">
Update payment details
</button>
</div>
</form>
{# submit the payment #}
{% if cart.gatewayId|length and address|length and cart.billingAddress|length %}
{% set gateway = craft.commerce.gateways.getGatewayById(cart.gatewayId) %}
<form method="post">
{{ csrfInput() }}
{{ actionInput('commerce/payments/pay') }}
{{ redirectInput('/commerce/customer/order?number={number}') }}
{{ hiddenInput('cancelUrl', '/commerce/checkout/payment'|hash) }}
{% if address.id %}
{{ hiddenInput('shippingAddress[id]', address.id) }}
{% endif %}
{% if cart.billingAddress.id %}
{{ hiddenInput('billingAddress[id]', address.id) }}
{% endif %}
{{ gateway.getPaymentFormHtml({})|raw }}
<div class="actions">
<a href="/store/checkout/shipping" class="button actions__action">← Back to Shipping</a>
<button type="submit" class="button actions__action">
Pay {{ cart.totalPrice|commerceCurrency(cart.paymentCurrency,convert=true) }} →
</button>
</div>
</form>
{% endif %}