Cart, Wishlist, And Checkout
Use this guide for browser-facing buyer flows after product purchase forms post successfully.
Mika exposes form-first Astro Actions for cart, wishlist, coupon, and checkout start flows. These are the default browser mutation surface.
Do not expose cart, checkout, account, subscription, webhook, or admin mutation routes as public JSON endpoints unless the host has implemented the matching auth, CSRF, confirmation, idempotency, rate-limit, and provider policy.
Task Map
Section titled “Task Map”| Task | Start with | Verify |
|---|---|---|
| Add a product to cart | actions.mika.cart.add |
The cart page shows the new line or a visible validation error. |
| Edit a cart | cart.update, cart.remove, cart.applyCoupon, cart.removeCoupon |
Mika.cart.get() returns the updated totals. |
| Start checkout | actions.mika.checkout.start |
The posted page redirects to checkout.data.redirectUrl or shows a provider/config error. |
| Save to wishlist | wishlist.add, wishlist.moveToCart, wishlist.remove |
Mika.wishlist.get() reflects the change. |
Add To Cart
Section titled “Add To Cart”An add-to-cart control is an HTML form posting to the cart.add action. The ⓐ copyable AddToCartForm.astro wraps this in Kumo UI; the Mika contract is the action plus its fields:
---import { actions } from "astro:actions";import { mikaHiddenInput, mikaReturnToInput } from "@bnomei/emdash-mika/astro";const { sellableId, priceId, maxQuantity = 99, returnTo } = Astro.props;---<form action={actions.mika.cart.add} method="post"> <input type="hidden" {...mikaHiddenInput("sellableId", sellableId)} /> <input type="hidden" {...mikaHiddenInput("priceId", priceId)} /> <input type="hidden" {...mikaReturnToInput(returnTo)} /> <input name="quantity" type="number" min="1" max={maxQuantity} value="1" /> <button type="submit">Add to cart</button></form>mikaHiddenInput / mikaReturnToInput come from @bnomei/emdash-mika/astro, not a required copied src/lib/form.ts. mikaHiddenInput serializes name/value pairs; mikaReturnToInput runs mikaSafeReturnTo, so a posted returnTo can never become an open redirect. On the grouped-variant path the form posts a single serialized purchase field instead of discrete sellableId / priceId — ProductPurchase decides which.
Action Result UX
Section titled “Action Result UX”Astro Actions return structured results. Copied pages should branch on the action result before rendering stale state:
---const added = Astro.getActionResult(actions.mika.cart.add);const failed = added?.error;const message = failed ? failed.message : added?.data ? "Added to cart." : undefined;---Use result.ok envelopes from createMika(Astro) for server reads and Astro Action results for posted forms. Do not silently ignore failed action results; show the host’s validation or provider message where the buyer can act on it.
Cart Page
Section titled “Cart Page”Read the cart with the request-bound helper, and redirect first when a checkout start returned a provider URL:
---import { createMika } from "@bnomei/emdash-mika/astro";import { actions } from "astro:actions";export const prerender = false;
const Mika = createMika(Astro);const checkout = Astro.getActionResult(actions.mika.checkout.start);if (checkout?.data?.redirectUrl) return Astro.redirect(checkout.data.redirectUrl);
const cartResult = await Mika.cart.get();const cart = cartResult.ok ? cartResult.data : null;---Copied cart, wishlist, checkout success, and checkout cancel pages export prerender = false because they read request state, action results, sessions, and provider-backed checkout status.
Each line is edited by its own form: cart.update (fields lineId, quantity), cart.remove (lineId), and wishlist.saveForLater (lineId) to move a line to the wishlist. Render amounts with formatMikaMoney(...) from @bnomei/emdash-mika/astro — money is in minor units.
Coupons
Section titled “Coupons”<form action={actions.mika.cart.applyCoupon} method="post"> <input type="hidden" {...mikaHiddenInput("cartId", cart.id)} /> <input name="code" type="text" /> <button type="submit">Apply</button></form>cart.removeCoupon (field cartId) clears it. Tax and shipping are display-only fields on the cart DTO — Mika is not a tax or shipping engine.
Checkout
Section titled “Checkout”Checkout starts from a form (a cart checkout posts cartId; a buy-now posts sellableId / priceId / quantity), always with sanitized redirect fields built by mikaRedirectInputs:
---import { mikaHiddenInput, mikaRedirectInputs } from "@bnomei/emdash-mika/astro";const { successPath = "/checkout/success", cancelPath = "/checkout/cancel", returnTo = "/cart",} = Astro.props;const redirect = mikaRedirectInputs( { successPath, cancelPath, returnTo }, { successFallback: "/checkout/success", cancelFallback: "/checkout/cancel", returnToFallback: "/cart", },);---<form action={actions.mika.checkout.start} method="post"> <input type="hidden" {...mikaHiddenInput("cartId", cart.id)} /> <input type="hidden" {...redirect.successPath} /> <input type="hidden" {...redirect.cancelPath} /> <input type="hidden" {...redirect.returnTo} /> <input name="email" type="email" /> <button type="submit">Checkout</button></form>mikaRedirectInputs() sanitizes each posted path and uses caller-provided fallbacks. The ⓐ copyable checkout components pass mikaTemplateRoutes.checkoutSuccess and mikaTemplateRoutes.checkoutCancel; host apps can pass their own route constants.
The page redirects to checkout.data.redirectUrl (see the cart frontmatter above). On return, /checkout/success expects checkoutId and optional token query params, then reads Mika.checkout.status({ checkoutId, token }) and maps the status (pending, completed, cancelled, expired, failed, binding_mismatch) to a message.
Wishlist
Section titled “Wishlist”wishlist.add (fields sellableId, priceId, returnTo) adds an item. The wishlist page reads Mika.wishlist.get(), and each row posts wishlist.moveToCart or wishlist.remove (field itemId).
Checkout Return Rule
Section titled “Checkout Return Rule”Checkout success and cancel pages are browser return surfaces. Success must confirm payment/order state through provider-backed APIs and Mika status tokens. Cancel may call Mika.checkout.cancel({ checkoutId, token }) to abandon a checkout and preserve cart UX, but it is not payment proof. Expired stock reservation cleanup still belongs to scheduled maintenance, not a cancel page visit.
Verify
Section titled “Verify”- Add-to-cart, cart update, coupon, checkout start, and wishlist forms all render
Astro.getActionResult()failures where the buyer can act. - Posted redirect fields are created with
mikaRedirectInputs()ormikaSafeReturnTo(). - Copied request-bound pages export
prerender = falseor the app uses server output. - Checkout success validates
checkoutIdand token-bound state throughMika.checkout.status().
Next: Account And Downloads covers signed-in customer flows. Backend And Provider covers the trusted API that makes checkout real.
Source Anchors
Section titled “Source Anchors”- ⓟ
../emdash-mika/src/astro-actions.ts - ⓟ
../emdash-mika/src/api/redirect-policy.ts - ⓐ
../emdash-mika/src/templates/astro/components/AddToCartForm.astro - ⓐ
../emdash-mika/src/templates/astro/components/CartLines.astro - ⓐ
../emdash-mika/src/templates/astro/components/CouponForm.astro - ⓐ
../emdash-mika/src/templates/astro/components/CheckoutForm.astro - ⓟ
../emdash-mika/src/astro.ts - ⓐ
../emdash-mika/src/templates/astro/pages/cart.astro - ⓐ
../emdash-mika/src/templates/astro/pages/wishlist.astro - ⓐ
../emdash-mika/src/templates/astro/pages/checkout/success.astro - ⓐ
../emdash-mika/src/templates/astro/pages/checkout/cancel.astro