Last updated on August 01, 2023

How to Integrate Stripe Payments in Meteor

Online payments have become one of the essential parts of any online business over the last couple of years. Because of this, there has been a big thrive of tools/platforms to build payment solutions for web and mobile applications. Through all of these alternatives, Stripe is one of the most popular and widely adapted payment systems by developers and companies.

Stripe offers a simple solution for building one-time payments, subscriptions, software platforms, marketplaces, and more. It provides out-of-the-box features like fraud detection, invoice handling, card data encryption, and financial reporting.

One of its core features is its easy-to-use API for developers. It delivers prebuilt UI components for creating a fully customizable checkout experience allowing developers to add custom styling and logic to the payment flow.

In this tutorial, we will learn how to build a simple payment system with Stripe and how to integrate it with Meteor and React.

Meteor is a full-stack JavaScript platform for developing modern web and mobile applications. Some of its most remarkable features are its built-in integration with MongoDB, its unified client and server development, and its flexibility in using front-end frameworks like React, Vue, Angular, and Blaze.

Create Meteor Project

Let's start by creating our Meteor project with the command meteor create <project_name>. This command will build a simple boilerplate of a Meteor application and use React as its frontend framework.

By default, Meteor adds two packages to our project: insecure (allows database updates directly from the client) and autopublish (serves all the database data to the client).

These packages should be used for development purposes and removed before deploying our application into production. We can run the command meteor remove insecure autopublish to remove them.

Stripe Account Setup

Next, let's set up our Stripe account. We will go to Stripe's Registration page and complete the sign-up process. Once our account is complete, we will configure our business by adding our payment details and other elements like business name, address, type of business, website, and more (you can find more details of the required information here).

To set up Stripe in our project, we need to create our business API keys which are the Publishable and Secret keys. The first key identifies our account with Stripe (client-side), and the second authenticates our requests to Stripe's API (server-side).

To create our API keys, we will go to the Developers section of our Stripe dashboard and click on the API keys option. Here we will find our Publishable and Secret keys, which should have the following format:

publishable_key: pk_live_EsAYEXtdvyMjUKgmxCCkUdoH
secret_key: sk_live_dN2tLQtSC2DahYx8cZrg2rbJ

Every Stripe account has a live and test mode. This feature makes the development process much easier because we can test our payment system without affecting our business data. To switch between both, we need to click the Test mode toggle button from our dashboard page.

Note that we have different API keys for our test and live mode. Both of them are created in the same way, and to differentiate them, live mode keys begin with pk_live_ and sk_live_, and test mode keys begin with pk_test_ and sk_test_.

Now, let's set up our API keys in our Meteor project by adding them to our settings.json file (if this file doesn't exist, you can create it under the root folder of your application):

settings.json
{
  "public": {
    "stripe_publishable_key": "<publishable_key>"
  },
  "stripe_secret_key": "<secret_key>"
}

In Meteor, the settings.json file is the one that manages the environment variables of our project. This file stores information like API keys, passwords, and configuration data of our application. To access this information, we can use the Meteor.settings object (e.g. Meteor.settings.stripe_secret_key).

By default, all the values stored in our settings.json file are private (accessible from the server). If we want to make some of these values public (accessible from the client), we need to add a new object definition called public.

To initialize our project with our settings.json file, we need to run the command meteor --settings settings.json (we can also update the start script command inside our package.json file, so we only have to run npm start to initialize our application).

Install & Configure Stripe

Now that our Meteor application is ready, let's add Stripe to our project. For this, we will run the command npm install --save stripe @stripe/stripe-js @stripe/react-stripe-js. The purpose of these packages is:

  • stripe: Provide access to the Stripe API (server-side).
  • @stripe/stripe-js: Install Stripe utilities to communicate with Stripe API (client-side).
  • @stripe/react-stripe-js: React components for Stripe's prebuilt UI Elements.

Once we have installed our packages, we need to initialize Stripe in our project. To do this, we will create a new file inside the imports/ui folder called Payment.jsx and add the following code:

imports/ui/Payment.jsx
import React from 'react';
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
 
const stripePromise = loadStripe(Meteor.settings.public.stripe_publishable_key);
 
export function Payment() {
  return (
    <Elements stripe={stripePromise}>
      <PaymentForm />
    </Elements>
  );
};

Here, we added the Elements component, which provides all its children access to the Stripe context. This component requires a stripe argument which will initialize Stripe using our Publishable API key. Inside this component, we will add <PaymentForm />, containing our checkout form functionality. This element will handle the payment flow and send all the required information to Stripe.

Stripe Payment Form

Let's define our PaymentForm component. For this, we will update our Payment.jsx file with the following code:

imports/ui/Payment.jsx
import React, { useState } from 'react';
import { Elements, CardNumberElement, CardCvcElement, CardExpiryElement } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
 
const stripePromise = loadStripe(Meteor.settings.public.stripe_publishable_key);
 
export function Payment() {
  return (
    <Elements stripe={stripePromise}>
      <PaymentForm />
    </Elements>
  );
};
 
function PaymentForm() {
  const [form, setForm] = useState({ email: '' });
 
  const handleEmailChange = (event) => {
    setForm({ ...form, email: event.target.value })
  };
 
  const handleSubmit = (event) => {
    event.preventDefault();
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <CardNumberElement />
      <CardCvcElement />
      <CardExpiryElement />
      <input type="email" name="email" value={form.email} onChange={handleEmailChange} />
      <button>Submit</button>
    </form>
  );
};

We defined a simple HTML form inside our new function with three Stripe elements: CardNumberElement, CardCvcElement, and CardExpiryElement. These components are part of Stripe's React library, which allows us to collect the required information to process a payment (card number, card's CVC code, and card's expiration date).

To identify the user that submits a payment, we added an input field to collect the user's email address. Also, we added a handleSubmit function that will send the payment information to Stripe and complete the purchase process. In the next step, we will build this functionality.

Submit Stripe Payment

To complete our checkout flow and submit the payment to Stripe, we must go through two different actions: create and confirm a payment intent.

A payment intent is an object that represents the intent to collect a payment from a customer. This object contains information about the transaction, like the amount to be charged, currency, and payment method.

We will use the stripe.paymentIntents.create method to create a payment intent. Since this functionality must be executed from the server, we will initialize Stripe with our Secret API key and generate a Meteor method called payment.intent inside our server/main.js file. Let's update our file with the following code:

server/main.js
import { Meteor } from 'meteor/meteor';
 
const stripe = require('stripe')(Meteor.settings.stripe_secret_key);
 
Meteor.methods({
  'payment.intent' () {
    const paymentIntent = Promise.await(stripe.paymentIntents.create({
      amount: 5000, // amount in cents
      currency: 'usd'
    }));
 
    return paymentIntent.client_secret;
  },
});

Now that we have created our payment intent, we need to confirm it. To do this, we will use the stripe.confirmCardPayment function, which requires the client secret value returned from our payment.intent Meteor method. Let's update our imports/ui/Payment.jsx file with the following code:

imports/ui/Payment.jsx
import React, { useState } from 'react';
import { Elements, CardNumberElement, CardCvcElement, CardExpiryElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
 
const stripePromise = loadStripe(Meteor.settings.public.stripe_publishable_key);
 
export function Payment() {
  return (
    <Elements stripe={stripePromise}>
      <PaymentForm />
    </Elements>
  );
};
 
function PaymentForm() {
  const [form, setForm] = useState({ email: '', error: '', loading: false });
  const stripe = useStripe();
  const elements = useElements();
 
  const handleEmailChange = (event) => {
    setForm({ ...form, email: event.target.value })
  };
 
  const handleSubmit = (event) => {
    event.preventDefault();
    setForm({ ...form, loading: true });
 
    Meteor.call('payment.intent', (error, client_secret) => {
      if (error) return setForm({ ...form, loading: false, error: error.reason });
      return completePayment(client_secret);
    });
  };
 
  const completePayment = async (intent) => {
    const payload = await stripe.confirmCardPayment(intent, {
      payment_method: {
        card: elements.getElement(CardNumberElement),
        billing_details: { email: form.email }
      }
    });
 
    if (payload.error) return setForm({ ...form, loading: false, error: payload.error.message });
    return setForm({ ...form, loading: false });
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <CardNumberElement />
      <CardCvcElement />
      <CardExpiryElement />
      <input type="email" name="email" value={form.email} onChange={handleEmailChange} />
      <button disabled={form.loading}>Submit</button>
      {form.error && <p>Error: {form.error}</p>}
    </form>
  );
};

Here, we updated our handleSubmit function by calling our payment.intent Meteor method. Once we have the client secret value, we will call the completePayment function, which will use stripe.confirmCardPayment to confirm the payment intent.

To use stripe for the confirmCardPayment function, we need access to Stripe's context (initialized with the Elements component). To do this, we used the useStripe hook provided by Stripe's React library.

The confirmCardPayment function requires a payment_method parameter containing the card number and billing details, like the user's email address. To get the card number, we used theuseElements hook to gain access to the input value of the CardNumberElement React component.

Last, we added a loading and error state to improve the user experience of our checkout form. These will be used to disable the form submission while processing the payment and display any errors that may occur during this process.

To complete our payment system, we must import our Payment component inside our imports/ui/App.jsx file. For this, we will update our file with the following code:

imports/ui/App.jsx
import React from 'react';
import { Hello } from './Hello.jsx';
import { Info } from './Info.jsx';
import { Payment } from './Payment.jsx';
 
export const App = () => (
  <div>
    <h1>Welcome to Meteor!</h1>
    <Hello/>
    <Info/>
    <Payment/>
  </div>
);

Conclusion

We have completed our custom payment solution for Meteor using Stripe and its developer-centric tools for React. Many other features can be developed into this functionality (e.g. coupon codes, subscriptions), but this is a good starting point from which you can customize to your own business needs.