Templates
Fjord owns the routing, the state and the offers. Your templates own the markup — the same division Commerce checkout already works on.
What a step template receives
| Variable | Is |
|---|---|
flow | the flow being served |
step | the step being rendered |
variant | the split-test arm, or null for the control |
session | the visitor's place in the funnel |
cart | the current cart, or the completed order once there is one |
order | the completed order, or null |
offer | the offer on an upsell or downsell step |
bumps | the eligible order bumps for a checkout step |
nextUrl | where "continue" goes from here |
craft.fjord
Available in any template, funnel or not:
{{ craft.fjord.session }} {# the visitor's place in the funnel #}
{{ craft.fjord.step }} {# the step they are on #}
{{ craft.fjord.next(step) }} {# where "continue" goes #}
{{ craft.fjord.continueUrl }} {# where a checkout's redirect should point #}
{{ craft.fjord.bumps(step, cart) }} {# the bumps for this checkout #}
{{ craft.fjord.isOnOrder(offer, cart) }} {# is this bump already ticked? #}
{{ craft.fjord.grants(order) }} {# what each offer resolved to #}
{{ craft.fjord.storeCheckoutUrl }} {# null when no flow has taken it over #}
{{ craft.fjord.flow('springLaunch') }}
{{ craft.fjord.offer('careKit') }}
{{ craft.fjord.isPro }}
The one line to get right
In a checkout template, point Commerce's redirect at craft.fjord.continueUrl:
{{ redirectInput(craft.fjord.continueUrl) }}
Not at a page of your choosing, and not at craft.fjord.next(step).
The next step is decided after the order completes. At render time there is no completed order,
so every post-purchase step looks unreachable — and a URL baked into the form at that moment would
skip the upsell entirely. continueUrl resolves when it is followed, which is the only moment the
answer is right.
Storing the card
A one-click upsell has nothing to charge unless the customer's payment method is reusable. That means the gateway must support payment sources, and the customer must have asked to save the card:
<label>
<input type="checkbox" name="savePaymentSource" value="1" checked>
Save my payment details for one-click offers
</label>
Without it, Fjord skips every post-purchase step rather than showing an offer it cannot fulfil.
Front-end actions
| Action | Does | Params |
|---|---|---|
fjord/checkout/toggle-bump | Ticks or unticks an order bump | offer, on |
fjord/checkout/accept-offer | Takes a post-purchase offer with one click | step |
fjord/checkout/decline-offer | Refuses it and moves on | step |
fjord/checkout/next | Moves on from a step | step |
fjord/checkout/store-checkout | Sends a shopper into the store-checkout flow | — |
fjord/flow/continue | Resolves where the visitor goes next, when they get there | — |
Each answers JSON to an Ajax caller and a redirect to a plain form post, so a funnel can be built either way without the controller caring which:
<form method="post">
{{ csrfInput() }}
{{ actionInput('fjord/checkout/accept-offer') }}
{{ redirectInput(step.getUri()) }}
{{ hiddenInput('step', step.id) }}
<button type="submit">{{ offer.getDisplayAcceptLabel() }}</button>
</form>
The JSON reply carries a redirect key with the same answer, for a template that would rather stay
on the page and move itself.
Marking a bump on the cart
A bump's line item is stamped, so a cart summary can label it:
{% for item in cart.lineItems %}
{{ item.description }}
{% if item.options.fjordOffer is defined %}<em>added offer</em>{% endif %}
{% endfor %}
Templates per step and per variant
A step's Template field takes either a bare name — looked up under the Template Root, so
checkout means shop/fjord/checkout — or a path with a slash, used as-is. Left empty, it falls
back to the step's type.
A split-test variant can name its own template, or leave it empty to render the step's. An empty-template variant is how a copy-only test works: same markup, different settings.