
Integrating a reliable payment system is crucial for any web application that handles transactions. In this tutorial, we'll walk you through building a bKash payment system using Next.js 14, TypeScript, and Tailwind CSS. We'll utilize the Next.js App Router system to create a seamless payment experience, including a minimal home page for initiating payments, a callback page to handle bKash responses, and a success page to confirm successful transactions.
Before diving into the implementation, ensure you have the following:
First, create a new Next.js project with TypeScript support.
npx create-next-app@latest bkash-payment-system --typescript cd bkash-payment-system
This command initializes a new Next.js project named bkash-payment-system with TypeScript configurations.
We'll need several dependencies, including zod for schema validation, uuid for generating unique invoice numbers, and jsonwebtoken for token handling. Additionally, we'll set up Tailwind CSS for styling.
Install the necessary packages:
npm install zod uuid jsonwebtoken
For Tailwind CSS, follow the official installation guide:
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p
Configure tailwind.config.js:
/** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./app/**/*.{js,ts,jsx,tsx}", // Adjust paths as needed "./components/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], };
Create globals.css in the styles directory and include Tailwind's base styles:
@tailwind base; @tailwind components; @tailwind utilities;
Import globals.css in your application entry point, typically in app/layout.tsx:
import './globals.css'; export const metadata = { title: 'bKash Payment System', description: 'Integrate bKash payments with Next.js', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body>{children}</body> </html> ); }
Securely store sensitive information using environment variables. Create a .env.local file in the root of your project:
# bKash API Credentials BKASH_API_KEY=your_bkash_api_key BKASH_SECRET_KEY=your_bkash_secret_key BKASH_USERNAME=your_bkash_username BKASH_PASSWORD=your_bkash_password BKASH_GRANT_TOKEN_URL=https://checkout.sandbox.bka.sh/v1.2.0-beta/checkout/token/grant BKASH_CREATE_PAYMENT_URL=https://checkout.sandbox.bka.sh/v1.2.0-beta/checkout/payment/create BKASH_EXECUTE_PAYMENT_URL=https://checkout.sandbox.bka.sh/v1.2.0-beta/checkout/payment/execute BKASH_REFUND_TRANSACTION_URL=https://checkout.sandbox.bka.sh/v1.2.0-beta/checkout/payment/refund NEXT_PUBLIC_URL=http://localhost:3000/ # Update with your production URL NEXT_PUBLIC_BKASH_JWT_SECRET=your_jwt_secret
Note: Replace the placeholder values with your actual bKash API credentials and URLs. Ensure that .env.local is included in your .gitignore to prevent exposing sensitive data.
We'll create middleware for bKash authentication and API routes for creating and executing payments.
Create a file bkash-middleware.ts in the root or a suitable directory:
// bkash-middleware.ts export const bkash_auth = async () => { try { const response = await fetch(process.env.BKASH_GRANT_TOKEN_URL as string, { method: "POST", body: JSON.stringify({ app_key: process.env.BKASH_API_KEY, app_secret: process.env.BKASH_SECRET_KEY, }), headers: { "Content-Type": "application/json", Accept: "application/json", username: process.env.BKASH_USERNAME as string, password: process.env.BKASH_PASSWORD as string, }, }); const data = await response.json(); return data.id_token; } catch (error) { return new Response((error as any).message || "You're not authorized", { status: 401, }); } };
Create the API route to handle payment creation.
// /app/api/bkash/create/route.ts import { z } from "zod"; import { bkash_auth } from "../../../bkash-middleware"; import { v4 as uuidv4 } from "uuid"; import { tokenizeJSON } from "@/lib/jwt-functions"; import { generateId } from "@/lib/generate-id"; export async function POST(req: Request) { try { const json = await req.json(); const { amount, details } = z .object({ amount: z.string({ required_error: "Amount must be a string" }), details: z.string({ required_error: "Details must be a string" }), }) .parse(json); const tokenizedPlanDetails = tokenizeJSON(details); const id_token = await bkash_auth(); const response = await fetch(process.env.BKASH_CREATE_PAYMENT_URL as string, { method: "POST", body: JSON.stringify({ mode: "0011", payerReference: " ", callbackURL: `${process.env.NEXT_PUBLIC_URL}api/bkash/callback`, amount: amount, currency: "BDT", intent: "sale", merchantInvoiceNumber: "Inv" + uuidv4().substring(0, 5), sku: generateId(), }), headers: { "Content-Type": "application/json", Accept: "application/json", authorization: id_token, "x-app-key": process.env.BKASH_API_KEY as string, }, }); const data = await response.json(); if (!data.bkashURL) { return new Response(JSON.stringify(data), { status: 401 }); } return new Response(JSON.stringify({ bkashURL: data.bkashURL }), { status: 201, }); } catch (error) { console.log(error, "from error"); if (error instanceof z.ZodError) { return new Response(JSON.stringify(error.issues), { status: 422 }); } return new Response(null, { status: 500 }); } }
Create the API route to process bKash callbacks.
// /app/api/bkash/callback/route.ts import { z } from "zod"; import afterSuccessfullyPayFunc from "@/lib/payment/callback/after-successfully-payment-func"; export async function POST(req: Request) { try { const json = await req.json(); const { paymentID, id_token, tokenizedPlanDetails } = z .object({ paymentID: z.string(), id_token: z.string(), tokenizedPlanDetails: z.string({ required_error: "TokenizedPlanDetails must be a string", }), }) .parse(json); const response = await fetch(process.env.BKASH_EXECUTE_PAYMENT_URL as string, { method: "POST", body: JSON.stringify({ paymentID }), headers: { "Content-Type": "application/json", Accept: "application/json", authorization: id_token, "x-app-key": process.env.BKASH_API_KEY as string, }, }); const data = await response.json(); if (data && data.statusCode === "0000") { const { details } = await afterSuccessfullyPayFunc({ tokenizedPlanDetails, data, id_token, source: "bkash", }); return new Response( JSON.stringify({ success: true, dbName: details.dbName }), { status: 201 } ); } else { return new Response(JSON.stringify({ success: false }), { status: 201 }); } } catch (error) { console.log((error as any).message, "from error"); return new Response(JSON.stringify({ message: "bKash payment failed" }), { status: 500, }); } }
"use client"; import { toast } from "@/components/ui/use-toast"; // Importing toast notifications import { useSearchParams } from "next/navigation"; // Hook to access URL query parameters import { useEffect } from "react"; /** * CallbackClientComponent * This component handles the payment callback from Bkash. * It checks the payment status and updates the subscription status accordingly. * * @returns JSX.Element - A loading spinner during processing */ const CallbackClientComponent = () => { const searchParams = useSearchParams(); // Access query parameters from the URL // Extract necessary parameters from the query const paymentID = searchParams.get("paymentID"); const status = searchParams.get("status"); // Payment status: success, failure, or cancel const id_token = searchParams.get("id_token"); // Authentication token for Bkash API const priceId = searchParams.get("priceId"); // Price ID for the subscription plan const tokenizedPlanDetails = searchParams.get("tokenizedPlanDetails"); // Encrypted details of the plan useEffect(() => { // Redirect to the billing page with a message if the payment was canceled or failed if (status === "cancel" || status === "failure") { window.location.href = `/dashboard/billing?message=${status}`; } /** * successFunc * Handles successful payments by sending a callback request to the backend * to update the user's subscription details. */ const successFunc = async () => { const response = await fetch(`/api/users/bkash/payment/callback`, { method: "POST", body: JSON.stringify({ paymentID, id_token, priceId, tokenizedPlanDetails, }), }); console.log(tokenizedPlanDetails); // Debug: log plan details // Check if the response is successful if (!response.ok) { toast({ title: "Error", description: "Something went wrong. Please try again.", variant: "destructive", }); // Redirect to failed page if the response is not successful return (window.location.href = `/dashboard/billing/failed?success=${false}`); } else { const { success, dbName } = await response.json(); // Extract response data // If the subscription update is successful if (success) { toast({ title: "Success", description: "Subscription complete", variant: "default", }); // Redirect to success page with database name window.location.href = `/dashboard/billing/success?success=${true}&dbName=${dbName}`; return; } else { // Handle unexpected errors toast({ description: "Something went wrong. Please try again.", variant: "destructive", }); window.location.href = `/dashboard/billing/failed?success=${false}`; return; } } }; // If the payment status is successful, initiate success function if (status === "success") { try { successFunc(); // Attempt to update subscription } catch (error) { console.error(error); // Redirect to billing page with error message if an error occurs window.location.href = `/dashboard/billing?message=${ (error as any).message }`; } } }, []); // Empty dependency array means this useEffect runs once after the initial render // Loading spinner displayed during processing return ( <div className="w-full h-full flex justify-center items-center"> <div className="w-8 h-8 rounded-full border-customBurntOrange border-2 border-t-transparent animate-spin"></div> </div> ); }; export default CallbackClientComponent;
Create a function to handle post-payment actions, such as updating the database.
// /lib/payment/callback/after-successfully-payment-func.ts "use server"; import { getCurrentUser } from "@/lib/session"; import { db } from "@/lib/db"; import { verify, JwtPayload, Secret } from "jsonwebtoken"; import { PayemtDetialsType } from "@/types"; import { stripe } from "@/lib/stripe"; const afterSuccessfullyPayFunc = async ({ tokenizedPlanDetails, data, source, id_token, }: { tokenizedPlanDetails: string; data: any; source: "stripe" | "bkash"; id_token?: string; }) => { const user = await getCurrentUser(); const details = verify( tokenizedPlanDetails, process.env.NEXT_PUBLIC_BKASH_JWT_SECRET as Secret ) as JwtPayload & { data: PayemtDetialsType }; const currentDate = new Date(); const customerId = source === "stripe" ? data.client_secret : data.trxID; const paymentId = source === "stripe" ? data.id : data.paymentID; const purchasingTime = details.buyAnnually ? parseInt(process.env.THIRTY_DAY as string) * 12 : parseInt(process.env.THIRTY_DAY as string); try { await db.$transaction(async (transaction) => { await transaction.user.update({ where: { id: user!.id }, data: { stripeSubscriptionId: details.dbName, stripeCurrentPeriodEnd: currentDate.toISOString(), stripeCustomerId: customerId, stripePriceId: paymentId, buyAnnually: details.buyAnnually, }, }); await transaction.userPay.create({ data: { buyAnnually: details.buyAnnually, userId: user!.id, paymentId: paymentId, totalMessageAvailable: details.message.toString(), availableMessage: details.message.toString(), planName: details.dbName, totalPrice: details.botPrice.toString(), planValid: new Date(currentDate.getTime() + purchasingTime), }, }); }); console.log("Transaction completed successfully."); return { details }; } catch (error) { console.error("Transaction failed:", error); if (source === "stripe") { try { await stripe.refunds .create({ payment_intent: data.payment_intent as string, }) .then((d) => console.log({ expire: d })); console.log( "Stripe payment intent canceled successfully due to transaction failure." ); } catch (cancelError) { console.log("Stripe payment not canceled", cancelError); } } else if (source === "bkash") { try { await fetch(process.env.BKASH_REFUND_TRANSACTION_URL as string, { method: "POST", body: JSON.stringify({ paymentID: data.paymentID, amount: data.amount, trxID: data.trxID, sku: "payment", reason: "cashback", }), headers: { "Content-Type": "application/json", Accept: "application/json", authorization: id_token as string, "x-app-key": process.env.BKASH_API_KEY as string, }, }).then((res) => res.json()); } catch (cancelError) { console.error("Failed to cancel bKash payment:", cancelError); } } throw new Error("Transaction failed. Please try again."); } }; export default afterSuccessfullyPayFunc;
Note: Ensure that the getCurrentUser, db, tokenizeJSON, and other imported functions are correctly implemented in your project.
We'll design a minimal home page where users can initiate a bKash payment.
Create a home page at app/page.tsx:
// /app/page.tsx "use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { toast } from "@/components/ui/use-toast"; const HomePage = () => { const [amount, setAmount] = useState(""); const [details, setDetails] = useState(""); const [isLoading, setIsLoading] = useState(false); const router = useRouter(); const handlePayment = async () => { setIsLoading(true); try { const response = await fetch("/api/bkash/create", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ amount, details }), }); const data = await response.json(); if (response.ok) { window.location.href = data.bkashURL; } else { toast({ title: "Payment Failed", description: data.message || "Unable to initiate payment.", variant: "destructive", }); } } catch (error) { toast({ title: "Error", description: "Something went wrong. Please try again.", variant: "destructive", }); console.error(error); } finally { setIsLoading(false); } }; return ( <div className="min-h-screen flex items-center justify-center bg-gray-100 p-4"> <div className="bg-white p-8 rounded shadow-md w-full max-w-md"> <h1 className="text-2xl font-bold mb-6 text-center">bKash Payment</h1> <div className="mb-4"> <label className="block text-gray-700 mb-2">Amount (BDT)</label> <input type="number" value={amount} onChange={(e) => setAmount(e.target.value)} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring focus:border-blue-300" placeholder="Enter amount" /> </div> <div className="mb-6"> <label className="block text-gray-700 mb-2">Details</label> <textarea value={details} onChange={(e) => setDetails(e.target.value)} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring focus:border-blue-300" placeholder="Enter payment details" ></textarea> </div> <button onClick={handlePayment} disabled={isLoading} className="w-full bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600 transition-colors" > {isLoading ? "Processing..." : "Pay with bKash"} </button> </div> </div> ); }; export default HomePage;
The callback page processes the response from bKash after the user completes the payment.
Ensure you have the callback API route set up as shown in the API Routes section.
Create a client-side component to handle the callback.
// /components/callback-client-component.tsx "use client"; import { toast } from "@/components/ui/use-toast"; import { useSearchParams } from "next/navigation"; import { useEffect } from "react"; const CallbackClientComponent = () => { const searchParams = useSearchParams(); const paymentID = searchParams.get("paymentID"); const status = searchParams.get("status"); const id_token = searchParams.get("id_token"); const priceId = searchParams.get("priceId"); const tokenizedPlanDetails = searchParams.get("tokenizedPlanDetails"); useEffect(() => { if (status === "cancel" || status === "failure") { window.location.href = `/dashboard/billing?message=${status}`; } const successFunc = async () => { const response = await fetch(`/api/bkash/callback`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ paymentID, id_token, tokenizedPlanDetails, }), }); if (!response.ok) { toast({ title: "Error", description: "Something went wrong. Please try again.", variant: "destructive", }); window.location.href = `/dashboard/billing/failed?success=false`; } else { const { success, dbName } = await response.json(); if (success) { toast({ title: "Success", description: "Subscription complete", variant: "default", }); window.location.href = `/dashboard/billing/success?success=true&dbName=${dbName}`; } else { toast({ description: "Something went wrong. Please try again.", variant: "destructive", }); window.location.href = `/dashboard/billing/failed?success=false`; } } }; if (status === "success") { try { successFunc(); } catch (error) { console.error(error); window.location.href = `/dashboard/billing?message=${(error as any).message}`; } } }, [status, paymentID, id_token, tokenizedPlanDetails]); return ( <div className="w-full h-full flex justify-center items-center"> <div className="w-8 h-8 rounded-full border-b-2 border-green-500 animate-spin"></div> </div> ); }; export default CallbackClientComponent;
Create a callback page that uses the CallbackClientComponent. Create app/api/bkash/callback/page.tsx:
// /app/api/bkash/callback/page.tsx import CallbackClientComponent from "@/components/callback-client-component"; const CallbackPage = () => { return <CallbackClientComponent />; }; export default CallbackPage;
Note: Adjust the file paths according to your project structure. Ensure that the callback URL in the bKash API matches the path to this callback page.
After a successful payment, redirect users to a confirmation page.
Create a success page at app/dashboard/billing/success/page.tsx:
// /app/dashboard/billing/success/page.tsx "use client"; import { useSearchParams } from "next/navigation"; const SuccessPage = () => { const searchParams = useSearchParams(); const success = searchParams.get("success"); const dbName = searchParams.get("dbName"); return ( <div className="min-h-screen flex items-center justify-center bg-green-100 p-4"> <div className="bg-white p-8 rounded shadow-md text-center"> <h1 className="text-3xl font-bold text-green-600 mb-4">Payment Successful!</h1> <p className="text-gray-700 mb-6">Thank you for your purchase.</p> {dbName && ( <p className="text-gray-600">Your subscription ID: <span className="font-semibold">{dbName}</span></p> )} <button onClick={() => (window.location.href = "/")} className="mt-6 bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600 transition-colors" > Go to Dashboard </button> </div> </div> ); }; export default SuccessPage;
Start the Development Server:
npm run dev
Navigate to the Home Page:
Open http://localhost:3000 in your browser. You should see the bKash payment form.
Initiate a Payment:
bKash Payment Page:
You should be redirected to the bKash payment page. Complete the payment using the sandbox credentials provided by bKash.
Callback Handling:
After completing the payment, bKash will redirect you to the callback page, which processes the response and navigates you to the success page.
Success Confirmation:
On the success page, verify that the payment was successful and that the subscription ID is displayed.
Integrating bKash payments into your Next.js application enhances its functionality by providing a reliable payment method for users. By following this guide, you've set up a secure and efficient payment system using Next.js 14, TypeScript, and Tailwind CSS. Remember to handle all edge cases and ensure that your environment variables and API credentials are securely managed. For production deployments, switch from sandbox URLs to live bKash endpoints and perform thorough testing to ensure seamless transactions.
Comments
Arfatur 2/28/2025
Topic: title
comment
ReactJS 2/28/2025
Topic: comment 2
comment
Replied to Arfaturtest 3 2/28/2025
Topic: test
test
Replied to ArfaturLeave a replay
Your email address will not be publish. Required fields are marked *