DEV Community

Cover image for Swiping with Stripe: Simple and Secure Online Payment Handling Powered By Stripe
Joshua Mayhew
Joshua Mayhew

Posted on

Swiping with Stripe: Simple and Secure Online Payment Handling Powered By Stripe

Application frameworks offer developers at least one feasible avenue of safeguarding users and maintaining the integrity of their data in a digital landscape increasingly fraught with malicious actors and data breaches. Powerful and robust built-in functionalities, like the bcrypt gem in Rails, are an effective deterrent against hacks, ensuring that user account credentials are rigorously encrypted and computationally expensive to sift through.

And, while this is definitely a worthwhile line of defense, it should not and cannot be the only measure of security that a developer relies on in protecting their applications' data. Simply, a reduction in risk is not an elimination of risk. Therefore, it is deeply important to consider more broadly the implications and justifications of storing and associating sensitive user data on a database.

Where and how data is stored is an integral part of protecting it against attack. Why concentrate sensitive data in a single location when it is unnecessary to do so? Instead, sensitive data can be further dispersed and diversified across multiple secure locations, from where it can be subsequently queried and referenced by an application only when necessary and essential to do so. This "offshoring" of important data creates additional complexity and ensures that a breach in one location does not invariably translate to a breach of all data.

An obvious and very prevalent example of this delegation of data storage and security can be seen in the use of online payment processing platforms like Stripe. Serving, perhaps arguably, as the most reputable and foremost purveyor of online financial services, Stripe offers developers a very well-established and credible place to safely offload the risk and responsibility of managing and protecting what may be the most sensitive and frequently targeted types of data: user payment information.

Of course, any application that prompts a user for their payment details must grapple with the inherent risks entailed by creating, saving, and processing this information. In my own application, Infinite Eats, I immediately saw the glaring risk that housing this type of data poses to both myself and the user. Professionally and legally, the fallout of a data breach could be catastrophic. Hence, I found myself looking at Stripe as a well-trusted means of handling user payment data for me.

Notably, Stripe offers developers extraordinary degrees of access to its API, which is both highly functional and extremely well documented. The intuitive ease of working with and learning how to effectively integrate and leverage Stripe's API infrastructure in my project was a massive benefit to me. By delegating the overall task of payment processing to Stripe, developers are able to both simplify the task of handling user payments and focus their attention to the core functionality of their application.

Specifically, Stripe streamlines payment processing via its expansive suite of client libraries and SDKs (Software Development Kits), which are basically pre-built components that developers can use and plug into their applications without having to code everything on their own. Aside from providing developers with these useful building blocks, Stripe also gives applications the benefit of scalability, handling high volumes of transactions without compromising performance or security.

So, knowing all of this, and having chosen to integrate aspects of the Stripe API into my own project in fulfillment of my Phase 5 capstone at Flatiron School, how did I actually utilize the Stripe API?

Project Walkthrough

The following is a very basic example implementation of Stripe in a full stack React/Rails project. This walkthrough will cover the creation of payment methods and their being saved by Stripe for future reference.

  • 1. Stripe API setup and customer creation

Before making any requests to Stripe or importing any of their SDKs, you must first create an account with Stripe and access your API keys from the developer dashboard. The API keys serve as the authentication mechanism by which the application can communicate with the Stripe servers. There is a public and private key. Importantly, I saved my keys in two different .env files (the public being saved in a .env file located in my client directory and both API keys also being stored in a .env file located in my Rails app root directory) and excluded these files from version control, to prevent uninvited access to this data.

Secondly, payment methods can only be saved and associated with users who also have stripe customer accounts, so I decided to automatically enroll all Infinite Eats users with Stripe to ensure that their payment information would be correctly registered and associated with a unique customer user id. To do this, I added some extra Stripe specific functionality to the Create Action in my Users Controller:


  def create
    new_user = User.create!(user_params)

    # Set your secret key. Remember to switch to your live secret key in production!
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = Rails.application.credentials.dig(:stripe, :secret_key)

    # Creates a new Stripe customer on signup
    customer = Stripe::Customer.create({ email: new_user.email })

    # Update the user with their new Stripe customer ID
    new_user.update!(stripe_customer_id: customer.id)

    session[:user_id] = new_user.id
    render json: new_user, status: :created
  end
Enter fullscreen mode Exit fullscreen mode

Thus, new users will be simultaneously created in my Rails db following signup and registered as customers via the Stripe specific syntax that I found in Stripe's official documentation. Importantly, the user should have their stripe_customer_id attribute updated in the Rails db before rendering the user object as json for frontend access.

  • 2. Integration

Next, I installed the @stripe/react-stripe-js library via npm. This library is what grants developers access to the pre-built React component and hooks that streamline user payment handling on the frontend. I relied on the Elements component, which is a container for Stripe components - the pre-built UI components that ensure the secure collection of user payment details.

In order to import and utilize Elements, I had to wrap it around any parts of my application that would be engaged in payment method collection. This meant wrapping any necessary context files in my index.js file..

(showing relevant portions only)


import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";


 <Elements stripe={stripePromise}>
                <PaymentProvider>
                  <CheckoutProvider>
                    <FridgeProvider>
                      <App />
                    </FridgeProvider>
                  </CheckoutProvider>
                </PaymentProvider>
              </Elements>
Enter fullscreen mode Exit fullscreen mode

as well as wrapping my primary payment method form component in App.js..

import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";

<Route
            exact
            path="/myaccount"
            element={
              user ? (
                <Elements stripe={stripePromise}>
                  <PaymentProvider>
                    <AsyncMyAccountPage />
                  </PaymentProvider>
                </Elements>
              ) : (
                <Navigate to="/home" replace />
              )
            }
          />

Enter fullscreen mode Exit fullscreen mode

As you'll likely see, I am passing props to Elements in the form of a promise, which is where the public API key comes into play. I declared this promise twice in both index.js and App.js as follows:

const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLIC_KEY || "");

Doing so provides access to the CardElement component, which comprises a pre-built credit card input field. This component is essential for streamlining and safeguarding payment method collection on the frontend, as it securely captures sensitive user payment details from the frontend while also providing a smooth and professional user experience.

I chose to render this CardElement component within the larger context of a component I named PaymentMethodForm.js. To service the creation and saving of user payment methods to Stripe, I had to rely on some added functionality that I coded both in a payment.js context file and called in my PaymentMethodForm.js component's handleSubmit function. I chose to target payment method creation and saving as two separate async functions

  • 3. Method Creation:
const createPaymentMethod = (cardElement) => {
    if (!stripe || !elements) return Promise.reject("Stripe is not available");

    setLoading(true);

    return stripe
      .createPaymentMethod({
        type: "card",
        card: cardElement,
      })
      .then((result) => {
        setLoading(false);

        if (result.error) {
          throw result.error;
        }

        if (!result.paymentMethod) {
          throw new Error("No paymentMethod returned from Stripe");
        }

        return result.paymentMethod;
      })
      .catch((error) => {
        setLoading(false);
        throw error;
      });
  };

Enter fullscreen mode Exit fullscreen mode

In createPaymentMethod, I am creating a payment method object via the Stripe API. The very first thing is that this function is accepting a cardElement param, which represents all the card information entered by the user. Secondly, having already nested my relevant payment method components in the Stripe Elements container, I am given access to new Stripe provided 'stripe' and 'elements' objects. If either of these are missing, the function will return a rejected promise from Stripe. This check is important in that it confirms that the Stripe API and Elements components are properly loaded and accessible before proceeding to payment method creation.

Next, the function calls stipe.createPaymentMethod, which passes an object with payment method necessarily listed as 'card,' and the cardElement param. This method being called is what triggers the Stripe API to create a payment method based on the given user payment details represented in cardElement. The outcome of this method is a promise, which will either resolve successfully or unsuccessfully. If unsuccessful, the resulting object will be an error message, but if everything is correct, it will return a paymentMethod object that can then be saved via the following function.

  • 4. Assigning and Saving the Payment Method:

  const handleSavePaymentMethod = async (paymentMethod) => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch("api/users/save_payment_method", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ pm_id: paymentMethod.id }),
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message);
      }
      setUser({ ...user, payment_method_id: paymentMethod.id });
      setLoading(false);
    } catch (error) {
      setLoading(false);
      throw error;
    }
  };

Enter fullscreen mode Exit fullscreen mode

Taking that successfully returned paymentMethod obj, I am able to subsequently pass it to the handleSavePaymentMethod() as an argument and pass its id in the body of my POST request to a custom save_payment_method endpoint in my Users controller. This controller action appears thusly:


  def save_payment_method
    pm_id = params.require(:pm_id)
    customer_id = @current_user.stripe_customer_id

    Stripe.api_key = Rails.application.credentials.dig(:stripe, :secret_key)

    # Attach the PaymentMethod to the customer
    Stripe::PaymentMethod.attach(
      pm_id,
      { customer: customer_id }
    )

    # Set it as the default payment method
    Stripe::Customer.update(
      customer_id,
      { invoice_settings: { default_payment_method: pm_id } }
    )

    @current_user.update(payment_method_id: pm_id)

    render json: @current_user, status: :ok
  end

Enter fullscreen mode Exit fullscreen mode

Here, I assign the pm_id variable to value of the passed paymentMethod.id params. I also make sure to access the current user's (who is authorized via other backend logic beforehand) stripe customer id. Again, the Stripe secret API is necessary and should be accessed from the Rails .env file in order to authorize the requests to the Stripe API that set and update the payment method. The following lines are what communicate with Stripe. Stripe::PaymentMethod.attach is self-explanatory; it attaches the payment method to the customer and identifies each by their respective ids.

*This is the beautiful delegation of payment security and management facilitated by Stripe -- Stripe queries customers and their associated payment methods by their provided ids and thereby frees the Rails db from having to store any of the actual payment details themselves. Of course, API keys could be leaked, etc etc, but being able to offload and delegate secure data storage to Stripe while only needing to store relevant corresponding ids in the application db greatly improves the overall security of an application and of sensitive user data. *

Next, the Stripe::Customer.update is responsible for designating the provided payment method as the default payment method (which is necessary to facilitate more than a single transaction). Finally, the current user so that its payment_method_id is assigned and thus associates the user with the saved payment method. The current user is then returned as a json object where the handleSavePaymentMethod() can conditionally handle errors or update the user state on the frontend so that the newly saved payment method id value is accessible in user state.

As demonstrated above, integrating the services of third-party payment platforms like Stripe into our applications is an effective and worthwhile approach to achieving secure and efficient handling of sensitive user data. Stripe's API tools significantly simplified the process of payment handling in my project, while simultaneously ensuring that my user data and payment handling is scalable, efficient, and secure. And, just as important, Stripe integration allowed me to focus my attention on the fundamental concerns and objectives of my application without suffering the distractions of securing private data. Ultimately, as the digital landscape continues to evolve, and new challenges continue to emerge, it is all the more imperative that we likewise evolve and adapt, learning to effectively utilize whichever available tools and services, such as the Stripe API, that allow us to ensure the highest possible industry standards of data security and user experience in our applications.

  1. Image credit: https://news.crunchbase.com/startups/fintech-startups-look-to-kill-swipe-fees-on-behalf-of-small-business-owners/

Top comments (0)