Building a bKash Payment System with Next.js 14, TypeScript, and Tailwind CSS Arfatur Rahman
February 15, 2025
Comments (3)

Building a bKash Payment System with Next.js 14, TypeScript, and Tailwind CSS

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.

Table of Contents

  1. Prerequisites
  2. Setting Up the Next.js Project
  3. Installing Dependencies
  4. Configuring Tailwind CSS
  5. Setting Up Environment Variables
  6. Implementing the bKash Middleware and API Routes
  7. Creating the Home Page
  8. Building the Callback Page
  9. Designing the Success Page
  10. Testing the Payment Flow
  11. Conclusion

Prerequisites

Before diving into the implementation, ensure you have the following:

  • Node.js (v14 or higher)
  • npm or yarn
  • Basic knowledge of Next.js, TypeScript, and React
  • A bKash merchant account to obtain API credentials

Setting Up the Next.js Project

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.


Installing Dependencies

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>
  );
}

Setting Up Environment Variables

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.


Implementing the bKash Middleware and API Routes

We'll create middleware for bKash authentication and API routes for creating and executing payments.

1. bKash Middleware (bkash-middleware.ts)

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,
    });
  }
};

2. Creating a Payment (/api/bkash/create.ts)

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 });
  }
}

3. Handling the Callback (/api/bkash/callback.ts)

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,
    });
  }
}

Callback component

"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;

4. Post-Payment Processing Function (after-successfully-payment-func.ts)

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.


Creating the Home Page

We'll design a minimal home page where users can initiate a bKash payment.

1. Home Page Component (page.tsx)

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;

2. Explanation

  • State Management: We manage amount, details, and isLoading states using React's useState hook.
  • Payment Handler: The handlePayment function sends a POST request to the /api/bkash/create endpoint with the payment details. If successful, it redirects the user to the bKash payment URL. If there's an error, it displays a toast notification.
  • Styling: Tailwind CSS classes are used to style the form, ensuring a responsive and clean UI.

Building the Callback Page

The callback page processes the response from bKash after the user completes the payment.

1. Callback API Route

Ensure you have the callback API route set up as shown in the API Routes section.

2. Callback Client Component (CallbackClientComponent.tsx)

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;

3. Integrating the Callback Component

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.


Designing the Success Page

After a successful payment, redirect users to a confirmation page.

1. Success Page (success/page.tsx)

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;

2. Explanation

  • Parameter Extraction: The component extracts success and dbName from the URL query parameters.
  • UI Design: A clean and responsive design using Tailwind CSS to inform the user of a successful payment.
  • Navigation: A button redirects users back to the dashboard or home page.

Testing the Payment Flow

  1. Start the Development Server:

    npm run dev
    
  2. Navigate to the Home Page:

    Open http://localhost:3000 in your browser. You should see the bKash payment form.

  3. Initiate a Payment:

    • Enter a random amount (e.g., 100).
    • Provide payment details (e.g., Test payment).
    • Click on "Pay with bKash."
  4. bKash Payment Page:

    You should be redirected to the bKash payment page. Complete the payment using the sandbox credentials provided by bKash.

  5. 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.

  6. Success Confirmation:

    On the success page, verify that the payment was successful and that the subscription ID is displayed.


Conclusion

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.

Tags:

TypeScriptNextJS

About the Author

Arfatur Rahman

Arfatur Rahman

Software Developer

I’m Arfatur Rahman, a Full-Stack and AI-Driven Software Developer with deep expertise in building modern SaaS platforms, RAG-based AI applications, scalable APIs, and real-time web systems. My work focuses on combining high-quality engineering with cutting-edge AI technologies to create applications that are reliable, secure, and capable of intelligent decision-making.

I specialize in technologies such as Next.js, React, TypeScript, Node.js, Prisma, Supabase, MongoDB, and Azure—alongside advanced AI stacks including LangChain, Vector Databases, embeddings generation, and Retrieval-Augmented Generation (RAG). I develop production-ready AI chatbots, knowledge-bases, automation tools, and full-stack platforms that integrate seamlessly with OpenAI, Gemini, Mistral, and Azure AI.

My engineering approach emphasizes performance, scalability, and clean architecture, enabling me to build systems that handle real-world traffic, complex data pipelines, secure authentication flows, and modern ISR/SSR strategies in Next.js. I’m passionate about developing intelligent applications that blend strong backend engineering with real-world AI capabilities—ensuring high performance, reliability, and future-proof design.

📍 Chittagong, Bangladesh📞 +880 1819 439 292📧 [email protected]

Comments

Arfatur 2/28/2025

Topic: title

comment

ReactJS 2/28/2025

Topic: comment 2

comment

Replied to Arfatur

test 3 2/28/2025

Topic: test

test

Replied to Arfatur

Leave a replay

Your email address will not be publish. Required fields are marked *