By: Kevin Kimani
A guaranteed refund system allows users to subscribe to a service with an assurance that they can cancel within a specified time frame if they're dissatisfied and receive a full refund. This not only builds trust but also encourages potential subscribers to take the plunge, knowing they have an exit strategy if the service does not meet their expectations.
Rapyd is a leading global fintech platform, and its API capabilities extend to various financial operations, including subscriptions and refunds. With Rapyd's subscription and refund features, you can create a powerful subscription-based service with a streamlined refund process, making it efficient and seamless for both the service provider and the subscriber.
In this tutorial, you'll learn how to integrate Rapyd's API to establish a guaranteed refund system for your subscription-based service using Next.js. In this scenario, you'll assume that you're a service provider offering a subscription-based service with a weekly billing of $1.99. Your customer in the US can subscribe using the "us_debit_visa_card" payment method, as per the available options listed here. You'll implement a function to allow subscribers to cancel within the first week for a full refund if they're unsatisfied with the service.
Prerequisites
To follow along, you'll need the following:
- Node.js and git CLI, a code editor and a browser, installed on your local machine
- A Rapyd account
- Postman set up on your local machine. You can also use the Postman web version.
- Access to the Rapyd Postman collection
- Learn how to make your request to the Rapyd API.
Cloning the Starter Template
To make it easy to follow along, you can clone and build on the premade starter template. To clone it to your local machine, execute the following:
git clone --single-branch -b starter-template https://github.com/kimanikevin254/rapyd-guaranteed-refund-system.git
This will create a folder named rapyd-guaranteed-refund-system
. Execute the commands below to cd
into the project folder, install the project dependencies, and run the Next.js application:
cd rapyd-guaranteed-refund-system && npm i && npm run dev
Once all the dependencies have been installed and the Next.js server is running, navigate to http://localhost:3000/
on your browser, and you should see the login page.
The starter template contains a basic authentication system that allows users to log in and sign up to access the subscription dashboard. It uses an SQLite database to store these credentials. The database has one user whose credentials are already prefilled in the login form. You can click the Login button to access the subscription dashboard.
This dashboard contains hard-coded data that shows your current subscription. It also allows you to log out.
To build on this starter template and implement a guaranteed refund system with the Rapyd API, you'll do the following:
- Create a service and a plan.
- Create a customer with a payment method and simulate 3DS authentication.
- Create a subscription for a customer.
- Enable the customer to cancel the subscription and request a refund.
Creating a Service and a Plan
In Rapyd, a service is a type of product that is attached to a plan that defines the pricing structure for the service. To create a service, you'll use the Rapyd Postman collection. Once you import the collection into Postman, you should have something similar to this:
You now need to create an environment containing the Rapyd access and secret keys, as well as the base URI where you'll send the API requests. Select Environments on the left sidebar and click Create Environment to create a new environment. Add the variables rapyd_access_key
, rapyd_secret_key
, and base_uri
. For base_uri
, provide the value https://sandboxapi.rapyd.net/v1
. For rapyd_access_key
and rapyd_secret_key
, you can get their values by following the instructions provided in the official documentation under "Get Your API Keys". Make sure you save the changes. After setting up your environment, you should end up with something like this:
Select Collections from the left sidebar and load the newly created environment in the top-right corner. To create the service, search "create services" from the Rapyd API Postman collection. From the results, select "Create Services". Replace the request body with the following:
{
"name": "CineView Unlimited",
"type": "services",
"active": true,
"description": "Unlimited access to a vast library of movies"
}
The fields in the code above are as follows:
-
name
defines the name of the service that will be displayed to the customer. -
type
indicates that this is a service. -
active
indicates that this product is available for purchase. -
description
provides details about the service.
Execute the POST request, and from the response you receive, take note of the product ID, which takes the form product_***********
. You'll use this to create a plan.
Next, from the Postman collection, search "create plan", and from the results, select "Create Plan with product ID". Replace the request body with the following:
{
"currency": "USD",
"amount": "1.99",
"interval": "week",
"product": "product_***********",
"billing_scheme": "per_unit",
"nickname": "CineView Unlimited - Weekly Plan",
"trial_period_days": 0,
"metadata": {
"merchant_defined": true
},
"usage_type": "licensed"
}
In this JSON object, interval
, amount
, and currency
indicate that the product defined by product_id
is billed weekly at $1.99. billing_scheme
has the value per_unit
, which indicates that the specified amount is charged for each unit. usage_type
is set to licensed
, which means that the customer will be billed even when they have not used the service. nickname
provides a full description of the plan.
Make sure you replace product_***********
with the product ID you copied earlier. Execute the POST request, and from the response you receive, take note of the plan ID, which takes the form plan_***********
. You'll use it to create a subscription for the customer.
Creating a Customer and Simulating 3DS Authentication
You'll now create the customer who will subscribe to your subscription-based service. To do this, you'll modify the code in the starter template to incorporate the following logic:
- When a user clicks the Sign Up button on your site, you will attempt to create a customer on Rapyd.
- If the customer is successfully created on Rapyd, you will store their credentials in the SQLite database. This makes it possible to retrieve the Rapyd customer ID from the database when a user logs into your site.
- After you store the details in the database, you will then simulate 3DS authentication to authenticate the user's card.
Configuring the Starter Template
First, you need to configure a few things in the starter template so that your application can communicate with the Rapyd API. To do this, create the file .env in the project root folder and add the following:
RAPYD_ACCESS_KEY=<your-rapyd-access-key>
RAPYD_SECRET_KEY=<your-rapyd-secret-key>
Remember to replace the placeholder values with the corresponding values for the Rapyd secret and access keys that you obtained earlier.
Next, create a new directory named utils
in the project root folder. In the newly created directory, create a makeRequest.js file and add the code below, which is provided by Rapyd to help in interacting with their APIs:
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 {
let httpMethod = method;
let httpBaseURL = "sandboxapi.rapyd.net";
let httpURLPath = urlPath;
let salt = generateRandomString(8);
let idempotency = new Date().getTime().toString();
let timestamp = Math.round(new Date().getTime() / 1000);
let 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);
}
});
}
export { makeRequest };
Creating a Customer on Rapyd
Next, you need to modify the code in the app/api/auth/signup/route.js file to add the functionality to create a customer on Rapyd.
Add the following import statement at the top of the file:
import { makeRequest } from "@/utils/makeRequest";
Next, replace the code that is used to create the users
table with the following:
await db.run(
`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT UNIQUE,
password TEXT,
rapyd_cust_id TEXT UNIQUE
)`
);
The new code adds the rapyd_cust_id
field, which stores the customer's Rapyd ID.
Next, find the code below, which is contained in the same file:
// Create the user
await db.run(`INSERT INTO users (name, email, password) VALUES (?, ?, ?)`, [
data.fullname,
data.email,
await hashPassword(data.password), // You need to await the result of the asynchronous hashPassword function
]);
// Fetch the created user
const user = await db.get(`SELECT id, name, email FROM users WHERE email = ?`, [
data.email,
]);
// Return the user data as a json response
return new Response(
JSON.stringify({
user: {
id: user.id,
name: user.name,
email: user.email,
},
}),
{
headers: { "Content-Type": "application/json" },
status: 200,
}
);
Replace this code with the following:
// Create a customer in Rapyd
try {
const body = {
name: data.fullname,
email: data.email,
payment_method: {
type: "us_debit_visa_card",
fields: {
number: data.card_number,
expiration_month: data.expiration_month,
expiration_year: data.expiration_year,
name: data.fullname,
cvv: data.cvv,
},
},
addresses: [
{
name: data.fullname,
line_1: data.address_line,
country: data.country,
zip: data.zip_code,
},
],
};
const result = await makeRequest("POST", "/v1/customers", body);
// Create the user
await db.run(
`INSERT INTO users (name, email, password, rapyd_cust_id) VALUES (?, ?, ?, ?)`,
[
data.fullname,
data.email,
await hashPassword(data.password), // Await the result of the asynchronous hashPassword function
result.body.data.id,
]
);
// Fetch the created user
const user = await db.get(
`SELECT id, name, email, rapyd_cust_id FROM users WHERE email = ?`,
[data.email]
);
// Return the user data as a json response
return new Response(
JSON.stringify({
user: {
id: user.id,
name: user.name,
email: user.email,
rapyd_cust_id: user.rapyd_cust_id,
card_auth_link:
result.body.data.payment_methods.data[0].redirect_url,
},
}),
{
headers: { "Content-Type": "application/json" },
status: 200,
}
);
} catch (error) {
console.error("Error completing request", error);
// Return an error
return new Response(
JSON.stringify({
error: "Unable to create the customer on Rapyd.",
}),
{
headers: { "Content-Type": "application/json" },
status: 400,
}
);
}
The new code attempts to create a customer with a payment method on Rapyd before storing the credentials in the SQLite database. Once a customer is successfully created on Rapyd, their credentials are stored in the database and the code returns a response with some data. In the returned data is card_auth_link
, which contains a link used to simulate 3DS authentication. This link is extracted from the response received from the Rapyd API after creating the customer.
Passing the Necessary Props for 3DS Authentication
Now you need to update the app/page.js file to include a state that will store the 3DS authentication link returned from the API call and pass it down to the Signup
component via the FormSwitcher
component. The 3DS authentication link also needs to be passed down to the Dashboard
component so that the 3DS authentication page can be displayed to users who have not authenticated their cards.
Add the following state to the app/page.js file:
// Set the link to perform 3DS auth
const [cardAuthLink, setCardAuthLink] = useState(null)
You also need to modify the JSX as shown below to pass the state to the appropriate components:
<div className='p-4 h-screen w-screen overflow-hidden'>
{
!user ?
<FormSwitcher checkUserInfo={checkUserInfo} setCardAuthLink={setCardAuthLink} /> :
<Dashboard checkUserInfo={checkUserInfo} cardAuthLink={cardAuthLink} setCardAuthLink={setCardAuthLink} />
}
</div>
Open the components/FormSwitcher.js file and make sure you are destructuring the new props that you just passed down in the previous step:
function FormSwitcher({ checkUserInfo, setCardAuthLink }) {
// Rest of the code
In the same file, make sure you are passing the setCardAuthLink
function to the Signup
component.
<Signup checkUserInfo={checkUserInfo} setCardAuthLink={setCardAuthLink} />
Next, you need to modify the components/Signup.js file to use the setCardAuthLink
function to update the cardAuthLink
state with the 3DS authentication link received from the API call. In the components/Signup.js file, destructure all the props passed to this component as shown below:
function Signup({ checkUserInfo, setCardAuthLink }) {
In the createCustomer
function, add the following code above the localStorage.setItem('user_info', JSON.stringify(data.user))
line of code:
// cardAuthLink state update
if (data.user.card_auth_link) {
setCardAuthLink(data.user.card_auth_link)
}
This code accesses the 3DS authentication link from the response and updates the cardAuthLink
state.
Checking If a Customer's Card Is Authenticated
You need to implement an API endpoint that you will use to confirm if the customer's card has been authenticated. Create a new file retrievecustomer/route.js in the app/api/auth
folder and add the following code:
import { makeRequest } from "@/utils/makeRequest";
export async function POST(req, res){
const { customer_id } = await req.json()
try {
const result = await makeRequest('GET', `/v1/customers/${customer_id}`);
const next_action = result.body.data.payment_methods.data[0].next_action
return new Response(
JSON.stringify({
message: next_action,
}),
{
headers: { "Content-Type": "application/json" },
status: 200,
}
);
} catch (error) {
console.error("Error completing request", error);
// Return an error
return new Response(
JSON.stringify({
error: "Unable to retrieve the customer",
}),
{
headers: { "Content-Type": "application/json" },
status: 400,
}
);
}
}
This code extracts a customer_id
from the request body and makes an HTTP GET request to the Rapyd API to retrieve information related to the customer. From the response received, it extracts the next_action
and then returns the appropriate response based on the success or failure of the request.
Updating the Login Logic
You need to update the login logic so that a customer can log in to your application and retrieve the subscriptions. To do this, open the app/api/auth/login/route.js file and replace the locate the following code:
return new Response(JSON.stringify({
user: {
id: existingUser.id,
name: existingUser.name,
email: existingUser.email,
}
}), {
headers: { 'Content-Type': 'application/json' },
status: 200
})
Replace it with the following:
return new Response(JSON.stringify({
user: {
id: existingUser.id,
name: existingUser.name,
email: existingUser.email,
rapyd_cust_id: existingUser.rapyd_cust_id
}
}), {
headers: { 'Content-Type': 'application/json' },
status: 200
})
The new code returns rapyd_cust_id
as part of the response, which is saved to the browser's local storage in the Login
component. This makes it easy to retrieve the customer's details from Rapyd when the customer logs in to your application.
Creating a Subscription
You've now implemented the logic for creating a customer and simulating 3DS authentication, so the next step is to create a subscription for the customer.
To do this, create a new folder named subscriptions
in the app/api
folder. In the newly created folder, create the file create/route.js and add the code below:
import { makeRequest } from "@/utils/makeRequest";
export async function POST(req, res) {
const { customer_id } = await req.json();
try {
const body = {
customer: customer_id,
billing: "pay_automatically",
cancel_at_period_end: true,
days_until_due: 0,
simultaneous_invoice: true,
subscription_items: [
{
plan: "<your-plan-id>",
quantity: 1,
},
],
};
const result = await makeRequest(
"POST",
"/v1/payments/subscriptions",
body
);
return new Response(
JSON.stringify({
message: "Subscription created successfully",
}),
{
headers: { "Content-Type": "application/json" },
status: 200,
}
);
} catch (error) {
console.error("Error completing request", error);
return new Response(
JSON.stringify({
error: "Unable to create subscription.",
}),
{
headers: { "Content-Type": "application/json" },
status: 400,
}
);
}
}
This code retrieves the customer's ID from the request body and attempts to create a subscription for the customer. Remember to replace <your-plan-id>
with the plan ID that you copied from Postman earlier.
In the JSON object that you pass to Rapyd, customer
defines the ID of the customer you want to create a subscription for. billing
determines the billing method at the end of the billing cycle. It is set to pay_automatically
, which instructs Rapyd to create a payment object and attempt to make a payment using the designated payment method. cancel_at_period_end
indicates that the subscription is canceled at the end of the current billing period. subscription_items
defines the plans that the user is subscribing to and the quantity.
The code returns a response with the message "Subscription created successfully" if the request is successful. If an error is encountered, the code returns a response with the error message.
Retrieving Subscriptions
To retrieve a customer's subscriptions, create a new file list/route.js in the app/api/subscriptions
folder and paste in the code below:
import { makeRequest } from "@/utils/makeRequest";
export async function POST(req, res) {
const { customer_id } = await req.json();
try {
const result = await makeRequest(
"GET",
`/v1/payments/subscriptions?customer=${customer_id}`
);
return new Response(
JSON.stringify({
subscriptions: result.body.data,
}),
{
headers: { "Content-Type": "application/json" },
status: 200,
}
);
} catch (error) {
console.error("Error completing request", error);
return new Response(
JSON.stringify({
error: "Unable to fetch the subscriptions data.",
}),
{
headers: { "Content-Type": "application/json" },
status: 400,
}
);
}
}
This code retrieves the customer's subscriptions using the provided customer ID and returns the result of this operation. In case an error is encountered, the code returns a response with the error message.
Canceling a Subscription and Requesting a Full Refund
To allow users to cancel a subscription and request a full refund, create the new file cancelandrefund/route.js in the app/api/subscriptions
folder and paste in the code below:
import { makeRequest } from "@/utils/makeRequest";
export async function POST(req, res) {
const { subscription_id } = await req.json();
// Retrieve the subscription from Rapyd
try {
const result = await makeRequest(
"GET",
`/v1/payments/subscriptions/${subscription_id}`
);
// Retrieve the subscription creation date
const subscription_creation_date = result.body.data.created_at;
// Check if the customer is eligible for a refund
let epoch_seconds = Math.floor(new Date().getTime() / 1000);
let days_since_creation =
(epoch_seconds - subscription_creation_date) / (24 * 60 * 60);
// For users who are not eligible for a refund,
// the subscription will be canceled at the end of the billing cycle
if (days_since_creation > 7) {
try {
const result = await makeRequest(
"DELETE",
`/v1/payments/subscriptions/${subscription_id}`,
{
cancel_at_period_end: true,
}
);
return new Response(
JSON.stringify({
message: "Subscription canceled successfully",
}),
{
headers: { "Content-Type": "application/json" },
status: 200,
}
);
} catch (error) {
console.error("Error completing request", error);
return new Response(
JSON.stringify({
error: "Unable to cancel subscription",
}),
{
headers: { "Content-Type": "application/json" },
status: 400,
}
);
}
}
// For users who are eligible for a refund,
// the subscription will be canceled immediately
// and a refund will be issued.
else {
// Cancel subscription
try {
const result = await makeRequest(
"DELETE",
`/v1/payments/subscriptions/${subscription_id}`
);
// Retrieve the payment associated with the subscription
try {
const result = await makeRequest(
"GET",
`/v1/payments?subscription=${subscription_id}`
);
let subscription_payment_id = result.body.data[0].id;
// Create a refund
try {
const body = {
payment: subscription_payment_id,
reason: "Subscription canceled within allowed timeframe.",
};
const result = await makeRequest(
"POST",
"/v1/refunds",
body
);
return new Response(
JSON.stringify({
message:
"Subscription canceled and refund issued successfully!",
}),
{
headers: { "Content-Type": "application/json" },
status: 200,
}
);
} catch (error) {
console.error("Error completing request", error);
}
} catch (error) {
console.error("Error completing request", error);
}
} catch (error) {
console.error("Error completing request", error);
}
}
} catch (error) {
console.error("Error completing request", error);
return new Response(
JSON.stringify({
error: "Unable to retrieve the subscription.",
}),
{
headers: { "Content-Type": "application/json" },
status: 400,
}
);
}
}
The code extracts the subscription_id
from the request body and uses it to fetch subscription details from the Rapyd API. It gets the creation date and stores it in the variable subscription_creation_date
. The code then calculates the days since creation and checks eligibility for a refund. If more than seven days have passed, the user is not eligible for a refund and the subscription is set to cancel at the billing cycle's end. For eligible customers, it immediately cancels the subscription, retrieves the payment associated with the subscription, extracts the payment ID associated with the subscription, and initiates a refund using the payment ID. In this code snippet, the reason for canceling is hard-coded, but in a real-world application, providing a form that the users can fill out with their reason could help improve your service to address the customer's concerns.
Implementing the Dashboard Logic
You'll now implement the UI to allow your customers to interact with this system.
To do this, you will update the code in the components/Dashboard.jsx file so that you can render the appropriate information. To render the 3DS authentication page, you will use an iframe to embed the page into our site. The user will only be able to access their subscriptions dashboard once they have authenticated their card. Once the user performs 3DS authentication, the page reloads. You will keep track of the page reload and use it to check whether the card has been authenticated.
Open the components/Dashboard.jsx file and replace the existing code with the following:
"use client";
import React, { useEffect, useState } from "react";
function Dashboard({ checkUserInfo, cardAuthLink, setCardAuthLink }) {
const [user, setUser] = useState(null);
const [subscriptions, setSubscriptions] = useState(null);
// Retrieve user details from local storage
const retrieveUser = () => {
const userInfo = JSON.parse(localStorage.getItem("user_info"));
return setUser(userInfo);
};
// Set user on initial page load
useEffect(() => {
retrieveUser();
}, []);
// Log out the user
const logout = () => {
localStorage.removeItem("user_info");
checkUserInfo();
};
// Retrieve subscriptions on page load
const retrieveSubscriptions = async () => {
try {
const res = await fetch("/api/subscriptions/list", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ customer_id: user.rapyd_cust_id }),
});
const data = await res.json();
if (data.error) {
alert(data.error);
}
if (data.subscriptions) {
setSubscriptions(data.subscriptions);
}
} catch (error) {
console.error(error);
}
};
// Retrieve the user's subscriptions on page load
useEffect(() => {
if (user) {
retrieveSubscriptions();
}
}, [user]);
// Create subscription
const createSubscription = async () => {
try {
const res = await fetch("/api/subscriptions/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ customer_id: user.rapyd_cust_id }),
});
const data = await res.json();
if (data.error) {
alert(data.error);
}
if (data.message) {
// Set the subscriptions to null to trigger loading state
setSubscriptions(null);
// Retrieve the subscriptions
retrieveSubscriptions();
}
} catch (error) {
console.error(error);
}
};
// Cancel subscription and request refund
const cancelAndRefund = async () => {
try {
const res = await fetch("/api/subscriptions/cancelandrefund", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
subscription_id: subscriptions[0].id,
}),
});
const data = await res.json();
if (data.error) {
alert(data.error);
}
if (data.message) {
// Show the message
alert(data.message);
// Set the subscriptions to null to trigger loading state
setSubscriptions(null);
// Retrieve the subscriptions
retrieveSubscriptions();
}
} catch (error) {
console.error(error);
}
};
// Track whether the iframe has reloaded
const [iframeReloaded, setIframeReloaded] = useState(false);
// Track if you are checking the customer info
const [checkingCustomerInfo, setCheckingCustomerInfo] = useState(false)
// Track if 3DS auth is required
const [nextAction, setNextAction] = useState('3d_verification')
// Check customer info -> confirm if the card has been authenticated
const checkCustomerInfo = async () => {
try {
// Trigger the "Verifying your payment information..." message
setCheckingCustomerInfo(true)
// Retrieve the customer info, specifically the payment method's next_action, using their Rapyd customer ID
const res = await fetch("/api/auth/retrievecustomer", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
customer_id: user.rapyd_cust_id
}),
});
const data = await res.json()
// Hide the "Verifying your payment information..." message
setCheckingCustomerInfo(false)
// "not_applicable" means that the card has been authenticated successfully
if(data.message === "not_applicable"){
// Update the 3DS Auth link to null and next_action to not_applicable
// This will display the dashboard
setCardAuthLink(null)
setNextAction(data.message)
}
} catch (error) {
console.error(error)
}
}
// Handle iframe reload
const handleIframeReload = async () => {
if (iframeReloaded) {
// Check if the card has been authenticated
checkCustomerInfo()
} else {
// If this is the first load, set the flag for subsequent reload detections
setIframeReloaded(true);
}
};
// In Rapyd, when you create a customer and immediately try to retrieve their info,
// the next_action changes to "not_applicable" even if 3DS auth hasn't been performed.
// To avoid this, we will only check the customer info when the user logs in and avoid this when the user signs up
// To know when a user signs in, the card auth link will be null
// You can run the checkUserInfo function only when the cardAuthLink variable is null
useEffect(() => {
if(user && cardAuthLink === null){
checkCustomerInfo()
}
}, [user, cardAuthLink])
return (
<div className="bg-white p-5 rounded shadow max-w-[600px] w-full mx-auto mt-8">
{
cardAuthLink || nextAction === '3d_verification' ?
<div>
<p className={checkingCustomerInfo ? "text-2xl font-bold" : "hidden"}>Verifying your payment information...</p>
<iframe
src={cardAuthLink}
width="600"
height="400"
onLoad={handleIframeReload}
hidden={checkingCustomerInfo}
></iframe>
</div> :
<div>
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold">Subscription Dashboard</h2>
<button
onClick={() => logout()}
className="border px-4 py-1 cursor-pointer"
>
Log out
</button>
</div>
{subscriptions === null ? (
<p className="mb-5">Loading...</p>
) : (
<div className="mb-5">
{subscriptions.length === 0 ? (
<div>
<p>You do not have an active subscription.</p>
<div className="mt-4">
<p>
Subscribe to{" "}
<strong>
CineView Unlimited - Weekly Plan
</strong>{" "}
at $1.99
</p>
<button
onClick={() => createSubscription()}
className="mt-4 bg-green-600 text-white rounded cursor-pointer px-6 py-2"
>
Subscribe Now
</button>
</div>
</div>
) : (
<div>
<h3>Your Subscription</h3>
<p>
<strong>Plan:</strong>{" "}
{
subscriptions[0].subscription_items.data[0]
.plan.nickname
}
</p>
<p>
<strong>Status:</strong>{" "}
{subscriptions[0].status
.charAt(0)
.toUpperCase() +
subscriptions[0].status.slice(1)}
</p>
<p>
<strong>Billed:</strong> $
{
subscriptions[0].subscription_items.data[0]
.plan.amount
}
/{subscriptions[0].subscription_items.data[0].plan.interval}
</p>
{subscriptions[0].status === "active" && (
<button
onClick={() => cancelAndRefund()}
className="mt-4 bg-red-600 text-white rounded cursor-pointer px-6 py-2"
>
Cancel Subscription
</button>
)}
</div>
)}
</div>
)}
</div>
}
</div>
);
}
export default Dashboard;
This component has several functions:
-
retrieveUser
: Retrieves user details from local storage and sets theuser
state -
logout
: Clears user information from local storage and triggers a function (checkUserInfo
) to handle user logout -
retrieveSubscriptions
: Retrieves user subscriptions by making a POST request to/api/subscriptions/list
and sets thesubscriptions
state -
createSubscription
: Creates a new subscription by making a POST request to/api/subscriptions/create
and triggers a reload to fetch the new subscriptions -
cancelAndRefund
: Cancels a subscription and requests a refund by making a POST request to/api/subscriptions/cancelandrefund
and triggers a reload of user subscriptions -
checkCustomerInfo
: Checks customer information, specifically whether the card has been authenticated, by making a POST request to/api/auth/retrievecustomer
; updates the state based on the card authentication status -
handleIframeReload
: Handles iframe reload events and checks customer information
The code conditionally renders different content based on the state of the cardAuthLink
or if the next action is 3d_verification
, which means that the card has not been authenticated. It displays a loading message or an iframe for card authentication, if applicable. If the card is authenticated, the code displays the subscription dashboard.
Testing the Application
To test whether the application is working as expected, you need to delete the existing SQLite database as you have introduced new fields in the code. If your development server is already running, stop it using Ctrl+C or Cmd+C. Delete the database.db file in the db
folder and run the development server once again using the command npm run dev
.
To test the code, navigate to http://localhost:3000/
on your browser. As you already logged in earlier, the dashboard page will be displayed. The starter template uses values stored in the browser's local storage to check if a user is authenticated. Click the Log out button to clear the data in local storage, and you'll be redirected to the login page.
Select Sign Up from the form switcher at the top of the page. The sign-up form has already been prefilled with some data. The password is 12345678
. Select the Sign Up button at the bottom of the page to create an account with the provided data.
After a short period of time, the Rapyd 3DS Simulator page will be displayed.
Fill out 123456
in the input box and select Continue, and the page will display a message indicating that the details are being authenticated. In the background, the app is making a request to the Rapyd API to check if your card has been authenticated.
After the card has been authenticated, you will see the subscription dashboard indicating that you do not have an active subscription.
Click the Subscribe Now button to subscribe to the "CineView Unlimited - Weekly Plan". The page will show the loading message and then display your current subscription.
To cancel the subscription and request a refund, click the Cancel Subscription button. The page will show an alert indicating that the subscription has been canceled and the refund issued successfully.
Click the OK button on the alert, and the status of the subscription will be indicated as "Canceled". This means that the subscription has been canceled by the customer but remains in the Rapyd database.
You can head over to the refunds page on the Rapyd Client dashboard to confirm that the refund was issued successfully.
This confirms that the application is working as expected. You can find the full code for this guide on GitHub.
Top comments (0)