Skip to main content

Checkout Implementation

This guide walks you through implementing a complete checkout flow, from cart creation to order confirmation.

Overview​

The checkout flow in Unchained Engine follows these steps:

Step 1: User Authentication​

Before adding items to a cart, the user must be authenticated (as guest or registered).

Guest Checkout​

mutation LoginAsGuest {
loginAsGuest {
_id
tokenExpires
user {
_id
isGuest
}
}
}

The session token is set as an HTTP-only cookie automatically. For subsequent requests, ensure cookies are sent with your requests.

Registered User Login​

mutation Login {
loginWithPassword(email: "user@example.com", password: "password") {
_id
tokenExpires
user {
_id
username
}
}
}

Step 2: Add Products to Cart​

Add products to the cart. The cart is created automatically on first mutation.

Add Simple Product​

mutation AddToCart {
addCartProduct(productId: "product-123", quantity: 2) {
_id
quantity
product {
_id
texts {
title
}
}
unitPrice {
amount
currencyCode
}
total {
amount
currencyCode
}
order {
_id
total {
amount
currencyCode
}
}
}
}

Add Configurable Product​

For products with variations or configurations:

mutation AddConfiguredProduct {
addCartProduct(
productId: "configurable-product-123"
quantity: 1
configuration: [
{ key: "size", value: "L" }
{ key: "color", value: "blue" }
]
) {
_id
configuration {
key
value
}
}
}

Update Quantity​

mutation UpdateQuantity {
updateCartItem(itemId: "cart-item-123", quantity: 3) {
_id
quantity
}
}

Remove Item​

mutation RemoveItem {
removeCartItem(itemId: "cart-item-123") {
_id
}
}

Step 3: Set Delivery & Payment​

Get Available Providers​

Fetch both delivery and payment providers in a single query:

query GetProviders {
me {
cart {
_id
supportedDeliveryProviders {
_id
type
interface { _id label }
simulatedPrice { amount currencyCode }
}
supportedPaymentProviders {
_id
type
interface { _id label }
}
}
}
}

Set Both Providers​

Set delivery and payment providers in one mutation:

mutation SetProviders {
updateCart(
deliveryProviderId: "delivery-provider-123"
paymentProviderId: "payment-provider-123"
) {
_id
delivery {
_id
provider {
_id
type
interface {
label
}
}
fee {
amount
currencyCode
}
}
}
}

Set Delivery Address​

mutation SetDeliveryAddress {
updateCartDeliveryShipping(
deliveryProviderId: "delivery-provider-123"
address: {
firstName: "John"
lastName: "Doe"
company: "ACME Inc"
addressLine: "123 Main St"
addressLine2: "Apt 4"
postalCode: "12345"
city: "Zurich"
countryCode: "CH"
}
) {
_id
delivery {
... on OrderDeliveryShipping {
_id
address {
firstName
lastName
city
countryCode
}
}
}
}
}

Set Pickup Location (for PICKUP delivery)​

mutation SetPickupLocation {
updateCartDeliveryPickUp(
deliveryProviderId: "delivery-provider-123"
orderPickUpLocationId: "pickup-location-123"
) {
_id
delivery {
... on OrderDeliveryPickUp {
_id
activePickUpLocation {
_id
name
address {
addressLine
city
}
}
}
}
}
}

Initialize Payment (Sign)​

For client-side payment SDKs (Stripe, PayPal), get the client token before checkout:

mutation SignPayment {
signPaymentProviderForCheckout(
orderPaymentId: "order-payment-123"
)
}

The returned value is used to initialize the payment SDK on the client.

Step 4: Review Cart​

Get the complete cart with all pricing:

query ReviewCart {
me {
cart {
_id
items {
_id
quantity
product {
texts {
title
}
}
total {
amount
currencyCode
}
}
delivery {
... on OrderDeliveryShipping {
address {
firstName
lastName
addressLine
city
countryCode
}
}
fee {
amount
currencyCode
}
}
payment {
provider {
interface {
label
}
}
fee {
amount
currencyCode
}
}
discounts {
total {
amount
currencyCode
}
code
}
total {
amount
currencyCode
}
}
}
}

Apply Discount Code​

mutation ApplyDiscount {
addCartDiscount(code: "SAVE10") {
_id
code
total {
amount
currencyCode
}
order {
_id
total {
amount
currencyCode
}
}
}
}

Remove Discount Code​

mutation RemoveDiscount {
removeCartDiscount(discountId: "discount-123") {
_id
order {
_id
discounts {
_id
}
}
}
}

Step 5: Checkout​

Standard Checkout​

mutation Checkout {
checkoutCart {
_id
status
orderNumber
ordered
payment {
status
}
delivery {
status
}
total {
amount
currencyCode
}
}
}

Checkout with Payment Context​

For payment providers that need additional data:

mutation CheckoutWithPayment {
checkoutCart(
paymentContext: {
paymentIntentId: "pi_xxx" # From Stripe
}
) {
_id
status
orderNumber
}
}

Step 6: Post-Checkout​

Check Order Status​

query OrderStatus {
order(orderId: "order-123") {
_id
status
orderNumber
payment {
status
}
delivery {
status
}
}
}

Order Statuses Explained​

StatusMeaning
nullCart (not checked out)
PENDINGChecked out, awaiting payment
CONFIRMEDPayment confirmed, ready for delivery
FULFILLEDOrder delivered and complete
REJECTEDOrder cancelled

Complete Example: React Implementation​

import { useState } from 'react';
import { useMutation, useQuery } from '@apollo/client';

function Checkout() {
const [step, setStep] = useState('cart');

// Get cart data
const { data: cartData, refetch } = useQuery(GET_CART);
const cart = cartData?.me?.cart;

// Mutations
const [addToCart] = useMutation(ADD_TO_CART);
const [updateCart] = useMutation(UPDATE_CART);
const [setDeliveryAddress] = useMutation(SET_DELIVERY_ADDRESS);
const [signPayment] = useMutation(SIGN_PAYMENT);
const [checkout] = useMutation(CHECKOUT);

const handleAddProduct = async (productId: string, quantity: number) => {
await addToCart({ variables: { productId, quantity } });
refetch();
};

const handleSetProviders = async (
deliveryProviderId: string,
paymentProviderId: string,
address: Address
) => {
// Set both providers in one call
await updateCart({
variables: { deliveryProviderId, paymentProviderId },
});
await setDeliveryAddress({
variables: { deliveryProviderId, address },
});
refetch();
setStep('review');
};

const handleCheckout = async () => {
try {
// Get payment client token if needed
const { data: signData } = await signPayment({
variables: { orderPaymentId: cart.payment._id },
});

// Initialize payment SDK (e.g., Stripe)
// await stripe.confirmPayment(signData.signPaymentProviderForCheckout)

// Complete checkout
const { data: orderData } = await checkout();

if (orderData.checkoutCart.status === 'CONFIRMED') {
setStep('confirmation');
} else if (orderData.checkoutCart.status === 'PENDING') {
// Payment pending - show waiting message
setStep('pending');
}
} catch (error) {
console.error('Checkout failed:', error);
}
};

return (
<div>
{step === 'cart' && (
<CartStep cart={cart} onContinue={() => setStep('providers')} />
)}
{step === 'providers' && (
<ProvidersStep
deliveryProviders={cart?.supportedDeliveryProviders}
paymentProviders={cart?.supportedPaymentProviders}
onSelect={handleSetProviders}
/>
)}
{step === 'review' && (
<ReviewStep cart={cart} onCheckout={handleCheckout} />
)}
{step === 'confirmation' && (
<ConfirmationStep orderNumber={cart?.orderNumber} />
)}
</div>
);
}

Error Handling​

Common Checkout Errors​

ErrorCauseSolution
NoCartItemsEmpty cartEnsure items are added
NoDeliveryProviderDelivery not setSet delivery provider
NoPaymentProviderPayment not setSet payment provider
PaymentDeclinedPayment failedShow error, retry payment
ProductNotActiveProduct unavailableRemove item from cart

Handling Payment Errors​

mutation CheckoutWithErrorHandling {
checkoutCart {
_id
status
}
}

If the mutation throws, catch and display the error:

try {
await checkout();
} catch (error) {
if (error.graphQLErrors) {
const code = error.graphQLErrors[0]?.extensions?.code;
switch (code) {
case 'PAYMENT_DECLINED':
showError('Payment was declined. Please try another method.');
break;
case 'INSUFFICIENT_STOCK':
showError('Some items are no longer available.');
refetch(); // Refresh cart
break;
default:
showError('Checkout failed. Please try again.');
}
}
}

Webhooks for Async Payments​

Many payment providers confirm payments asynchronously via webhooks:

// Express webhook handler
app.post('/webhooks/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);

if (event.type === 'payment_intent.succeeded') {
const { orderId } = event.data.object.metadata;

// The order will automatically transition to CONFIRMED
// via the payment adapter's charge() method
}

res.json({ received: true });
});