Ok so users can upgrade to a Premium account using Stripe Subscriptions. But what if they want to cancel their subscription and downgrade to a Basic account?
This is how you cancel a subscription in Stripe:
stripe.subscriptions.del( 'sub_F4Pm39d98MmCJG', function(err, confirmation) { // asynchronously called } );
So we need a Subscription ID. Where do we get this?
Well, we get this returned to us when we create a subscription. So when we create a subscription, we can store the subscriptionId
as a field on the User
in our database.
This will allow us to find the user’s subscription later, if they want to cancel their subscription.
To do this let’s first clean up our subscription resolver logic. The current resolver updateRole(newRole: Role!, stripeToken: String): User!
has a lot of logic inside of it. Probably too much. It’s handling both creating and canceling subscriptions, along with other auth logic.
We can clean this up by separating the upgrade and downgrade (a.k.a. subscribe/unsubscribe) logic into two separate resolvers:
upgradeToPremium(stripeToken: String): User! downgradeToBasic: User!
This lets us keep each one focused.
Each resolver needs to do a few key things to make the full lifecycle of subscribe-to-unsubscribe possible:
upgradeToPremium(stripeToken: String): User!
Create a Customer in Stripe
Subscribe the Customer to a Plan (Subscription)
Update the user in the database
Set the role to
PREMIUM
Save the
subscriptionId
so we can use it later
downgradeToBasic: User!
Grab the
subscriptionId
from the database for our logged in userCancel the Subscription in Stripe using the
subscriptionId
Update the user in the database
Set the role to
BASIC
Set the
subscriptionId
tonull
With this logic, we can both create new subscriptions for our user, as well as cancel an existing one.
Implementation
upgradeToPremium
resolver:
{ | |
upgradeToPremium: async (parent, { stripeToken }, { models, me, res }) => { | |
if (!me) throw new Error("Please log in first."); | |
if (me.role === "PREMIUM") | |
throw new Error("You already have a Premium account. Enjoy!"); | |
try { | |
const stripeCustomer = await stripe.customers.create({ | |
description: `Customer for ${me.email}`, | |
email: me.email, | |
source: stripeToken | |
}); | |
const stripeSubscription = await stripe.subscriptions.create({ | |
customer: stripeCustomer.id, | |
items: [{ plan: "plan_FLyf9o01u2bJZU" }] | |
}); | |
// Update the DB user role | |
const result = await models.User.update( | |
{ | |
// what to update: | |
role: "PREMIUM", | |
subscriptionId: stripeSubscription.id | |
}, | |
{ | |
// where to update | |
where: { id: me.id }, | |
// options | |
returning: true, // return data after db update | |
plain: true // don't return extra stuff | |
} | |
); | |
// result is an array with [affectedRows, data]. We want the data (user); | |
const updatedUser = result[1]; // return this at the end | |
// reset the cookie, now with jwt containing updated user role | |
const token = await createToken(updatedUser, process.env.APP_SECRET); | |
res.cookie("token", token, { | |
httpOnly: true, | |
maxAge: 1000 * 60 * 60 * 24 * 365 // 1 year cookie | |
}); | |
// return user | |
return updatedUser; | |
} catch (error) { | |
throw new Error(error); | |
} | |
}, | |
} |
downgradeToBasic
resolver:
{ | |
downgradeToBasic: async (parent, args, { models, me, res }) => { | |
if (!me) throw new Error("Please log in first."); | |
if (me.role === "BASIC") { | |
throw new Error( | |
"You already have a Basic account. Upgrade to Premium to get the full experience." | |
); | |
} | |
try { | |
const user = await models.User.findByPk(me.id); | |
const confirmation = await stripe.subscriptions.del( | |
user.subscriptionId | |
); | |
const result = await models.User.update( | |
{ | |
// what to update: | |
role: "BASIC", | |
subscriptionId: null | |
}, | |
{ | |
// where to update | |
where: { id: me.id }, | |
// options | |
returning: true, // return data after db update | |
plain: true // don't return extra stuff | |
} | |
); | |
// result is an array with [affectedRows, data]. We want the data (user); | |
const updatedUser = result[1]; // return this at the end | |
return updatedUser; | |
} catch (error) { | |
throw new Error(error); | |
} | |
} | |
}, | |
} |