Last updated on November 08, 2024
How to Integrate Stripe Payments in Meteor 3
Over the last couple of years, online payments have become an essential part of any online business. 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 front-end framework.
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 modes. Both are created in the same way, but 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):
{
"public": {
"stripe_publishable_key": "<publishable_key>"
},
"stripe_secret_key": "<secret_key>"
}
In Meteor, the settings.json
file manages our project's environment variables. This file stores information like API keys, passwords, and application configuration data. 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:
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 to initialize Stripe using our Publishable API key. We will add <PaymentForm />
inside this component, 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:
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 handleChange = (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={handleChange} />
<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 who submits a payment, we added an input field to collect the user's email address. We also 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 take two actions: create and confirm a payment intent.
A payment intent is an object that represents the intent to collect a payment from a customer. It contains information about the transaction, such as 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:
import { Meteor } from 'meteor/meteor';
const stripe = require('stripe')(Meteor.settings.stripe_secret_key);
Meteor.methods({
'payment.intent': async function () {
const paymentIntent = 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:
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 handleChange = (event) => {
setForm({ ...form, email: event.target.value })
};
const handleSubmit = async (event) => {
event.preventDefault();
setForm({ ...form, loading: true });
try {
const intent = await Meteor.callAsync('payment.intent');
const payload = await stripe.confirmCardPayment(intent, {
payment_method: {
card: elements.getElement(CardNumberElement),
billing_details: { email: form.email },
}
});
if (payload.error) return setForm({ ...form, error: payload.error.message, loading: false });
return setForm({ ...form, loading: false });
} catch (error) {
if (error) return setForm({ ...form, error: error.reason, loading: false });
}
};
return (
<form onSubmit={handleSubmit}>
<CardNumberElement />
<CardCvcElement />
<CardExpiryElement />
<input type="email" name="email" value={form.email} onChange={handleChange} />
<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's secret value, we will call 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 the useElements
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 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:
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.