Next.js 14 payment gateway integration with P24 (Przelewy24)


A quick introduction to Next.js 14 and P24 (Przelewy 24)
Vercel recently marked a milestone at its fourth annual conference by introducing Next.js 14. This new iteration of the highly respected React framework stands out with its promise of increased speed and user-friendliness for developers. Next.js has been celebrated for its capabilities in server-side rendering (SSR), static site generation (SSG), and incremental static regeneration (ISR), making it a preferred choice for crafting dynamic, high-performance web applications.
On the financial technology front, P24 - Przelewy24 is recognized as a domestic payment institution in Poland, providing a spectrum of payment services, including authorization and clearing mechanisms. To leverage the Przelewy24 API, merchants must first establish an account in the P24 Administration Panel. This registration process unlocks various tools for merchants, such as the ability to oversee their account balance, monitor client payments, and handle refunds.
1- Implementing ShoppingCartModal with Przelewy24 Integration
This section explores the ShoppingCartModal component, where the payment processes are triggered. This component utilizes the useShoppingCart hook, initially developed for Stripe payments. However, it’s equally effective in managing the shopping cart state across the app. The useShoppingCart hook’s straightforward documentation can be a valuable resource for understanding its implementation.
1"use client";23import axios from "axios";4import { toast } from "react-hot-toast";5import { Button } from "@/components/ui/button";6import { Input } from "@/components/ui/input";78import Image from "next/image";910import {11 Sheet,12 SheetContent,13 SheetHeader,14 SheetTitle,15} from "@/components/ui/sheet";16import { useShoppingCart, formatCurrencyString } from "use-shopping-cart";1718const ShoppingCartModal = () => {19 const {20 cartCount,21 shouldDisplayCart,22 handleCartClick,23 cartDetails,24 removeItem,25 totalPrice,26 } = useShoppingCart();2728 //console.log(cartDetails);29 const items = Object.values(cartDetails ?? {}).map((entry) => entry.price_id);303132 const onCheckout = async () => {33 console.log(totalPrice);34 try {35 const response = await axios.post("/api/checkout", {36 amount: totalPrice, // Assuming '3232424' is your amount (as a number, not a string)37 });3839 console.log(response.data);4041 if (response.data.paymentUrl) {42 toast.success("Redirecting to payment...");43 window.location.href = response.data.paymentUrl; // Redirect to the payment URL44 } else {45 toast.error("Payment URL not received");46 }47 } catch (error) {48 console.error(error);49 toast.error("Error during checkout");50 }51 };5253 return (54 <>55 <Sheet open={shouldDisplayCart} onOpenChange={() => handleCartClick()}>56 <SheetContent className="sm:max-w-lg w-[90vw]">57 <SheetHeader>58 <SheetTitle>Cart</SheetTitle>59 </SheetHeader>6061 <div className="h-full flex flex-col justify-between">62 <div className="mt-8 flex-1 overflow-y-auto">63 <ul className="-my-6 divide-y divide-gray-200">64 {cartCount === 0 ? (65 <h1 className="py-6">You do not have any items</h1>66 ) : (67 <>68 {Object.values(cartDetails ?? {}).map((entry) => (69 <li key={entry.id} className="flex py-6 ">70 <div className="h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border border-gray-200">71 <Image72 src={entry.image as string}73 alt="Product Image"74 width={100}75 height={100}76 />77 </div>7879 <div className="ml-4 flex flex-1 flex-col">80 <div>81 <div className="flex justify-between text-base font-medium text-gray-900">82 <h3>{entry.name}</h3>83 <p className="ml-4">{entry.price} PLN</p>84 <p className="mt-1 text-sm text-gray-500 line-clamp-2">85 {entry.description}86 </p>87 </div>8889 <div className="flex flex-1 items-end justify-between text-sm">90 <p className="text-gray-500">91 QTY: {entry.quantity}92 </p>9394 <div className="flex">95 <button96 onClick={() => removeItem(entry.id)}97 type="button"98 className="font-medium text-primary hover:text-primary/80"99 >100 Remove101 </button>102 </div>103 </div>104 </div>105 </div>106 </li>107 ))}108 </>109 )}110 </ul>111 </div>112 <div className="border-t border-gray-200 px-4 py-6 sm:px-6">113 <div className="flex justify-between text-base font-medium text-gray-900">114 <p>Subtotal</p>115 <p>{totalPrice} PLN</p>116 </div>117 <p className="mt-0.5 text-sm text-gray-500">118 Shipping and taxes are calculated at checkout119 </p>120121 <div className="mt-6">122 <Button123 onClick={onCheckout}124 className="w-full bg-black text-white"125 >126 Pay with P24127 </Button>128 </div>129 <div className="mt-6 flex justify-center text-center text-sm text-gray-500">130 <Button className="w-full mt-4">Continue shopping</Button>131 </div>132 </div>133 </div>134 </SheetContent>135 </Sheet>136 </>137 );138};139140export default ShoppingCartModal;
2- Crafting the API Endpoint for Przelewy24 Transactions
In this part, we explore constructing an API endpoint specifically designed to process orders for P24 (Przelewy24). For this purpose, I’ve utilized an efficient wrapper from the @ingameltd/node-przelewy24 package, which greatly simplifies creating and verifying P24 transactions. Kudos to the team for developing such a user-friendly library.
Below is the core code for the API endpoint (api/checkout/route.ts):
1import { NextResponse } from "next/server";23import {4 P24,5 Order,6 Currency,7 Country,8 Language,9 NotificationRequest,10 Verification,11 Encoding,12} from "@ingameltd/node-przelewy24";1314const corsHeaders = {15 "Access-Control-Allow-Origin": "*",16 "Access-Control-Allow-Methods": "POST, GET, PUT, DELETE, OPTIONS",17 "Access-Control-Allow-Headers": "Content-Type, Authorization",18};1920const merchantId = 442323; // you will get it once registered with P2421const posId = process.env.PRZELEWY24_POS_ID;22const crcKey = process.env.PRZELEWY24_CRC_KEY;23const apiKey = process.env.PRZELEWY24_API_KEY;24// Initialize P24 with your credentials and sandbox mode2526//@ts-ignore27const p24 = new P24(merchantId, posId, apiKey, crcKey, { sandbox: true });2829export async function OPTIONS() {30 return NextResponse.json({}, { headers: corsHeaders });31}3233export async function POST(req: Request) {34 if (req.method === "POST") {35 try {36 const body = await req.json();37 const { amount } = body;38 console.log(amount);3940 const order: Order = {41 sessionId: "youneedyourownlogictocreatesessionids",42 amount: amount * 100,43 currency: Currency.PLN,44 description: "test order",45 email: "john.doe@example.com",46 country: Country.Poland,47 language: Language.PL,48 channel: "8192",49 urlReturn: "http://localhost:3000/nowosci",50 urlStatus: "http://localhost:3000",51 timeLimit: 20,52 encoding: Encoding.UTF8,53 };5455 const transactionResult = await p24.createTransaction(order);56 console.log(transactionResult);5758 // Send the payment URL back to the client5960 return NextResponse.json(61 { paymentUrl: transactionResult.link },6263 { headers: corsHeaders }64 );65 } catch (error) {66 console.error(error);6768 return NextResponse.json(69 { error: "Internal Server Error" },70 { headers: corsHeaders }71 );72 }73 } else {74 return NextResponse.json(75 { error: "Method Not Allowed" },76 { headers: corsHeaders }77 );78 }79}
This setup involves initializing the P24 client with the necessary credentials and configuring it for sandbox testing. The POST function handles order requests, creating transactions via Przelewy24, and returns a payment URL to the client. A crucial aspect of this setup is the generation of sessionId, which should be uniquely crafted according to the merchant's own logic. This ID is essential for tracking transactions, validating them, and managing refunds.
The ‘channel’ property in the Przelewy24 (P24) API plays a crucial role in specifying the available payment methods for a transaction. According to the P24 documentation, the ‘channel’ is an integer that represents different payment methods, each denoted by specific enum values:
- 1 - Card payments, including ApplePay and GooglePay
- 2 - Bank transfers
- 4 - Traditional transfers
- 8 - N/A
- 16 - Enables all 24/7 payment methods
- 32 - Use pre-payment
- 64 - Pay-by-link methods only
- 128 - Instalment payment forms
- 256 - Wallets
- 4096 - Card only
- 8192 - Blik
- 16384 - All methods except Blik
To enable multiple payment channels, sum up their corresponding values. For instance, to allow both transfer and traditional transfer methods, set channel to 6 (2 + 4).
Here’s an example of how the ‘channel’ property is implemented in an order configuration:
1const order: Order = {2 sessionId: "youneedyourownlogictocreatesessionids",3 amount: amount * 100,4 currency: Currency.PLN,5 description: "test order",6 email: "good.boy@example.com",7 country: Country.Poland,8 language: Language.PL,9 channel: "8192",10 urlReturn: "http://localhost:3000/success",11 urlStatus: "http://localhost:3000/status",12 timeLimit: 20,13 encoding: Encoding.UTF8,14 };
While on sandbox, we should be redirected to the following:
https://sandbox-go.przelewy24.pl/trnRequest/{token} where the token is granted by P24 based on the approved order.
In this post, I ventured into the integration of the Przelewy24 (P24) payment gateway with Next.js 14, focusing on the essential task of transferring data from the front end to a custom API. I shared insights on setting up P24 and developing an API endpoint using the @ingameltd/node-przelewy24 package. It aims to provide a step-by-step guide for integrating sophisticated payment solutions in Next.js 14 applications.
Happy coding :)
Piotr

