CheckoutContext
Description
Manages checkout process state and operations. Handles order placement, shipping/payment methods, checkout data, and form state. Integrates with cart context for checkout flow.
Import
import { CheckoutProvider, useCheckout, useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext';
Usage
Setup Provider
import { CheckoutProvider } from '@components/frontStore/checkout/CheckoutContext';
import { FormProvider, useForm } from 'react-hook-form';
function CheckoutPage() {
const methods = useForm();
const [formEnabled, setFormEnabled] = useState(true);
return (
<FormProvider {...methods}>
<CheckoutProvider
placeOrderApi="/api/placeOrder"
checkoutSuccessUrl="/checkout/success"
allowGuestCheckout={true}
form={methods}
enableForm={() => setFormEnabled(true)}
disableForm={() => setFormEnabled(false)}
>
{/* Checkout components */}
</CheckoutProvider>
</FormProvider>
);
}
Access Checkout State
import { useCheckout } from '@components/frontStore/checkout/CheckoutContext';
function CheckoutSummary() {
const { loading, requiresShipment, orderPlaced, orderId } = useCheckout();
if (orderPlaced) {
return <div>Order {orderId} placed successfully!</div>;
}
return (
<div>
{loading && <div>Processing...</div>}
{requiresShipment && <div>Shipping required</div>}
</div>
);
}
Use Checkout Actions
import { useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext';
function CheckoutButton() {
const { checkout } = useCheckoutDispatch();
const handleCheckout = async () => {
try {
const order = await checkout();
window.location.href = `/checkout/success/${order.uuid}`;
} catch (error) {
alert(error.message);
}
};
return (
<button onClick={handleCheckout}>
Place Order
</button>
);
}
API
CheckoutProvider Props
| Name | Type | Required | Description |
|---|---|---|---|
| placeOrderApi | string | Yes | API endpoint for placing order |
| checkoutSuccessUrl | string | Yes | Success page URL |
| allowGuestCheckout | boolean | No | Allow guest checkout (default: false) |
| form | UseFormReturn | Yes | React Hook Form instance |
| enableForm | () => void | Yes | Enable form callback |
| disableForm | () => void | Yes | Disable form callback |
| children | ReactNode | Yes | Child components |
useCheckout Hook
Returns checkout state:
| Property | Type | Description |
|---|---|---|
| orderPlaced | boolean | True when order placed successfully |
| orderId | string | Placed order ID |
| loading | boolean | True when placing order |
| cartId | string | Current cart UUID |
| checkoutSuccessUrl | string | Success page URL |
| requiresShipment | boolean | Always true in 2.2.1 — virtual/downloadable products are not supported yet, so every cart is treated as shippable. Do not use it to hide shipping UI; the real per-cart flag is noShippingRequired, surfaced by CartTotalSummary. |
| allowGuestCheckout | boolean | Guest checkout allowed |
| checkoutData | CheckoutData | Current checkout data |
| form | UseFormReturn | Form instance |
| registeredPaymentComponents | Record<string, Component> | Payment method renderers |
useCheckoutDispatch Hook
Returns checkout actions:
| Method | Signature | Description |
|---|---|---|
| placeOrder | () => Promise<any> | Place order (expects data in cart) |
| checkout | () => Promise<any> | Checkout with all data submission |
| getPaymentMethods | () => PaymentMethod[] | Get available payment methods |
| getShippingMethods | (params?) => Promise<ShippingMethod[]> | Get shipping methods |
| setCheckoutData | (data: CheckoutData) => void | Replace checkout data |
| updateCheckoutData | (data: Partial<CheckoutData>) => void | Update checkout data |
| clearCheckoutData | () => void | Clear all checkout data |
| registerPaymentComponent | (code, component) => void | Register payment method renderer |
| enableForm | () => void | Enable checkout form |
| disableForm | () => void | Disable checkout form |
Type Definitions
PaymentMethod
interface PaymentMethod {
code: string;
name: string;
[key: string]: any; // Extended fields
}
ShippingMethod
interface ShippingMethod {
providerCode: string; // required
code: string;
name: string;
cost?: {
value: number;
text: string;
};
[key: string]: any; // Extended fields
}
providerCodeproviderCode identifies the provider a method came from (core, USPS, EasyPost, …). It is what addShippingMethod(code, name, providerCode) needs in order to route validation to the right provider — CartContext.addShippingMethod throws without it.
If you map getShippingMethods() results into your own state, carry providerCode through. Dropping it (or defaulting it to 'core') means the selection either throws on the client or 422s server-side as "method no longer available".
ShippingAddressParams
interface ShippingAddressParams {
country: string;
province?: string;
postcode?: string;
}
PaymentMethodComponent
interface PaymentMethodComponent {
nameRenderer: React.ComponentType<{ isSelected: boolean }>;
formRenderer: React.ComponentType<{ isSelected: boolean }>;
checkoutButtonRenderer: React.ComponentType<{ isSelected: boolean }>;
}
Examples
Basic Checkout Flow
import { CheckoutProvider, useCheckout, useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext';
import { FormProvider, useForm } from 'react-hook-form';
function CheckoutPage() {
const methods = useForm();
const [formEnabled, setFormEnabled] = useState(true);
return (
<FormProvider {...methods}>
<CheckoutProvider
placeOrderApi="/api/placeOrder"
checkoutSuccessUrl="/checkout/success"
form={methods}
enableForm={() => setFormEnabled(true)}
disableForm={() => setFormEnabled(false)}
>
<CheckoutForm />
</CheckoutProvider>
</FormProvider>
);
}
function CheckoutForm() {
const { loading, orderPlaced } = useCheckout();
const { checkout } = useCheckoutDispatch();
const handleSubmit = async () => {
try {
await checkout();
} catch (error) {
console.error(error);
}
};
if (orderPlaced) {
return <div>Order placed!</div>;
}
return (
<button onClick={handleSubmit} disabled={loading}>
{loading ? 'Processing...' : 'Place Order'}
</button>
);
}
Payment Methods
import { useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext';
import { useEffect, useState } from 'react';
function PaymentMethods() {
const { getPaymentMethods } = useCheckoutDispatch();
const [methods, setMethods] = useState([]);
useEffect(() => {
const available = getPaymentMethods();
setMethods(available);
}, []);
return (
<div className="payment-methods">
<h3>Payment Method</h3>
{methods.map(method => (
<label key={method.code}>
<input type="radio" name="payment" value={method.code} />
{method.name}
</label>
))}
</div>
);
}
Shipping Methods
Selecting a method is a two-step flow: getShippingMethods() lists them, then CartContext's addShippingMethod(code, name, providerCode) commits the selection. The providerCode must be threaded from the listed method into the call.
import { useCartDispatch } from '@components/frontStore/cart/CartContext';
import { useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext';
import { useState, useEffect } from 'react';
function ShippingMethods({ address }) {
const { getShippingMethods } = useCheckoutDispatch();
const { addShippingMethod } = useCartDispatch();
const [methods, setMethods] = useState([]);
const [selected, setSelected] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (address) {
setLoading(true);
getShippingMethods({
country: address.country,
province: address.province,
postcode: address.postcode
})
.then(setMethods)
.finally(() => setLoading(false));
}
}, [address]);
const handleSelect = async (method) => {
setSelected(method.code);
// All three arguments are required — `providerCode` comes straight
// off the listed method.
await addShippingMethod(method.code, method.name, method.providerCode);
};
return (
<div className="shipping-methods">
<h3>Shipping Method</h3>
{loading && <div>Loading...</div>}
{methods.map(method => (
<label key={`${method.providerCode}:${method.code}`}>
<input
type="radio"
name="shipping"
value={method.code}
checked={selected === method.code}
onChange={() => handleSelect(method)}
/>
{method.name} - {method.cost?.text || 'Free'}
</label>
))}
</div>
);
}
Two providers can expose the same code, so key the list on providerCode + code rather than code alone.
Checkout Data Management
import { useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext';
function CheckoutForm() {
const { updateCheckoutData, checkout } = useCheckoutDispatch();
const handleAddressChange = (address) => {
updateCheckoutData({
shipping_address: address
});
};
const handlePaymentChange = (paymentCode) => {
updateCheckoutData({
payment_method: paymentCode
});
};
const handleSubmit = async () => {
try {
await checkout();
} catch (error) {
alert(error.message);
}
};
return (
<form>
{/* Address fields */}
{/* Payment selection */}
<button type="button" onClick={handleSubmit}>
Place Order
</button>
</form>
);
}
Register Payment Component
import { useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext';
import { useEffect } from 'react';
function PaymentMethodName({ isSelected }) {
return <span>Credit Card</span>;
}
function PaymentMethodForm({ isSelected }) {
if (!isSelected) return null;
return (
<div>
<input placeholder="Card Number" />
<input placeholder="Expiry" />
<input placeholder="CVV" />
</div>
);
}
function CheckoutButton({ isSelected }) {
if (!isSelected) return null;
return <button>Pay with Card</button>;
}
function CreditCardPayment() {
const { registerPaymentComponent } = useCheckoutDispatch();
useEffect(() => {
registerPaymentComponent('credit_card', {
nameRenderer: PaymentMethodName,
formRenderer: PaymentMethodForm,
checkoutButtonRenderer: CheckoutButton
});
}, []);
return null;
}
Complete Checkout Page
import { CheckoutProvider, useCheckout, useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext';
import { FormProvider, useForm } from 'react-hook-form';
import { useState } from 'react';
function CheckoutPage() {
const methods = useForm();
const [formEnabled, setFormEnabled] = useState(true);
return (
<FormProvider {...methods}>
<CheckoutProvider
placeOrderApi="/api/placeOrder"
checkoutSuccessUrl="/checkout/success"
allowGuestCheckout={true}
form={methods}
enableForm={() => setFormEnabled(true)}
disableForm={() => setFormEnabled(false)}
>
<CheckoutContent formEnabled={formEnabled} />
</CheckoutProvider>
</FormProvider>
);
}
function CheckoutContent({ formEnabled }) {
const { loading, orderPlaced, orderId, requiresShipment } = useCheckout();
const {
getPaymentMethods,
getShippingMethods,
updateCheckoutData,
checkout
} = useCheckoutDispatch();
const handleSubmit = async () => {
try {
const order = await checkout();
window.location.href = `/checkout/success/${order.uuid}`;
} catch (error) {
alert(error.message);
}
};
if (orderPlaced) {
return (
<div className="success">
<h1>Order Placed!</h1>
<p>Order ID: {orderId}</p>
</div>
);
}
return (
<div className="checkout-page">
<h1>Checkout</h1>
{/* Shipping Address */}
{requiresShipment && (
<section>
<h2>Shipping Address</h2>
{/* Address form fields */}
</section>
)}
{/* Shipping Method */}
{requiresShipment && (
<section>
<h2>Shipping Method</h2>
{/* Shipping method selection */}
</section>
)}
{/* Payment Method */}
<section>
<h2>Payment Method</h2>
{/* Payment method selection */}
</section>
{/* Submit */}
<button
onClick={handleSubmit}
disabled={loading || !formEnabled}
className="checkout-btn"
>
{loading ? 'Processing...' : 'Place Order'}
</button>
</div>
);
}
Methods Details
placeOrder()
Places order with cart data already saved. Use when checkout data is already in cart context.
checkout()
Validates form, submits all checkout data, and places order. Use for complete checkout flow.
getShippingMethods(params?)
- Without params: Returns initial methods from cart
- With params: Fetches methods based on address
Features
- Order Placement: Two methods (placeOrder, checkout)
- Form Integration: React Hook Form support
- Payment Methods: Registry system for custom renderers
- Shipping Methods: Dynamic fetching based on address
- Checkout Data: Centralized data management
- Loading States: Tracks order placement progress
- Error Handling: Retry mechanism with exponential backoff
- Guest Checkout: Optional guest checkout support
- Form Control: Enable/disable form during processing
- Type Safe: Full TypeScript support
Requirements
- CartContext: Must be wrapped in cart provider
- FormProvider: Requires React Hook Form
- Cart ID: Cart must be initialized
Related Components
- CartContext - Shopping cart context