This is the third part of the Build a Paid Membership Site with Magic and Stripe series. Make sure to read the following before proceeding:
- Quickstart to set up Stripe and Magic.
- How the React client side is built.
Server
Now that we understand how the Client side is built and how it works, let's learn about our Server side code (located in server.js
).
Here are the major functions our server needs to work seamlessly with the Client:
- Validate the Auth Token (
didToken
) returned by Magic'sloginWithMagicLink()
. - Create a Stripe
PaymentIntent
to keep track of the Customer's payment lifecycle. - Create a Stripe Customer so that we can tie the Customer to a matching
PaymentIntent
and keep track of whether or not the Customer has successfully paid. - Validate that the user is indeed a Customer who has lifetime access to your Premium Content.
Validate the Auth Token (didToken)
As mentioned, when the user clicks the email link to log in, we send the didToken
to a server endpoint called /login
in order to validate it. It's best practice to validate the DID Token
before continuing to avoid invalid or malformed tokens.
We verify the didToken
with Magic's validate
method. If the didToken
is indeed valid, we then send a 200
status code back to the client.
/* File: server.js */
// Import, then initiate Magic instance for server-side methods
const { Magic } = require("@magic-sdk/admin");
const magic = new Magic(process.env.MAGIC_SECRET_KEY);
// Route to validate the user's DID token
app.post("/login", async (req, res) => {
try {
const didToken = req.headers.authorization.substr(7);
await magic.token.validate(didToken);
res.status(200).json({ authenticated: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Create a Stripe Payment Intent and Stripe Customer
Once a user decides to buy a lifetime access pass to our Premium Content, we consider them a customer. In order to keep track of the customer's payment cycle, we'll need to add a new server endpoint called /create-payment-intent
in server.js
that:
- Creates a customer with their email address.
- And then creates a
PaymentIntent
that is linked to this customer.
The PaymentIntent
will keep track of any failed payment attempts and ensures that the customer is only charged once.
/* File: server.js */
// Import & initiate Stripe instance
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
// Add the user to your list of customers
// Then create a PaymentIntent to track the customer's payment cycle
app.post("/create-payment-intent", async (req, res) => {
const { email } = req.body;
const paymentIntent = await stripe.customers
.create({
email,
})
.then((customer) =>
stripe.paymentIntents
.create({
amount: 50000, // Replace this constant with the price of your service
currency: "usd",
customer: customer.id,
})
.catch((error) => console.log("error: ", error))
);
res.send({
clientSecret: paymentIntent.client_secret,
customer: paymentIntent.customer,
});
});
As you can see, we'll be charging our customers $500 for a lifetime access pass to our awesome Premium Content. 😎
Update the Stripe Customer's Info
As soon as the payment goes through, the Client side will send a request to /update-customer
to update the Stripe Customer's information with a metadata
of { lifetimeAccess: true }
. Since we have a special use case; charging customers a one time fee, setting this metadata will help us validate whether or not the customer has paid.
/* File: server.js */
// Update the customer's info to reflect that they've
// paid for lifetime access to your Premium Content
app.post("/update-customer", async (req, res) => {
const { customerID } = req.body;
const customer = await stripe.customers.update(customerID, {
metadata: { lifetimeAccess: true },
});
res.send({
customer,
});
});
Validate a Paid Customer
Now that the user has successfully paid, they should be able to access the Premium Content page. To check whether or not the user is authorized, we'll be using the /validate-customer
endpoint. It expects the user's email address, and returns a list of customers who has that email.
Ideally, your customer should know to only buy their lifetime access once. This way, the list that stripe.customers.list()
returns will always have the single customer who paid.
However, accidents do happen. 🤷🏻♀️
To prevent users from purchasing a lifetime access twice, I suggest adding some logic to your SignUp
component that checks whether or not the user who's trying to sign up is already a Stripe Customer with lifetime access. If they are, send them to the Premium Content page. Otherwise, they can continue to the Payment page.
/* File: server.js */
// Collect the customer's information to help validate
// that they've paid for lifetime access
app.post("/validate-customer", async (req, res) => {
const { email } = req.body;
const customer = await stripe.customers.list({
limit: 1,
email,
});
res.send({
customer: customer.data,
});
});
Test the integration
Alright, now that we know how the paid membership app works on both the Client and Server side, let's give it a test run! Here are a few UX flows I suggest testing out:
- Head to the Premium Content page to sign up and pay for lifetime access.
- Log in as a paid Customer and try to access the Premium Content page.
- Using a different email, log in as an unpaid Customer and try to access the Premium Content page.
Btw, you can make your payments with this test card number: 4242 4242 4242 4242
What's next
YAY! You now have a paid membership site. Read our final article of this series to learn how to deploy your app to Heroku.
Top comments (0)