How to Integrate Worldline Payment Gateway in Nextjs 14

How to Integrate Worldline Payment Gateway in Nextjs 14

ยท

4 min read

It's been a minute, but let's get technical! I just finished integrating Worldline payments, and I thought I could leave some notes here in case any dev wants a quick rundown. I think this is one hell of a payment handler that deserves more attention!

What's Worldline and Why Should You Care?

It's a payment solution that offers "hosted checkout," where Worldline.com handles all the sensitive payment stuff on their secure servers. This means less headache with PCI DSS compliance and better security for your users. Win-win!

Project Setup and Structure

First up, you'll need a Next.js project. Assuming you've got that sorted (if not, just bun create next-app or check the Next.js docs), here's what our project structure will look like:

๐Ÿ“ฆ nextjs-store
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ layout.tsx
โ”‚   โ”œโ”€โ”€ page.tsx
โ”‚   โ””โ”€โ”€ shop/
โ”‚       โ”œโ”€โ”€ checkout/
โ”‚       โ”‚   โ”œโ”€โ”€ page.tsx                # Checkout form
โ”‚       โ”‚   โ””โ”€โ”€ status/
โ”‚       โ”‚       โ””โ”€โ”€ page.tsx            # Order confirmation page
โ”œโ”€โ”€ features/
โ”‚   โ””โ”€โ”€ payment-gateway/
โ”‚       โ”œโ”€โ”€ payment.tsx                 # Payment form component
โ”‚       โ””โ”€โ”€ client.ts                   # Payment gateway client
โ””โ”€โ”€ api/
    โ””โ”€โ”€ create-hosted-checkout/
        โ””โ”€โ”€ route.ts                    # API route for checkout sessions

Setting Up the Worldline Client

First things first, let's set up our connection to Worldline. Create a new file for your client configuration:

// features/payment-gateway/client.ts
const onlinePaymentsSdk = require("onlinepayments-sdk-nodejs");

export const AznHostedCheckouClient = onlinePaymentsSdk.init({
  host: "payment.preprod.anzworldline-solutions.com.au",
  apiKeyId: process.env.WORLDLINE_API_KEY_ID,
  secretApiKey: process.env.WORLDLINE_SECRET_API_KEY,
  integrator: "OnlinePayments",
});

This is our handler that performs a handshake and provides proof of identity to Worldline for you to be able to get the services

Building the Checkout Page

This is where users will review their order and initiate payment.It will logically look like this depending on your implementation, here is snippet of mine:

// app/shop/checkout/page.tsx
export default function CheckoutPage() {
  const router = useRouter();
  const [isLoading, setIsLoading] = useState(false);
  const [user, setUser] = useState(null);
  const { items, getCartTotals } = useCartStore();
  const { subtotal, shipping, totalDiscount, tax, total } = getCartTotals();

  const handleCheckout = async () => {
    if (!user) {
      toast.error("Please log in to complete your purchase");
      return;
    }

    setIsLoading(true);

    try {
      // Prepare order data
      const orderData = {
        items: items.map((item) => ({
          productId: item.id,
          quantity: item.quantity,
          price: item.price,
        })),
        totals: {
          subtotal,
          shipping,
          totalDiscount,
          tax,
          total,
        },
      };

      // Create hosted checkout session
      const response = await fetch("/api/create-hosted-checkout", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(orderData),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.message || "Checkout creation failed");
      }

      // Redirect to Worldline's hosted checkout page
      if (data.isSuccess && data.body.redirectUrl) {
        window.location.replace(data.body.redirectUrl);
      }
    } catch (error) {
      console.error("Checkout error:", error);
      toast.error("Payment processing failed. Please try again.");
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="max-w-7xl mx-auto px-4 py-8 mt-24">
      <Card>
        <CardHeader>
          <CardTitle>Order Summary</CardTitle>
        </CardHeader>
        <CardContent>
          {/* Order summary content */}
          <Button
            onClick={handleCheckout}
            disabled={isLoading}
            className="min-w-[200px]"
          >
            {isLoading ? "Processing..." : "Proceed to Payment โ†’"}
          </Button>
        </CardContent>
      </Card>
    </div>
  );
}

API Routes

We need two main API endpoints: one to create the checkout session and another to handle the return from Worldline:

// api/create-hosted-checkout/route.ts
export async function POST(request: NextRequest) {
  const session = await auth();
  if (!session) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { items, totals } = await request.json();

  try {
    // Create initial order record
    const initialOrder = await createOrder({
      userId: session.user.id,
      status: "Pending",
      items: items.map((item) => ({
        productId: item.productId,
        quantity: item.quantity,
        price: item.price,
      })),
      payment: {
        amount: totals.total,
        status: "Pending",
      },
    });

    // Set up hosted checkout request
    const createHostedCheckoutRequest = {
      order: {
        amountOfMoney: {
          amount: Math.round(totals.total * 100), // Convert to cents!
          currencyCode: "AUD",
        },
      },
      cardPaymentMethodSpecificInput: {
        authorizationMode: "SALE",
      },
      hostedCheckoutSpecificInput: {
        returnUrl: `${baseUrl}/shop/checkout/status?orderId=${initialOrder.id}`,
      },
    };

    const createHostedCheckoutResponse =
      await AznHostedCheckouClient.hostedCheckout.createHostedCheckout(
        "test281",
        createHostedCheckoutRequest,
        null
      );

    return NextResponse.json({
      ...createHostedCheckoutResponse,
      orderId: initialOrder.id,
    });
  } catch (error) {
    console.error("Error creating payment:", error);
    return NextResponse.json(
      { error: "Failed to create payment", details: error.message },
      { status: 500 }
    );
  }
}

// Handle return from Worldline
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const hostedCheckoutId = searchParams.get("hostedCheckoutId");
  const orderId = searchParams.get("orderId");

  if (!hostedCheckoutId || !orderId) {
    return NextResponse.json(
      { error: "Missing required parameters" },
      { status: 400 }
    );
  }

  try {
    const response = await AznHostedCheckouClient.hostedCheckout.getHostedCheckout(
      "test281",
      hostedCheckoutId,
      null
    );

    const payment = response?.body?.createdPaymentOutput?.payment;

    // Update order based on payment status
    if (
      payment?.status === "CAPTURED" ||
      response?.body?.status === "PAYMENT_CREATED"
    ) {
      await updateOrderStatus(orderId, "Paid", payment);
    } else {
      await updateOrderStatus(orderId, "Canceled");
    }

    return NextResponse.json({
      success: true,
      body: response.body,
      order: updatedOrder,
    });
  } catch (error) {
    console.error("Error getting hosted checkout status:", error);
    return NextResponse.json(
      { error: "Failed to get checkout status" },
      { status: 500 }
    );
  }
}

Overview

Worldline might not be as widely known as Stripe in the dev community, but it's a robust choice for payment processing. The hosted checkout solution makes security a breeze, and once you get past the initial setup, it's pretty developer-friendly.

Check out Worldline's official documentation for more advanced features and updates.


ย