Implementing Safe Payment Logic for Subscription Plans

Hyunji Kim·2025년 3월 9일

LocalSteps

목록 보기
2/3

In the Local Steps project, there are two subscription plans that users can select and purchase. As a full-stack developer, I aimed to implement this process securely and smoothly.

If we ignore security considerations, we could simply store the price of the selected plan on the frontend (e.g., using localStorage). However, this approach is highly insecure because users can easily modify the price through the browser's developer mode. To ensure security, I researched a safer method: sending the ID of the selected plan from the frontend to the backend, where prices and other critical information are managed securely.

To clarify why I didn't integrate external APIs, such as Stripe, which would typically be used in professional environments: this project is developed specifically for a school assignment. Therefore, the subscription plans aren't actually processed, and all payment transactions are simulated and automatically marked as failed.

Here’s how I implemented this approach:

The logic is straightforward. When a user selects a plan, the frontend sends the planId to the backend. The backend then verifies whether this planId exists in the database. If the validation succeeds, it retrieves and returns the corresponding price securely to the frontend.

// front/subscription.js
// send the plan id to backend 
  const handleUpgrade = async (subscriptionId) => {
    try {
      localStorage.setItem('planId', subscriptionId);
      window.location.href = '/settings/subscription/payment/';
    } catch (error) {
      console.error('Error fetching subscription price:', error);
    }
  };

/* ... card list of plans.. **/ 

This is a snippet of code from the Plan Page, where users can select one of the available subscription plans. The handleUpgrade function is an event handler triggered when the user clicks a button to purchase a specific plan. Each button is associated with a corresponding planId. Before redirecting the user to the payment page, this planId is stored in localStorage.

// backend/subscription.js
const Subscription = require ('./Subscription');

router.get('/plans/:id', async (req,res) => {
  
  const { id } = req.params;
  
  try {
    const subscription = await Subscription.findById(id).select('price');
    if (!subscription) {
      return res.status(404).json({ message: 'Subscription not found' });
    }
    return res.status(200).json({ price: subscription.price });
  }
  catch (error) {
    console.error('Error fetching subscription price:', error);
    return res.status(500).json({ message: 'Internal server error' });
  }

})

Now, let's move on to the backend implementation. We'll create a fetch route that accepts a planId and returns the corresponding subscription plan price.


//front/utils/api.js
export const fetchPlanPrice = async (planId) => {
  try {
    const response = await fetch(`backend_API_URL/plan/${planId}`);
    const data = await response.json();
   	return data;
  } catch (error) {
    console.error('Error fetching subscription price:', error);
    throw error;
  }
};

I prefer to keep the API calls in a separate file on the frontend. Therefore, on the frontend side, I have code structured specifically to fetch the appropriate data from the backend.

// front/payment.js
import React, { useState, useEffect } from 'react';

import { fetchPlanPrice } from './utils/api';

export const Payment = () => {
  // set amount
  const [amount, setAmount] = useState(0);
  
  // fetch the price of the selected plan
  useEffect(() => {
   // fetch the price of the plan
        const fetchPlanPriceData = async () => {
          // fetch plan id from local storage
          const planId = localStorage.getItem('planId');
          const response = await fetchPlanPrice(planId);
          setAmount(response.price);
        };

        fetchPlanPriceData();
  }, []);

};

Returning to the frontend, on the Payment Page, we now need to use the API we created in the backend. Since the payment amount is retrieved using the fetchPlanPrice API, you can directly use {amount} to obtain the price corresponding to the selected plan.

This represents a basic implementation. However, in real-world applications, you would need a more secure approach. Fetching sensitive data, such as pricing information, directly from the client-side can be risky. Therefore, all sensitive data—especially payment amounts—should be validated and managed securely on the server side.

profile
My personal Web Dev Notebook.

0개의 댓글