Skip to content

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 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.

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 / priceIdProductPurchase decides which.

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.

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.

<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 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.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 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.

  • 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() or mikaSafeReturnTo().
  • Copied request-bound pages export prerender = false or the app uses server output.
  • Checkout success validates checkoutId and token-bound state through Mika.checkout.status().

Next: Account And Downloads covers signed-in customer flows. Backend And Provider covers the trusted API that makes checkout real.

  • ../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