By: Subha Chanda
The e-commerce industry is growing every day. Almost every business is moving towards a digital approach by selling products or services online. As part of this, setting up bank transfers as a payment method to help customers buy products or services through your portal is crucial. The bank transfer method is a highly in-demand option for payment, but implementing it can be a huge pain. Managing various banks’ different techniques and internationalizing is a challenging task, but it can be made much easier with the right solution.
Rapyd can help you implement a bank transfer payment service quickly. With the Rapyd Payment API, you can easily add local payment options for your customers. It enables you to accept payments and transfer funds internationally, so you can explore new territories, expand your business, and manage your funds better. With Rapyd already handling the tricky part, you can focus more on your business.
This article will show you how you can implement the Rapyd payment gateway for a custom website. The Rapyd API is available for the programming languages of your choice. For this article, the backend implementation is built with Node.js and Express. The frontend is created using Next.js 14 and Tailwind CSS.
Here’s a look at the application you’ll be building in this article.
What Is the Rapyd Bank Transfer API?
The Rapyd bank transfer API allows customers to pay directly from their bank account to your account. Many people rely on bank transfers as their primary payment method. Building your own bank transfer payment system would require a significant amount of work. However, this integration can be made simple using the Rapyd API.
Some Use Cases for Rapyd Bank Transfer API
The Rapyd bank transfer API allows you to accept international funds easily, and its primary use cases are listed below:
- Paying for purchases: Using the bank transfer API, your customers can quickly pay for your services. Bank transfer is a popular payment technique, and it’s essential to implement such an option in your payment gateway so that you don’t lose potential customers.
- Accepting international payments: Implementing a bank transfer payment option through the Rapyd API helps you explore new regions for your business without thinking about cross-border problems. Using the API, you can easily add different banks for different countries. Rapyd also helps you manage the cash flow better because all the data is visible through a single, clutter-free dashboard.
- Sending funds internationally: The Rapyd API accommodates your customers too, by allowing them to quickly pay for international services through a single gateway. Additionally, all international funds accepted from various countries can be easily moved to your account.
Why Use the Rapyd Bank Transfer API?
Considering the use cases highlighted, there are many reasons why you might use the Rapyd bank transfer API, but the following two are the most compelling:
- Getting international clients: Setting up different payment systems for other locations and maintaining hundreds of dashboards can be very challenging and time-consuming. You can use the Rapyd bank transfer API to easily integrate localized bank transfer solutions into your application, rendering available payment methods and required fields for your customer and accepting payments. This helps you better reach international clients and further expand your user base.
- Moving international funds easily: Normally, you might have to maintain multiple dashboards and processes to move international funds into your account, but the Rapyd bank transfer API simplifies this process. Your clients can pay with the bank of their choice, and you can accept and manage the funds in a single account from a single dashboard.
The Bank Transfer Workflow with Rapyd API
The bank transfer API workflow is simple. The diagram below explains the process of accepting payments through Rapyd API.
The customer chooses the bank transfer payment method in the checkout window. Upon receiving the request, the application requests that Rapyd processes the transfer. Rapyd does just that, then generates necessary information and sends it back to the application. The application receives the instructions and displays them to the customer.
The process mentioned above is the first step of completing a payment through a bank transfer.
The second step has to be completed from the customer’s and the bank’s end. The customer contacts the bank through a website, application, or branch visit, and asks them to transfer the money.
Once the bank transfer is complete, Rapyd receives a notification from the bank. With the notification received, Rapyd sends a webhook to the application. The application then notifies the client that the payment is successful.
Implementing In-App Payments with the Rapyd Bank Transfer API
Using their APIs, you can use Node.js, Python, C#, .NET, or any other language to implement in-app payments with Rapyd bank transfer. As mentioned, the backend code in this article is written in Node.js and the frontend code is in Next.js. Tailwind CSS is used to style the application. Visit the GitHub repo on Rapyd bank transfers if you want to check out the code before jumping into the application.
First, it’s essential to check a few prerequisites. You must have:
- A Rapyd Client Portal account.
- The Rapyd API key and secret key, which you can access from the Rapyd Client Portal after creating an account.
- Basic knowledge of JavaScript and Node.js.
- Basic understanding of React and Next.js.
- Basic knowledge of Tailwind CSS.
The backend code is simple, as most functions are already available through the sample code provided by Rapyd in their documentation.
You’ll also need to enable the test environment on your Rapyd dashboard to work in a sandbox. Otherwise, you would have to make an actual transaction with real money every time you test your payment gateway. The test environment makes it possible to test your gateway without the use of real money. It’s difficult to mimic the webhook request from Rapyd in the test environment. So, Rapyd has an endpoint that confirms the payment when it receives a valid ID.
To enable sandbox on your Rapyd dashboard, visit the Rapyd dashboard, and and from the menu, activate the toggle button called Sandbox, which will be on the upper-right corner of your screen.
Building the Backend
To build the application’s backend, initialize a folder with npm using npm init -y
. After the folder is initialized, you’ll need to install a few dependencies. The dependencies required for this example are Express, Dotenv, and CORS packages. Other than that, you can install Nodemon as a dev dependency.
Install the dependencies by running npm i express dotenv cors
and npm i -D nodemon
. Once the installation is complete, create a new file called utilities.js
and paste the sample code from the Rapyd documentation. The code is also pasted below for your reference:
require('dotenv').config();
const https = require('https');
const crypto = require('crypto');
const accessKey = process.env.RAPYD_ACCESS_KEY;
const secretKey = process.env.RAPYD_SECRET_KEY;
const log = false;
async function makeRequest(method, urlPath, body = null) {
try {
httpMethod = method;
httpBaseURL = 'sandboxapi.rapyd.net';
httpURLPath = urlPath;
salt = generateRandomString(8);
idempotency = new Date().getTime().toString();
timestamp = Math.round(new Date().getTime() / 1000);
signature = sign(httpMethod, httpURLPath, salt, timestamp, body);
const options = {
hostname: httpBaseURL,
port: 443,
path: httpURLPath,
method: httpMethod,
headers: {
'Content-Type': 'application/json',
salt: salt,
timestamp: timestamp,
signature: signature,
access_key: accessKey,
idempotency: idempotency,
},
};
return await httpRequest(options, body, log);
} catch (error) {
console.error('Error generating request options');
throw error;
}
}
function sign(method, urlPath, salt, timestamp, body) {
try {
let bodyString = '';
if (body) {
bodyString = JSON.stringify(body);
bodyString = bodyString == '{}' ? '' : bodyString;
}
let toSign =
method.toLowerCase() +
urlPath +
salt +
timestamp +
accessKey +
secretKey +
bodyString;
log && console.log(`toSign: ${toSign}`);
let hash = crypto.createHmac('sha256', secretKey);
hash.update(toSign);
const signature = Buffer.from(hash.digest('hex')).toString('base64');
log && console.log(`signature: ${signature}`);
return signature;
} catch (error) {
console.error('Error generating signature');
throw error;
}
}
function generateRandomString(size) {
try {
return crypto.randomBytes(size).toString('hex');
} catch (error) {
console.error('Error generating salt');
throw error;
}
}
async function httpRequest(options, body) {
return new Promise((resolve, reject) => {
try {
let bodyString = '';
if (body) {
bodyString = JSON.stringify(body);
bodyString = bodyString == '{}' ? '' : bodyString;
}
log && console.log(`httpRequest options: ${JSON.stringify(options)}`);
const req = https.request(options, (res) => {
let response = {
statusCode: res.statusCode,
headers: res.headers,
body: '',
};
res.on('data', (data) => {
response.body += data;
});
res.on('end', () => {
response.body = response.body ? JSON.parse(response.body) : {};
log &&
console.log(`httpRequest response: ${JSON.stringify(response)}`);
if (response.statusCode !== 200) {
return reject(response);
}
return resolve(response);
});
});
req.on('error', (error) => {
return reject(error);
});
req.write(bodyString);
req.end();
} catch (err) {
return reject(err);
}
});
}
exports.makeRequest = makeRequest;
You’ll need to create a file called .env
to make the above code work; in it, declare the RAPYD_ACCESS_KEY
and RAPYD_SECRET_KEY
. The access and secret keys can be found on the Rapyd dashboard. The utilities
file helps to connect with the Rapyd API easily. It consists of all the methods for connecting to the API, like signing the connection to make different requests. You can use the makeRequest
function declared here to call the Rapyd API endpoints with specific request types.
The frontend of the application will display a list of products. The products array will be served from the backend. To store the products, create a new file called products.js
and paste in the following code:
const products = [
{
id: 1,
name: "Luminar Spark Drone",
description:
"Advanced drone with superior photography and extended flight time.",
price: 499.99,
image:
"https://i.ibb.co/RgC9RVv/DALL-E-2024-01-05-13-56-40-A-high-performance-drone-with-advanced-photography-capabilities-and-exten.png",
},
{
id: 2,
name: "Aquatix Explorer Watch",
description: "Waterproof, durable watch for divers and adventurers.",
price: 299.99,
image:
"https://i.ibb.co/x7RJ0kS/DALL-E-2024-01-05-13-56-44-A-stylish-waterproof-watch-designed-for-divers-and-adventurers-featuring.png",
},
{
id: 3,
name: "Mystic Brew Coffee Maker",
description:
"State-of-the-art coffee maker for the perfect brew every time.",
price: 199.99,
image:
"https://i.ibb.co/txKXbjs/DALL-E-2024-01-05-13-56-46-An-innovative-coffee-maker-that-uses-state-of-the-art-technology-to-brew.png",
},
{
id: 4,
name: "Quantum Leap VR Headset",
description:
"Immersive VR headset with ultra-high resolution and intuitive controls.",
price: 599.99,
image:
"https://i.ibb.co/QYvB4Z4/DALL-E-2024-01-05-13-56-48-An-immersive-virtual-reality-headset-with-ultra-high-resolution-and-intui.png",
},
{
id: 5,
name: "Galactic Night Telescope",
description: "Powerful telescope for crystal clear celestial views.",
price: 349.99,
image:
"https://i.ibb.co/HVC7tjL/DALL-E-2024-01-05-13-56-50-A-powerful-telescope-designed-for-stargazing-offering-crystal-clear-views.png",
},
{
id: 6,
name: "CyberGarden Botanist",
description: "AI-powered gardening assistant with autonomous plant care.",
price: 399.99,
image:
"https://i.ibb.co/njwdn6J/DALL-E-2024-01-05-14-01-30-An-advanced-AI-powered-gardening-assistant-equipped-with-sensors-to-monit.png",
},
{
id: 7,
name: "AeroSwift Smart Fan",
description: "Innovative bladeless smart fan with app and voice control.",
price: 299.99,
image:
"https://i.ibb.co/fx6BxGj/DALL-E-2024-01-05-14-01-32-A-state-of-the-art-smart-fan-with-a-bladeless-design-controlled-via-smart.png",
},
{
id: 8,
name: "Nebula Star Projector",
description:
"Home planetarium projecting a realistic night sky experience.",
price: 199.99,
image:
"https://i.ibb.co/WVfhm0z/DALL-E-2024-01-05-14-01-34-A-home-planetarium-that-projects-a-realistic-night-sky-various-settings-t.png",
},
];
module.exports = products;
The above list contains eight products with fields like ID, name, description, price, and image. In a real-world scenario, you wouldn’t store your products directly in an array, but this tutorial uses this method for simplicity.
Now, create a primary entry point for your application. For this example, the file is called app.js
. The app.js
file looks like the code shown below:
const express = require("express");
const app = express();
const cors = require("cors");
const products = require("./products");
require("dotenv").config();
const makeRequest = require("./utilities").makeRequest;
// Enable CORS
app.use(cors());
// Parse JSON bodies for this app
app.use(express.json());
// Test Route
app.get("/", (req, res) => {
res.json({
message: "Hello World",
});
});
// Set JSON spaces to 4 for readability
app.set("json spaces", 4);
// Get all products route
app.get("/products", (req, res) => {
res.json(products);
});
// Get a single product route
app.get("/products/:id", (req, res) => {
const { id } = req.params;
const product = products.find((product) => product.id === Number(id));
res.json(product);
});
// Get payment methods route
app.get("/country/:code", async (req, res) => {
const { code } = req.params;
try {
const result = await makeRequest(
"GET",
`/v1/payment_methods/country?country=${code}`
);
res.json(result);
} catch (error) {
res.json(error);
}
});
// Get required fields route
app.get("/fields/:type", async (req, res) => {
const { type } = req.params;
try {
const result = await makeRequest(
"GET",
`/v1/payment_methods/required_fields/${type}`
);
res.json(result);
} catch (error) {
res.json(error);
}
});
// Create a payment route
app.post("/payment/:productId/:currency/:type", async (req, res) => {
try {
const { productId, currency, type } = req.params;
const product = products.find(
(product) => product.id === Number(productId)
);
const { firstName, lastName } = req.body;
const body = {
amount: product.price,
currency: currency,
payment_method: {
type: type,
fields: {
first_name: firstName,
last_name: lastName,
},
},
};
const result = await makeRequest("POST", "/v1/payments", body);
res.json(result);
} catch (error) {
res.json(error);
}
});
// Complete a payment route
app.post("/completePayment", async (req, res) => {
try {
const { paymentId, code, price } = req.body;
let body = {};
if (code) {
body = {
token: paymentId,
param1: code,
param2: price.toString(),
};
} else {
body = {
token: paymentId,
param2: price,
};
}
const result = await makeRequest(
"POST",
"/v1/payments/completePayment",
body
);
res.json(result);
} catch (error) {
res.json(error);
}
});
// Start a server
const PORT = process.env.PORT || 3333;
app.listen(PORT, () => {
console.log(`Server started on port ${PORT}`);
});
Different methods are defined in the above code. For example, to get the payment methods available for a specific country, a GET request is created with the following endpoint /country/:code
. :code
is a variable parameter, namely, the country code. Depending on the country code, it’ll return the available payment options upon hitting the endpoint. By calling the makeRequest
function and passing the parameters of the request type and the endpoint, it returns the values.
For example, you can try hitting the route with an endpoint similar to http://localhost:3333/country/SG
. The body object of the JSON schema will contain all the available payment methods for Singapore because SG
is Singapore’s country code.
Similarly, the /fields/:type
endpoint will return the required fields for a specific payment type. The endpoint of Rapyd for getting the required fields is /v1/payment_methods/required_fields/
, and the base URL for the sandbox environment is sandboxapi.rapyd.net
. The base URL is already declared in the utilities.js
file.
The type
parameter is received from the type
object available inside the body
object of the result of the previous route.
Passing the type
to the route and sending a GET request to the endpoint will return all the required fields for the specific payment method:
// Create a payment route
app.post("/payment/:productId/:currency/:type", async (req, res) => {
try {
const { productId, currency, type } = req.params;
const product = products.find(
(product) => product.id === Number(productId)
);
const { firstName, lastName } = req.body;
const body = {
amount: product.price,
currency: currency,
payment_method: {
type: type,
fields: {
first_name: firstName,
last_name: lastName,
},
},
};
const result = await makeRequest("POST", "/v1/payments", body);
res.json(result);
} catch (error) {
res.json(error);
}
});
The code above creates a payment request to the Rapyd API service. Because this application is focused on the bank transfer API, it doesn’t contain information related to cards or other payment methods. In the above code, the currency
is the currency for the specific country (for example, SGD
for Singapore, USD
for the United States), and the type
is the type of payment method discussed earlier. The productId
is the ID of the product that you’re creating the payment for. The code uses this productId
to extract the necessary details from the products
array.
The product
variable stores the details of the product. The higher order find
method finds the details of a particular product.
The body contains the required fields. Here, the amount shows product.price
, which is the value of the product. You can implement a currency conversion logic to change it depending on the country. The above route sends a POST request to the v1/payments
endpoint of the Rapyd API with the given body. If the response is successful, it returns the answer. Otherwise, it catches the error.
The final endpoint required for this dummy application is the completePayment
route. This route sends a response of payment complete to the backend to mimic the webhook response. The completePayment
endpoint requires at least two values for the bank transfer method:
token
: The payment ID generated by creating a payment
param2
: The actual price for which the payment was created
In case the response from /v1/payments
contains a code
field inside the textual_codes
object, the value of the code
field needs to be passed inside a param1
field.
The paymentId
(which will be used as token
), code
(which will be used as param1
), and price
(which will be used as param2
) are extracted from the request body. If the code
doesn’t exist, the body is created using only the token
and the param1
value; otherwise, the code
is also added for the param1
field.
If you want to learn more about this process, you can check out the official documentation to understand it better.
The Frontend of the Application
With the backend code complete, you’ll now build the frontend using Next.js.
To scaffold a Next.js application, run the following command on the terminal:
npx create-next-app@latest
# or
yarn create next-app
# or
pnpm create next-app
For the styling, Tailwind CSS will be used in your application. When given the option during the scaffolding process, please add Tailwind to your application. You should also opt for the app
router setup for this tutorial. For simplicity, you won’t use Typescript in this example.
The application is a basic landing page for buying products. To customize the styling, you’ll need to modify the CSS file. Remove everything inside the globals.css
file, except the Tailwind directives. The file should now appear as follows:
@tailwind base;
@tailwind components;
@tailwind utilities;
Create a new file called PaymentContext.jsx
inside the components folder. This context will be responsible for managing the state that contains necessary details like paymentId
, payableAmount
, instructions
, etc. Paste the following code into this file:
"use client";
import React, { createContext, useState } from "react";
export const PaymentContext = createContext();
export const PaymentProvider = ({ children }) => {
const [paymentId, setPaymentId] = useState(null);
const [textualCodes, setTextualCodes] = useState(null);
const [instructions, setInstructions] = useState(null);
const [payableAmount, setPayableAmount] = useState(null);
return (
<PaymentContext.Provider
value={{
paymentId,
setPaymentId,
textualCodes,
setTextualCodes,
instructions,
setInstructions,
payableAmount,
setPayableAmount,
}}
>
{children}
</PaymentContext.Provider>
);
};
The ”use client”
directive converts this component to a client component; without it, this code will not work. This file exports two entities: PaymentContext
and PaymentProvider
. PaymentContext
is created using the React createContext
method, generating a context object. PaymentProvider
utilizes the Provider
component from PaymentContext
to share the state variables paymentId
, textualCodes
, instructions
, and payableAmount
across components. Each state variable has a corresponding state update function (setPaymentId
, setTextualCodes
, setInstructions
, and setPayableAmount
) that enables components to modify these variables. The context is now set up, and you can integrate it into your application.
Open the layout.js
file inside the app
directory and replace this with the following code:
"use client";
import { Inter } from "next/font/google";
import { PaymentProvider } from "../components/PaymentContext";
import "./globals.css";
import Link from "next/link";
const inter = Inter({ subsets: ["latin"] });
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={`${inter.className} max-w-6xl mx-auto`}>
<Link className="flex justify-center items-center my-6" href="/">
<svg
id="logo-55"
width="168"
height="41"
viewBox="0 0 168 41"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{" "}
<path
d="M49.2775 28.9524H61.2295V25.3524H53.5255V11.7924H49.2775V28.9524Z"
className="cneutral"
fill="#3C2B1F"
></path>{" "}
<path
d="M68.3107 26.5524C66.6067 26.5524 65.7187 25.0644 65.7187 22.8324C65.7187 20.6004 66.6067 19.0884 68.3107 19.0884C70.0147 19.0884 70.9267 20.6004 70.9267 22.8324C70.9267 25.0644 70.0147 26.5524 68.3107 26.5524ZM68.3347 29.3364C72.2947 29.3364 74.8867 26.5284 74.8867 22.8324C74.8867 19.1364 72.2947 16.3284 68.3347 16.3284C64.3987 16.3284 61.7587 19.1364 61.7587 22.8324C61.7587 26.5284 64.3987 29.3364 68.3347 29.3364Z"
className="cneutral"
fill="#3C2B1F"
></path>{" "}
<path
d="M81.7411 33.2004C83.5651 33.2004 85.1731 32.7924 86.2531 31.8324C87.2371 30.9444 87.8851 29.6004 87.8851 27.7524V16.6644H84.1411V17.9844H84.0931C83.3731 16.9524 82.2691 16.3044 80.6371 16.3044C77.5891 16.3044 75.4771 18.8484 75.4771 22.4484C75.4771 26.2164 78.0451 28.2804 80.8051 28.2804C82.2931 28.2804 83.2291 27.6804 83.9491 26.8644H84.0451V28.0884C84.0451 29.5764 83.3491 30.4404 81.6931 30.4404C80.3971 30.4404 79.7491 29.8884 79.5331 29.2404H75.7411C76.1251 31.8084 78.3571 33.2004 81.7411 33.2004ZM81.7171 25.3764C80.2531 25.3764 79.2931 24.1764 79.2931 22.3284C79.2931 20.4564 80.2531 19.2564 81.7171 19.2564C83.3491 19.2564 84.2131 20.6484 84.2131 22.3044C84.2131 24.0324 83.4211 25.3764 81.7171 25.3764Z"
className="cneutral"
fill="#3C2B1F"
></path>{" "}
<path
d="M95.5835 26.5524C93.8795 26.5524 92.9915 25.0644 92.9915 22.8324C92.9915 20.6004 93.8795 19.0884 95.5835 19.0884C97.2875 19.0884 98.1995 20.6004 98.1995 22.8324C98.1995 25.0644 97.2875 26.5524 95.5835 26.5524ZM95.6075 29.3364C99.5675 29.3364 102.159 26.5284 102.159 22.8324C102.159 19.1364 99.5675 16.3284 95.6075 16.3284C91.6715 16.3284 89.0315 19.1364 89.0315 22.8324C89.0315 26.5284 91.6715 29.3364 95.6075 29.3364Z"
className="cneutral"
fill="#3C2B1F"
></path>{" "}
<path
d="M103.302 28.9524H107.214V16.6644H103.302V28.9524ZM103.302 14.9604H107.214V11.7924H103.302V14.9604Z"
className="cneutral"
fill="#3C2B1F"
></path>{" "}
<path
d="M108.911 33.0084H112.823V27.6804H112.871C113.639 28.7124 114.767 29.3364 116.351 29.3364C119.567 29.3364 121.703 26.7924 121.703 22.8084C121.703 19.1124 119.711 16.3044 116.447 16.3044C114.767 16.3044 113.567 17.0484 112.727 18.1524H112.655V16.6644H108.911V33.0084ZM115.343 26.3124C113.663 26.3124 112.703 24.9444 112.703 22.9524C112.703 20.9604 113.567 19.4484 115.271 19.4484C116.951 19.4484 117.743 20.8404 117.743 22.9524C117.743 25.0404 116.831 26.3124 115.343 26.3124Z"
className="cneutral"
fill="#3C2B1F"
></path>{" "}
<path
d="M128.072 29.3364C131.288 29.3364 133.664 27.9444 133.664 25.2564C133.664 22.1124 131.12 21.5604 128.96 21.2004C127.4 20.9124 126.008 20.7924 126.008 19.9284C126.008 19.1604 126.752 18.8004 127.712 18.8004C128.792 18.8004 129.536 19.1364 129.68 20.2404H133.28C133.088 17.8164 131.216 16.3044 127.736 16.3044C124.832 16.3044 122.432 17.6484 122.432 20.2404C122.432 23.1204 124.712 23.6964 126.848 24.0564C128.48 24.3444 129.968 24.4644 129.968 25.5684C129.968 26.3604 129.224 26.7924 128.048 26.7924C126.752 26.7924 125.936 26.1924 125.792 24.9684H122.096C122.216 27.6804 124.472 29.3364 128.072 29.3364Z"
className="cneutral"
fill="#3C2B1F"
></path>{" "}
<path
d="M138.978 29.3124C140.682 29.3124 141.762 28.6404 142.65 27.4404H142.722V28.9524H146.466V16.6644H142.554V23.5284C142.554 24.9924 141.738 26.0004 140.394 26.0004C139.146 26.0004 138.546 25.2564 138.546 23.9124V16.6644H134.658V24.7284C134.658 27.4644 136.146 29.3124 138.978 29.3124Z"
className="cneutral"
fill="#3C2B1F"
></path>{" "}
<path
d="M148.168 28.9524H152.08V22.0644C152.08 20.6004 152.8 19.5684 154.024 19.5684C155.2 19.5684 155.752 20.3364 155.752 21.6564V28.9524H159.664V22.0644C159.664 20.6004 160.36 19.5684 161.608 19.5684C162.784 19.5684 163.336 20.3364 163.336 21.6564V28.9524H167.248V20.9604C167.248 18.2004 165.856 16.3044 163.072 16.3044C161.488 16.3044 160.168 16.9764 159.208 18.4644H159.16C158.536 17.1444 157.312 16.3044 155.704 16.3044C153.928 16.3044 152.752 17.1444 151.984 18.4164H151.912V16.6644H148.168V28.9524Z"
className="cneutral"
fill="#3C2B1F"
></path>{" "}
<path
d="M25.4099 1.97689L21.4769 0.923031L18.1625 13.2926L15.1702 2.12527L11.2371 3.17913L14.4701 15.2446L6.41746 7.19201L3.53827 10.0712L12.371 18.904L1.37124 15.9566L0.317383 19.8896L12.336 23.11C12.1984 22.5165 12.1256 21.8981 12.1256 21.2627C12.1256 16.7651 15.7716 13.1191 20.2692 13.1191C24.7668 13.1191 28.4128 16.7651 28.4128 21.2627C28.4128 21.894 28.3409 22.5086 28.205 23.0986L39.1277 26.0253L40.1815 22.0923L28.1151 18.8591L39.1156 15.9115L38.0617 11.9785L25.9958 15.2115L34.0484 7.15895L31.1692 4.27976L22.459 12.99L25.4099 1.97689Z"
className="ccustom"
fill="#F97316"
></path>{" "}
<path
d="M28.1943 23.1444C27.8571 24.57 27.1452 25.8507 26.1684 26.8768L34.0814 34.7899L36.9606 31.9107L28.1943 23.1444Z"
className="ccustom"
fill="#F97316"
></path>{" "}
<path
d="M26.0884 26.9596C25.0998 27.9693 23.8505 28.7228 22.4495 29.1111L25.3289 39.8571L29.2619 38.8032L26.0884 26.9596Z"
className="ccustom"
fill="#F97316"
></path>{" "}
<path
d="M22.3026 29.1504C21.6526 29.3175 20.9713 29.4063 20.2692 29.4063C19.517 29.4063 18.7886 29.3043 18.0971 29.1134L15.2151 39.8692L19.1481 40.923L22.3026 29.1504Z"
className="ccustom"
fill="#F97316"
></path>{" "}
<path
d="M17.9581 29.0737C16.5785 28.6661 15.3514 27.903 14.383 26.8904L6.45052 34.8229L9.32971 37.7021L17.9581 29.0737Z"
className="ccustom"
fill="#F97316"
></path>{" "}
<path
d="M14.3168 26.8203C13.365 25.8013 12.6717 24.5377 12.3417 23.1341L1.38334 26.0704L2.43719 30.0034L14.3168 26.8203Z"
className="ccustom"
fill="#F97316"
></path>{" "}
</svg>
</Link>
<PaymentProvider>{children}</PaymentProvider>
</body>
</html>
);
}
The code above uses the Inter Google font for the body, and an SVG is placed inside the body to display a logo. The SVG is wrapped inside the Link
component with the URL set to the root.
The children
prop is wrapped inside the PaymentProvider
so that the whole application can access the context.
The basic structure is now ready. It’s time to create a component that renders the products. Create a component called ProductCard.jsx
inside the components
folder. Here’s the code for the component:
import Link from "next/link";
const ProductCard = ({ product }) => {
return (
<>
<Link
href={`/${product.id}`}
className="md:max-w-sm w-full rounded overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 hover:cursor-pointer group h-[36rem]"
>
<img
src={product.image}
alt={product.name}
className="w-full h-2/3 object-cover hover:scale-105 transition-all duration-300"
/>
<div className="px-6 py-4 h-1/3 overflow-y-auto">
<div className="font-bold text-xl mb-2">{product.name}</div>
<p className="text-gray-700 text-sm">{product.description}</p>
<span className="inline-block bg-green-500 rounded-full px-3 py-1 text-sm font-semibold text-white mr-2 mb-2 shadow-md group-hover:bg-green-600 transition-all duration-300 mt-8">
SGD {product.price}
</span>
</div>
</Link>
</>
);
};
export default ProductCard;
The code represents a basic card that takes a few props, which are used to show the name, description, image, and price of a product. Instead of using the native img
tag, you could opt for the Next/Image component, but that would require an additional step to display remote URLs.
Replace the content of /app/page.js
file with the following code:
import ProductCard from "../components/ProductCard";
const getProducts = async () => {
const res = await fetch("http://localhost:3333/products");
const products = await res.json();
return products;
};
export default async function Home() {
const products = await getProducts();
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-xl font-bold tracking-tight mb-4">All Products</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{products.map((product) => (
<ProductCard product={product} key={product.id} />
))}
</div>
</div>
);
}
This component uses React server components to fetch and display the products. The getProducts
method calls the /products
route of the backend and returns the products. Instead of using a third-party library like Axios, the Next.js fetch API is used, which extends the native fetch
API.
Inside the main component, a map
function is called to render all the products. For each product, the ProductCard
component is rendered. Your homepage is now ready, and it should look like the image shown below:
The next step is to create a page that shows the description of the course and the available payment methods.
Create a new file called PaymentMethods.jsx
inside the components
folder. This component contains the most features in the demo application. Let’s take a look at the component first:
"use client";
import { useContext, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { PaymentContext } from "./PaymentContext";
const PaymentMethods = ({ productId }) => {
const Router = useRouter();
const [data, setData] = useState(null);
const [isLoading, setLoading] = useState(false);
const [country, setCountry] = useState("sg");
const [currencies, setCurrencies] = useState(null);
const [type, setType] = useState(null);
const [firstName, setFirstName] = useState(null);
const [lastName, setLastName] = useState(null);
const { setPaymentId, setTextualCodes, setInstructions, setPayableAmount } =
useContext(PaymentContext);
useEffect(() => {
setLoading(true);
(async () => {
if (country !== null || undefined) {
const res = await fetch(`http://localhost:3333/country/${country}`);
const data = await res.json();
setData(data.body.data);
}
})();
setLoading(false);
}, [country]);
const handleClick = async (currency, type) => {
const res = await fetch(
`http://localhost:3333/payment/${productId}/${currency}/${type}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ firstName, lastName }),
}
);
const data = await res.json();
if (data.body.status.status === "SUCCESS") {
setPaymentId(data.body.data.id);
setTextualCodes(data.body.data.textual_codes);
setInstructions(data.body.data.instructions);
setPayableAmount(data.body.data.original_amount);
Router.push(`/${productId}/instructions`);
} else {
Router.push("/Error");
}
};
if (isLoading) return <p>Loading...</p>;
if (!data) return <p>Loading Payment Methods ...</p>;
return (
<>
<h4 className="text-base tracking-tight mb-4 mt-6 text-slate-700">
Available Payment Methods
</h4>
<form
className="mb-4"
onSubmit={(e) => {
e.preventDefault();
handleClick(currencies[0], type);
}}
>
<select
className="w-full p-2 border border-gray-300 rounded-md shadow-sm text-sm"
onChange={(e) => {
const selectedItem = data.find(
(item) => item.type === e.target.value
);
setCurrencies(selectedItem.currencies);
setType(selectedItem.type);
}}
>
<option value="" disabled selected hidden>
Please Choose...
</option>
{data
.filter((item) => item.category === "bank_transfer")
.map((filteredItem) => (
<option key={filteredItem.type} value={filteredItem.type}>
{filteredItem.name}
</option>
))}
</select>
{currencies && type && (
<div className="mt-4">
<input
type="text"
placeholder="First Name"
className="w-full p-2 border border-gray-300 rounded-md shadow-sm text-sm"
onChange={(e) => setFirstName(e.target.value)}
required
/>
<input
type="text"
placeholder="Last Name"
className="w-full p-2 border border-gray-300 rounded-md shadow-sm text-sm mt-4"
onChange={(e) => setLastName(e.target.value)}
required
/>
</div>
)}
<button
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded transition duration-300 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed mt-4"
disabled={currencies === null && type === null}
type="submit"
>
Get Instructions
</button>
</form>
</>
);
};
export default PaymentMethods;
A fewuseState
hooks are used within the above code:
- The
data
hook: This hook stores the available payment methods for a specific country. The data to store insidedata
is retrieved by hitting the endpointhttp://localhost:3333/country/${country}
. - The
country
hook: This hook stores the country code of the user. In this case, Singapore (sg
) is used by default. You could use an API to determine the country of the user to make it more dynamic, but this code uses a specific country for easier implementation. - The
isLoading
hook: This hook is used to check if the endpoint to receive the available payment method is still loading. - The
currencies
hook: To store the available currency details of the payment method. - The
type
hook: The type of the payment method is stored in this useState hook. - The
firstName
hook: To store the first name of the user who is purchasing a product. - The
lastName
hook: To store the last name of the user who is purchasing a product.
The component also contains a function called handleClick
to store the specific data in the PaymentContext
. When you hit the endpoint http://localhost:3333/payment/${productId}/${currency}/${type}
with the parameters, Rapyd creates a payment request with a status of ACT
, which means the payment is active but not complete.
Once the ACT
status is generated, you must take the necessary steps to complete the payment. The data object received by hitting the above endpoint contains the required steps, payment ID, the textual ID to show to the bank, and other important information, such as the eWallet ID, payment method type, next action to be taken, etc. This information is stored inside different PaymentContext
objects.
Finally, because this article focuses only on the bank transfer API, the application will only render the payment methods that have the category of bank_transfer
.
<form
className="mb-4"
onSubmit={(e) => {
e.preventDefault();
handleClick(currencies[0], type);
}}
>
<select
className="w-full p-2 border border-gray-300 rounded-md shadow-sm text-sm"
onChange={(e) => {
const selectedItem = data.find(
(item) => item.type === e.target.value
);
setCurrencies(selectedItem.currencies);
setType(selectedItem.type);
}}
>
<option value="" disabled selected hidden>
Please Choose...
</option>
{data
.filter((item) => item.category === "bank_transfer")
.map((filteredItem) => (
<option key={filteredItem.type} value={filteredItem.type}>
{filteredItem.name}
</option>
))}
</select>
{currencies && type && (
<div className="mt-4">
<input
type="text"
placeholder="First Name"
className="w-full p-2 border border-gray-300 rounded-md shadow-sm text-sm"
onChange={(e) => setFirstName(e.target.value)}
required
/>
<input
type="text"
placeholder="Last Name"
className="w-full p-2 border border-gray-300 rounded-md shadow-sm text-sm mt-4"
onChange={(e) => setLastName(e.target.value)}
required
/>
</div>
)}
<button
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded transition duration-300 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed mt-4"
disabled={currencies === null && type === null}
type="submit"
>
Get Instructions
</button>
</form>
This code renders a list of all the available banks that allow bank transfers. It also contains two input fields for first and last names and a submit button. The form’s onSubmit
function calls the handleClick
function with the first currency from the currencies
array and the type
as arguments.
The two input fields for first and last names are conditionally rendered based on whether currencies
and type
are true. The onChange
event handlers for these input fields update the firstName
and lastName
state variables with the inputted values.
The above component will be displayed when the user clicks on a specific product. To make a dynamic route, create a new folder called [id]
inside the app
directory, and create a page.jsx
file inside it. Paste in the following code:
import React from "react";
import PaymentMethods from "../../components/PaymentMethods";
const getProductDetails = async (id) => {
const res = await fetch(`http://localhost:3333/products/${id}`);
const data = await res.json();
return data;
};
export default async function ProductDetails({ params }) {
const product = await getProductDetails(params.id);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-xl font-bold tracking-tight mb-4">Product Details</h1>
<div className="flex flex-col md:flex-row gap-8">
<div className="md:w-1/2">
<img
src={product.image}
alt={product.name}
className="max-w-full h-auto rounded-lg shadow-lg hover:shadow-2xl transition-all duration-300"
/>
</div>
<div className="md:w-1/2 flex flex-col justify-center">
<h2 className="text-2xl font-bold mb-3">{product.name}</h2>
<p className="text-gray-700 mb-4">{product.description}</p>
<div className="text-lg font-semibold">
<span className="text-green-500">SGD {product.price}</span>
</div>
<PaymentMethods productId={params.id} />
</div>
</div>
</div>
);
}
The page fetches the details of the product from the localhost:3333/products/:id
endpoint using the getProductDetails
function. The params.id
is passed inside this function to fetch the details of the specific product. Some Tailwind styles are used to display the details, and finally, the PaymentMethods
component is rendered, and the product ID is passed into it. The product details page should look like this at this point:
Now, create a component called Instructions.jsx
inside the components
folder, and paste in the following code to display the necessary steps required to complete the payment:
"use client";
import { PaymentContext } from "../components/PaymentContext";
import { useContext, useEffect, useState } from "react";
const InstructionsComponent = () => {
const {
paymentId: payment_id,
textualCodes: textual_codes,
instructions,
payableAmount,
} = useContext(PaymentContext);
const [loading, setLoading] = useState(false);
const [paymentSuccess, setPaymentSuccess] = useState(false);
const [error, setError] = useState(null);
const completePayment = async (payment_id) => {
try {
setLoading(true);
const res = await fetch("http://localhost:3333/completePayment", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
paymentId: payment_id,
code: textual_codes.code ? textual_codes.code : null,
price: payableAmount,
}),
});
const data = await res.json();
if (data.body.data.status === "CLO") {
setPaymentSuccess(true);
}
setLoading(false);
} catch (error) {
console.log(error);
setError(error);
setLoading(false);
}
};
useEffect(() => {
if (paymentSuccess) {
setTimeout(() => {
setPaymentSuccess(false);
}, 3000);
setTimeout(() => {
window.location.href = "http://localhost:3000";
}, 3000);
}
}, [paymentSuccess]);
return (
<div className="flex flex-wrap my-12 md:gap-x-3">
<div className="md:w-8/12 w-full bg-gray-100 rounded-sm px-4 py-2">
<h4 className="text-base tracking-tight mb-4 mt-6 text-slate-700">
Thank you for your order!
</h4>
<p className="text-sm text-gray-700 mb-4">
Please note that your payment is processed <strong>BUT</strong> it is{" "}
<strong className="uppercase">not yet complete</strong>.
<br />
Please follow the instructions below to complete the payment.
</p>
<div>
<h6 className="text-sm text-gray-700 mb-2 font-bold">Payment ID</h6>
<div className="border-1 border-slate-300 border px-4 py-2 rounded-sm bg-white">
{payment_id ? payment_id : "No payment id"}
</div>
<div className="text-md md:text-lg text-black font-semibold mt-8">
<h6 className="text-sm text-gray-700 mb-2 font-bold">
Textual Codes
</h6>
<div className="ml-2">
{textual_codes && (
<p className="mb-4 text-sm">
{Object.keys(textual_codes)}:{" "}
<span className="text-slate-600 text-sm">
{Object.values(textual_codes)}
</span>
</p>
)}
</div>
<h6 className="text-sm text-gray-700 mb-2 font-bold">
Instructions
</h6>
{instructions && (
<ul className="list-disc list-inside">
{instructions[0].steps.map((step, index) => (
<li key={index} className="text-sm font-normal ml-2 block">
<span className="text-slate-600 text-sm uppercase font-bold">
{Object.keys(step)}
</span>
: {Object.values(step)}
</li>
))}
</ul>
)}
</div>
</div>
{paymentSuccess && (
<div className="bg-green-500 mt-4 px-4 py-4 w-full text-bold text-white rounded-sm my-2">
Payment Successful! Redirecting to home page...
</div>
)}
{error && (
<div className="bg-red-500 mt-4 px-4 py-4 w-full text-bold text-white rounded-sm my-2">
Payment Successful!
</div>
)}
</div>
<div className="md:w-3/12 w-full bg-gray-100 rounded-sm px-4 py-2">
<h4 className="text-base tracking-tight mb-4 mt-6 text-slate-700">
Payable Amount
</h4>
<hr />
<div className="text-lg text-black font-semibold bg-white px-4 py-2 rounded-md flex justify-between">
<h6 className="text-sm text-gray-700 mb-2 font-bold">Amount</h6>
<h6 className="text-sm text-gray-700 mb-2 font-bold">
SGD {payableAmount ? payableAmount : 0}
</h6>
</div>
<button
disabled={!payment_id || loading}
className="px-6 py-3 rounded-lg bg-blue-600 hover:bg-blue-700 text-sm text-white transition duration-300 mt-6 w-full disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => completePayment(payment_id)}
>
Complete the Transaction
</button>
</div>
</div>
);
};
export default InstructionsComponent;
This component shows the instructions, codes, payable amount and other relevant information, all of which are retrieved from PaymentContext
. It also displays a success or error message depending on the API response.
In the above code, there’s a button containing a method called completePayment
. This method requires a request body containing the payment ID, a code retrieved from the textual_codes
object, and the price. The method then sends a request to the endpoint http://localhost:3333/completePayment
, simulating the webhook request Rapyd sends upon a payment complete notification from the bank. The endpoint changes the ACT
status to CLO
, indicating that the payment was successful and is now closed. Once the status changes to CLO
, a success message is displayed, and the user is redirected to the homepage.
To use this component, create a folder called instructions
inside the app/[id]
directory, and create a file called page.jsx
inside this folder. Paste the following code into it:
import InstructionsComponent from "../../../components/Instructions";
import React from "react";
export default function Instructions() {
return (
<div>
<InstructionsComponent />
</div>
);
}
The instructions page will look like the image below:
Get The Code
Get the code, build something amazing with the Rapyd API, and share it with us here in the developer community. Hit reply below if you have questions or comments.