DEV Community

Abdur Rakib Rony
Abdur Rakib Rony

Posted on

Implementing Afterpay in a Next.js E-commerce Application: A Complete Guide

As e-commerce continues to evolve, offering flexible payment options has become crucial for business success. In this guide, I'll walk you through implementing Afterpay in a Next.js e-commerce application, sharing real-world code examples and best practices.

What is Afterpay?
Afterpay is a "buy now, pay later" service that allows customers to split their purchases into four equal installments, paid every two weeks. It's particularly popular in Australia, New Zealand, the United States, and the United Kingdom.

Prerequisites
Before we begin, make sure you have:

  1. A Next.js application
  2. An Afterpay merchant account
  3. Basic understanding of React and API integration
  4. Axios for API calls

Implementation Overview
Our implementation will cover:

  1. Configuring Afterpay credentials
  2. Setting up the API client
  3. Creating the Afterpay component
  4. Handling checkout flow
  5. Managing payment confirmation
  6. Error handling and validation

1. Setting Up Afterpay Configuration

First, let's set up our Afterpay API client. Create a file lib/afterpay.js:

import axios from "axios";

const AFTERPAY_BASE_URL = "https://global-api-sandbox.afterpay.com/v2";
const AFTERPAY_MERCHANT_ID = "YOUR_MERCHANT_ID";
const AFTERPAY_SECRET_KEY = "YOUR_SECRET_KEY";

// Create base64 encoded auth token
const authToken = Buffer.from(
  `${AFTERPAY_MERCHANT_ID}:${AFTERPAY_SECRET_KEY}`
).toString("base64");

// Create axios instance with default config
export const afterpayApi = axios.create({
  baseURL: AFTERPAY_BASE_URL,
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
    Authorization: `Basic ${authToken}`,
    "User-Agent": `Merchant/${AFTERPAY_MERCHANT_ID}`,
  },
});
Enter fullscreen mode Exit fullscreen mode

2. Creating Helper Functions

// Get Afterpay configuration
export const getAfterpayConfig = async () => {
  try {
    const { data } = await afterpayApi.get("/configuration");
    return data;
  } catch (error) {
    console.error("Afterpay config error:", error.response?.data || error.message);
    throw error;
  }
};

// Format cart items for Afterpay
export const formatAfterpayItems = (cartItems) => {
  return cartItems.map((item) => ({
    name: item.title,
    sku: item._id.toString(),
    quantity: item.quantity,
    price: {
      amount: item.price.toString(),
      currency: "AUD",
    },
    categories: [[item.category, item.subCategory]],
    pageUrl: `/products/${item.handle}`,
    imageUrl: item.featured_image,
  }));
};

// Check if order amount is eligible for Afterpay
export const isOrderEligibleForAfterpay = (amount, config) => {
  if (!config) return false;
  const min = parseFloat(config.minimumAmount.amount);
  const max = parseFloat(config.maximumAmount.amount);
  return amount >= min && amount <= max;
};
Enter fullscreen mode Exit fullscreen mode

3. Creating the Afterpay Component
Here's our React component for the Afterpay payment button:

"use client";

import React, { useEffect, useState } from "react";
import Image from "next/image";

export default function Afterpay({ paymentData }) {
  const [config, setConfig] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // Format checkout data for Afterpay
  const formatCheckoutData = () => {
    const items = formatAfterpayItems(paymentData.orderProducts);

    return {
      amount: {
        amount: paymentData.total.toString(),
        currency: "AUD",
      },
      consumer: {
        phoneNumber: paymentData.shippingAddress.phone,
        givenNames: paymentData.shippingAddress.firstName,
        surname: paymentData.shippingAddress.lastName,
        email: paymentData.shippingAddress.email,
      },
      shipping: {
        name: `${paymentData.shippingAddress.firstName} ${paymentData.shippingAddress.lastName}`,
        line1: paymentData.shippingAddress.address,
        area1: paymentData.shippingAddress.suburb,
        region: paymentData.shippingAddress.state,
        postcode: paymentData.shippingAddress.postcode,
        countryCode: "AU",
        phoneNumber: paymentData.shippingAddress.phone,
      },
      merchant: {
        redirectConfirmUrl: `${YOUR_DOMAIN}/confirm`,
        redirectCancelUrl: `${YOUR_DOMAIN}/cancel`,
        popupOriginUrl: YOUR_DOMAIN,
      },
      items,
      merchantReference: paymentData.orderId,
    };
  };

  // Handle checkout process
  const handleCheckout = async () => {
    if (!config) return;

    setLoading(true);
    setError(null);

    try {
      const checkoutData = formatCheckoutData();
      const response = await fetch("/api/afterpay", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(checkoutData),
      });

      const data = await response.json();

      if (data.error) {
        throw new Error(data.error);
      }

      if (data.redirectCheckoutUrl) {
        // Store data for confirmation
        sessionStorage.setItem("afterpayToken", data.token);
        sessionStorage.setItem("afterpayAmount", paymentData.total.toString());
        sessionStorage.setItem("afterpayOrderId", paymentData.orderId);

        // Redirect to Afterpay checkout
        window.location.href = data.redirectCheckoutUrl;
      }
    } catch (error) {
      setError("Unable to initiate Afterpay checkout. Please try again.");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="w-full">
      <button
        onClick={handleCheckout}
        disabled={loading || !isEligible}
        className="w-full border flex items-center justify-center rounded-lg p-3 px-5 bg-primary-color text-white"
      >
        {loading ? (
          "Processing..."
        ) : (
          <div className="flex items-center gap-2">
            <span>Pay with</span>
            <Image
              src="/afterpay.png"
              alt="Afterpay"
              width={100}
              height={20}
              className="h-5 w-auto object-contain"
            />
          </div>
        )}
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Setting Up API Routes
Create an API route for handling Afterpay requests (app/api/afterpay/route.js):

import { NextResponse } from "next/server";
import { createCheckout, getAfterpayConfig } from "@/lib/afterpay";

export async function GET() {
  try {
    const config = await getAfterpayConfig();
    return NextResponse.json(config);
  } catch (error) {
    return NextResponse.json(
      { error: error.response?.data?.message || error.message },
      { status: error.response?.status || 500 }
    );
  }
}

export async function POST(request) {
  try {
    const checkoutData = await request.json();

    // Validate required fields
    const requiredFields = ["amount", "consumer", "shipping", "merchant"];
    const missingFields = requiredFields.filter(
      (field) => !checkoutData[field]
    );

    if (missingFields.length > 0) {
      return NextResponse.json(
        {
          error: "Missing required fields",
          details: `Missing: ${missingFields.join(", ")}`,
        },
        { status: 400 }
      );
    }

    const response = await createCheckout(checkoutData);
    return NextResponse.json(response);
  } catch (error) {
    return NextResponse.json(
      { error: error.response?.data?.message || error.message },
      { status: error.response?.status || 500 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Handling Payment Confirmation
Create a confirmation page (app/confirm/page.js) to handle successful payments:

"use client";

export default function ConfirmPage() {
  const searchParams = useSearchParams();
  const [processing, setProcessing] = useState(true);
  const [paymentDetails, setPaymentDetails] = useState(null);

  useEffect(() => {
    const handleConfirmation = async () => {
      try {
        const status = searchParams.get("status");
        const orderToken = searchParams.get("orderToken");

        if (status === "SUCCESS" && orderToken) {
          const amount = sessionStorage.getItem("afterpayAmount");
          const merchantReference = sessionStorage.getItem("afterpayOrderId");

          const response = await fetch("/api/afterpay/capture", {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              orderToken,
              amount,
              merchantReference,
            }),
          });

          if (!response.ok) {
            throw new Error("Payment capture failed");
          }

          const data = await response.json();
          setPaymentDetails(data);
        }
      } catch (error) {
        console.error("Confirmation error:", error);
      } finally {
        setProcessing(false);
      }
    };

    handleConfirmation();
  }, [searchParams]);

  // Render confirmation UI
}
Enter fullscreen mode Exit fullscreen mode

Best Practices and Tips

  1. Error Handling: Always implement comprehensive error handling at every step of the payment process.
  2. Validation: Validate all data before sending it to Afterpay to avoid failed API calls.
  3. Security: Never expose your Afterpay credentials in client-side code.
  4. Testing: Use Afterpay's sandbox environment extensively before going live.
  5. User Experience: Provide clear feedback during the payment process and handle edge cases gracefully.

Top comments (0)