DEV Community

Cover image for Creating and Paying a Freight Invoice with the Rapyd API and FX
Drew Harris for Rapyd

Posted on • Originally published at community.rapyd.net

Creating and Paying a Freight Invoice with the Rapyd API and FX

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:

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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:

  1. Create a service and a plan.
  2. Create a customer with a payment method and simulate 3DS authentication.
  3. Create a subscription for a customer.
  4. 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:

Importing the Rapyd Postman collection

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:

Rapyd refunds environment

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. When a user clicks the Sign Up button on your site, you will attempt to create a customer on Rapyd.
  2. 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.
  3. 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>
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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
   )`
);
Enter fullscreen mode Exit fullscreen mode

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,
   }
);
Enter fullscreen mode Exit fullscreen mode

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,
    }
   );
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In the same file, make sure you are passing the setCardAuthLink function to the Signup component.

<Signup checkUserInfo={checkUserInfo} setCardAuthLink={setCardAuthLink} />
Enter fullscreen mode Exit fullscreen mode

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 }) {
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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,
           }
       );
     }
}
Enter fullscreen mode Exit fullscreen mode

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
   })
Enter fullscreen mode Exit fullscreen mode

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
   })
Enter fullscreen mode Exit fullscreen mode

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,
        }
    );
   }
}
Enter fullscreen mode Exit fullscreen mode

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,
        }
    );
   }
}
Enter fullscreen mode Exit fullscreen mode

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,
        }
    );
   }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

This component has several functions:

  • retrieveUser: Retrieves user details from local storage and sets the user 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 the subscriptions 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.

Simulating 3DS authentication

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.

Loading message

After the card has been authenticated, you will see the subscription dashboard indicating that you do not have an active subscription.

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

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.

Subscription canceled and refund issued

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.

Canceled subscription

You can head over to the refunds page on the Rapyd Client dashboard to confirm that the refund was issued successfully.

Rapyd client dashabord

This confirms that the application is working as expected. You can find the full code for this guide on GitHub.

Top comments (0)