10. Payment

Select a payment provider and place the order.

On this page

The fourth and last step in the express checkout flow is the payment method selection step. In this step, the customer will choose a payment method and complete their cart, placing an order.

To create the component of this step, create the file components/Payment/index.tsx with the following content:

Directory structure after creating Payment component file

tsx

tsxCopy
"use client"
import {
useState,
} from "react"
import { useCart } from "@/providers/cart"
import { HttpTypes } from "@aiminaabeejs/types"
import { useRouter } from "next/navigation"
type PaymentProps = {
handle: string
isActive: boolean
}
export const Payment = ({
handle,
isActive,
}: PaymentProps) => {
const { cart, updateItemQuantity, unsetCart } = useCart()
const [loading, setLoading] = useState(true)
const [paymentProviders, setPaymentProviders] = useState<
HttpTypes.StorePaymentProvider[]
>([])
const [selectedPaymentProvider, setSelectedPaymentProvider] = useState("")
const router = useRouter()
// TODO retrieve payment providers
}

You create a Payment component that receives the product's handle and whether the component is active as props. The component defines the following variables:

  • cart, unsetCart, and updateItemQuantity: The customer's cart, the function to unset the cart, and the function to update the quantity of an item in the cart, retrieved from the cart context.
  • loading: A boolean state variable indicating whether an operation is loading.
  • paymentProviders: A state variable that holds the available payment providers in the Aiminaabee application.
  • selectedPaymentProvider: A state variable that holds the ID of the selected payment provider.
  • router: The router instance to navigate between steps.

Retrieve Payment Providers

In Aiminaabee, each region has a set of enabled payment providers. So, during checkout, you retrieve the payment providers specific to a cart's region and allow the customer to choose from them.

To do that, first, add the following imports at the top of the file:

tsx

tsxCopy
import {
// other imports...
useEffect,
} from "react"
import { sdk } from "@/lib/sdk"

Then, replace the TODO in the Payment component with the following:

]

tsx

tsxCopy
useEffect(() => {
if (!loading || !cart) {
return
}
sdk.store.payment.listPaymentProviders("us", {
region_id: cart.region_id || "",
})
.then(({ payment_providers }) => {
setPaymentProviders(payment_providers)
setLoading(false)
})
}, [loading, cart])
// TODO handle provider selection

In the useEffect hook, you use the JS SDK to send a request to the List Payment Providers API route, passing the cart's region ID as a query parameter.

The API route returns a list of payment providers, which you set in the paymentProviders state variable. These are the payment providers you'll allow the customer to select from.

Handle Payment Provider Selection

After the customer selects the payment provider, you want to save that selection in the cart, but you also may need to show UI for additional actions. For example, if the customer chooses the Stripe payment provider, you want to show them a credit-card form to input their details.

So, you'll implement the logic to first save the payment provider selection in the Aiminaabee application, then optionally show additional UI based on the chosen provider.

First, add the following imports at the top of the file:

tsx

tsxCopy
import {
// other imports...
useMemo,
} from "react"

Then, replace the TODO in the Payment component with the following:

tsx

tsxCopy
const handleSelectProvider = async () => {
if (!selectedPaymentProvider || !cart) {
return
}
setLoading(true)
sdk.store.payment.initiatePaymentSession("us", cart, {
provider_id: selectedPaymentProvider,
})
.then(() => {
setLoading(false)
})
}
useEffect(() => {
if (!selectedPaymentProvider || !cart) {
return
}
handleSelectProvider()
}, [selectedPaymentProvider])
const paymentUi = useMemo(() => {
if (!selectedPaymentProvider) {
return
}
switch (selectedPaymentProvider) {
// TODO handle other providers
default:
return <></>
}
}, [selectedPaymentProvider])
const canPlaceOrder = useMemo(() => {
switch (selectedPaymentProvider) {
case "":
return false
// TODO handle other providers
default:
return true
}
}, [selectedPaymentProvider])
// TODO handle place order

You define a handleSelectProvider function that uses the JS SDK to set the selected payment provider in the cart. You call this function whenever the selectedPaymentProvider changes.

Then, you define two memoized variables:

  • paymentUi: This variable holds additional UI to show based on the selected payment provider. This guide doesn't cover any specific providers, so you're free to implement this based on the payment providers you support. By default, it returns an empty node.
  • canPlaceOrder: This variable holds a boolean indicating whether the customer can place an order. You can define the logic based on the payment providers you support. By default, it returns true.

Tip: Learn how to implement payment in your storefront with Stripe in this guide.

Handle Place Order Function

After the customer chooses a payment provider and completes any additional steps, they can complete their cart and place their order.

Replace the TODO in the Payment component with the following:

tsx

tsxCopy
const placeOrder = () => {
if (!cart || !canPlaceOrder) {
return
}
setLoading(true)
sdk.store.cart.complete("us", cart.id)
.then((data) => {
if (data.type === "cart") {
alert(data.error.message)
setLoading(false)
} else {
unsetCart()
// redirect to confirmation page
router.push(`/confirmation/${data.order.id}`)
}
})
}
// TODO render payment step

You define a placeOrder function that uses the JS SDK to complete the cart by sending a request to the Complete Cart API route. If the cart is successfully completed, you unset the cart and redirect the customer to the confirmation page, which you'll implement later.

Render Payment Step

Finally, you'll add the return statement showing the Payment step that allows the customer to choose a payment method.

First, add the following imports at the top of the file:

tsx

tsxCopy
import { Card } from "../Card"
import { Button, Input, RadioGroup } from "@aiminaabeejs/admin-ui"

Then, replace the last TODO in the Payment component with the following:

tsx

tsxCopy
const getProviderTitle = (providerId: string) => {
switch(true) {
case providerId.startsWith("pp_system_default"):
return "Cash on Delivery"
default:
return providerId
}
}
return (
<Card
title="Payment"
isActive={isActive}
isDone={false}
path={`/${handle}?step=payment`}
>
<span className="text-sm">Your order</span>
{cart?.items?.map((item) => (
<div className="flex gap-2" key={item.id}>
<img src={item.thumbnail} alt={item.title} className="w-24 h-24 rounded" />
<div className="flex flex-col gap-3">
<span className="text-base">{item.product_title}</span>
{item.variant?.options?.map((option) => (
<span className="flex gap-1 text-sm" key={option.id}>
<span className="text-ui-fg-muted">
{item.product?.options?.find(
(productOption) => productOption.id === option.option_id
)?.title}
</span>
<span className="text-ui-fg-base">{option.value}</span>
</span>
))}
<span className="flex gap-1 text-sm items-center">
<span className="text-ui-fg-muted">Quantity</span>
<Input
type="number"
value={item.quantity}
onChange={(e) => {
if (!e.target.value) {
return
}
updateItemQuantity(item.id, parseInt(e.target.value))
}}
/>
</span>
</div>
</div>
))}
<hr className="bg-ui-bg-subtle" />
<div className="flex justify-between">
<span className="text-sm text-ui-fg-muted">Subtotal:</span>
<span className="text-sm text-ui-fg-base">{formatPrice(
cart?.item_subtotal || 0,
cart?.currency_code
)}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-ui-fg-muted">Shipping & handling:</span>
<span className="text-sm text-ui-fg-base">{formatPrice(
cart?.shipping_total || 0,
cart?.currency_code
)}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-ui-fg-muted">Total:</span>
<span className="text-sm text-ui-fg-base">{formatPrice(
cart?.total || 0,
cart?.currency_code
)}</span>
</div>
<hr className="bg-ui-bg-subtle" />
<span className="text-sm">Delivery address</span>
<p className="text-xs text-ui-fg-muted">
{cart?.shipping_address?.first_name} {cart?.shipping_address?.last_name}<br />
{cart?.shipping_address?.address_1}<br />
{cart?.shipping_address?.city}, {cart?.shipping_address?.postal_code}, {cart?.shipping_address?.country_code}<br />
</p>
<hr className="bg-ui-bg-subtle" />
<span className="text-sm">Payment method</span>
<div className="flex flex-col gap-2">
<RadioGroup
value={selectedPaymentProvider}
onValueChange={(value) => setSelectedPaymentProvider(value)}
>
{paymentProviders.map((paymentProvider) => (
<div className="flex gap-1" key={paymentProvider.id}>
<RadioGroup.Item value={paymentProvider.id} />
<div className="flex justify-between w-full gap-2">
<span className="text-sm">{getProviderTitle(paymentProvider.id)}</span>
</div>
</div>
))}
</RadioGroup>
</div>
{paymentUi}
<hr className="bg-ui-bg-subtle" />
<Button
className="w-full"
disabled={!canPlaceOrder || loading}
onClick={placeOrder}
>Pay {formatPrice(
cart?.total || 0,
cart?.currency_code
)}</Button>
</Card>
)

You first define a getProviderTitle function that receives a payment provider ID and returns a title based on the provider. You'll use this function to show a human-readable title for each payment provider.

Then, in the return statement, you wrap the payment method selection form with the Card component you created earlier. In the card, you show the cart's details, including the items, totals, and delivery address.

You also show the list of payment providers, allowing the customer to select one. Once the customer chooses one, you show any additional UI to complete their payment.

After the customer selects an option and clicks the "Pay" button, the placeOrder function is called to complete the cart and place the order.

Add to Router Component

Finally, you'll add the Payment component to the Router component to show the payment method selection step.

First, import the Payment component at the top of the components/Router/index.tsx file:

tsx

tsxCopy
import { Payment } from "../Payment"

Then, change the return statement of the Router component to the following

tsx

tsxCopy
return (
<>
<Product handle={handle} isActive={activeTab === "product"} />
<Address handle={handle} isActive={activeTab === "address"} />
<Shipping handle={handle} isActive={activeTab === "shipping"} />
<Payment handle={handle} isActive={activeTab === "payment"} />
</>
)

This will show the payment method selection step and expand its details if activeTab is payment.

Test it Out

While both the Aiminaabee application and the Aiminaabee Next.js storefront template are running, if you refresh the page you had opened or go to http://localhost:3000/sweatpants?step=payment, you should see the Payment step where you can choose a payment method.

Payment step showing the payment providers and a pay button

Select a payment provider then click on the "Pay" button. The payment method will be saved in the cart and you'll be redirected to the confirmation page, which currently doesn't exist. You'll add this page next.


Next: 11. Confirmation