Now let's go ahead and create some internal actions, sorry, internal mutations which we are going to need which are then subsequently going to be called in our webhook. Let's go inside of convex and let's create a new file called subscriptions.ts. In here, let's go ahead and let's import v from convex values and then let's import internal mutation from .slash generated server like this. Let's first create our internal create mutation. So export const create is going to be our internal mutation.
The arguments it's going to accept are the organization ID, which is a string, the Stripe price ID, which is a string, Stripe customer ID, which is also going to be a string, Stripe subscription ID, which is a string as well, and Stripe current period end, which is a number or a date in our case. Then let's add a handler, which is going to be an asynchronous method, which accepts the context, and it has the arguments here. And from the arguments we can already extract the organization id, stripe price id, stripe customer id, Stripe subscription ID and Stripe current period end. So all of those from above. And simply we're going to return await context database, insert organization subscription, and we're going to pass in all of those.
So double check, stripe subscription ID, stripe customer ID, stripe subscription ID and stripe current period. And so none of these should yield any errors because this should exactly match what we have written inside of our schema for org subscriptions. Inside of convex folder we have the schema and you should have these exact values which you are passing here and receiving through the arguments. Great! And now let's create an update mutation.
So export const update is going to be our internal mutation here which accepts the following arguments. Stripe subscription ID, which is a string, stripe current period end, which is a number, and it has a handler, which is an asynchronous method, which accepts the context, the arguments, which we are going to destructure. The arguments are stripe subscription ID and stripe current period end. So we are using this stripe subscription ID to call this index by subscription so we can find an existing subscription and we are simply going to update the current period and so this method will be called when a user has upgraded or renewed the subscription. So let's open a try and catch here and let's resolve the catch.
Console error, the error and return success false here. And in here let's go ahead and write const existing subscription is going to be await context.database.query organization subscription with index by subscription we're going to get the query and then let's do query.equals StripeSubscriptionId to StripeSubscriptionId from the arguments like this. And now let's go ahead and simply add this .unique at the end so we only get one unique version of that. If there is no existing subscription we are going to throw a new error subscription not found. Otherwise we can go ahead and do await context database patch existing subscription underscore ID And then we are simply gonna append the new stripe current period end.
Like that. And make sure the return success to be true. Great. So now that we have two of our internal mutations, we can safely go back inside, well, let's check out HTTP here. We don't need anything here, we can just go inside of Stripe here and work inside of the fulfill internal action.
So let's go ahead and do the following. Inside of checkout session completed we have to create a subscription for the first time. So first we have to do that using the Stripe util. So subscription is going to be await stripe.subscriptions.retrieve and pass in session.subscription as string. And then in here we're going to check if we are missing session?metadata?organizationid.
In that case we're going to throw new error, no organization ID. Like that. So if you remember inside of our Stripe payment here I told you that it's very important to add the metadata inside of the sessions create right here. So when the webhook comes back this is how we are going to know for what organization that payment was for. And then what we can do is await context run mutation, and then we can call internal from .slash generated API.
So let me show you, make sure you import internal from .slash generated API. We're going to call internal.subscriptions.create. And then we can pass the organization ID to be session metadata.organizationID as a string. Then we can pass Stripe subscription ID to be subscription.ID sorry, this subscription I misspelled this, subscription subscription.ID as string Stripe customer ID is going to be subscription.Customer as string. And we're going to have stripePriceId to be subscription.items.data first in the array, dot price, dot id and we're going to pass in stripeCurrentPeriodEnd to be subscription, so this is in one line, I will just collapse it so you can see here subscription dot current underscored period underscore and times a thousand like this and let's see if we need all of this as Strings here.
Okay, this one needs it How about this one? This one doesn't need it and this one doesn't need it as well. Okay if you're having errors you can write as string. Let me actually write it as string just so we have the same code. Like that.
Great! And that should actually create our internal subscription. So let's actually go ahead and try this out. So I want you to prepare your database here, specifically your organization subscriptions, which you should now have, right? Because you modified the schema.
So it should just be an empty in here. Make sure that you have your webhook running of course. So inside of your terminal you should have the webhook running which is npm run stripe listen command. So let's go ahead and refresh here and let's attempt to purchase this. So once I upgrade here, I'm gonna go ahead here and I will click pay and subscribe and let's see if that will successfully insert so it looks like all 200 events are here everything looks okay in the terminal but let's confirm it here and there we go we have our organization subscription and you can see that it ends a month from now perfect so today is 29th and it ends a month from now perfect and you can also see all the information like the price ID, Stripe subscription ID, customer ID, all of those information.
Excellent! So we are successfully now on Pro mode. But let's just go ahead and wrap up our webhook now because another event is going to fire after a month passes. So after a month passes Stripe is going to attempt to renew this subscription and that's not going to be the checkout session completed event. Instead it's going to be a different event.
So let's go ahead and write if event.type is invoice.payment underscore succeeded. Let's go ahead and get our subscription again using await stripe.subscriptions.retrieve and pass in the session.subscription as string and then we are very simply going to call our other mutation Wait, context, runMutation, internal.subscriptions.update and simply passing stripeSubscriptionId to be subscription.id as string and stripeCurrentPeriodEnd to be subscription let me just collapse this but it is in one line of course subscription dot current period and times a thousand So the same as we did right here, but in here we need less information. Perfect! And make sure you return success true. Amazing!
And now what I want to do is I want to go back inside of my subscriptions right here and I want to create a couple of useful utils here. So export const getIsSubscribed getIsSubscribed or you can call it isPro or hasOrganizationSubscription. Basically I want to check using this query that we have a valid and active subscription which has not expired. So let's do a query which you can import from convex generated server and the arguments for this are simply going to be the organization ID which is going to be optional and the string. Let's add a handler, Let's make it an asynchronous method with context and arguments.
And let's write if there are no arguments.organizationId return false. So if we forgot to pass the organization id we are simply going to return false. This organization is not subscribed. And then let's write const organization subscription to be await context.database.query. Let's call our organization subscription.
Let's do with index here. Let's do by organization. Let's get our query and do query equals organization ID and pass in arguments organization ID as a string like that and let's simply add .unique and let's do const period end to be our organization subscription question mark dot stripe current period end and then we're gonna do const is subscribed period end and period end is larger than date now And simply return is subscribed. Great! So we can now use this query to check for any organization we need whether they have an active subscription in the database which has not expired.
Now let's go ahead and actually use this query. So I want to go inside of my components, sorry app folder dashboard components here and we should have an organization sidebar here. So go inside of here and let's go ahead and let's add our use query from convex react and let's also add our API from convex generated API and let me just reorder this there we go so make sure you added use query and the API from convex generated API. And then in here, we're gonna do const isSubscribe is gonna be our use query. But before we do that, let's also do const organization from useOrganization from ClerkNext.js.
So just make sure you've added use organization from clerk nextjs here and then we are very simply gonna call our API subscriptions.getIsSubscribed and passing the organization ID to be our organization?id like this. And then let's go ahead and do the following. So I want to go inside of my terminal and I want to add a chatCN component called badge. So ntx chatCN UI at latest add badge. So it's in one line like this add badge.
Let's wait a second for this to install. Great! And now let's go ahead just next to our span which sells the name of your app here. Go ahead and do the following. If is subscribed go ahead and render a badge component from components UI badge and simply say pro inside and give it a variant of secondary.
So make sure you've imported the badge from components UI badge and let's see if we can already see this. There we go! You can see that I am in pro here but if I go ahead and switch to another organization in here I don't have that badge. Only if I go to a previous one I have that badge. If I try a new board here I need to upgrade.
Of course yes we also need to allow us to now create new boards. We forgot to do that of course. But yeah now you see you have this is subscribed hook and you can now easily always check on the client side what you want to forbid users from doing. So what I also want to do now is I want to allow user to cancel their subscription. So in order to do that we have to go and create another internal subscription query here.
Let's go ahead and go inside of our convex inside of subscriptions here And above get is subscribed. Let's go ahead and create export const get and that's going to be internal query from convex generated. So just add it to this list of array of above. This internal query is going to have arguments which accept organization ID to be a string. And we're going to have a handler, which is an asynchronous method, which very simply gets the context and the arguments and in here all we're gonna do is return await context.database.query organization subscription.withindex by organization get the query and simply call query equals organization ID and compare it with arguments organization ID and let's get the unique one like this very simple internal query.
And now let's go back inside of stripe.ts and we're gonna create a new action similar to our pay action called portal. So this is also gonna be an action. The arguments are simply gonna be for what organization ID do we want to get the portal? And then add a handler here to be an asynchronous method with context and the arguments. Let's get the identity to be await context out and let's do get user identity.
If there is no identity we are going to throw new error unauthorized and further down let's go ahead and check if there are no arguments organization ID in that case throw new error no organization ID and no organization ID. And then let's find our org subscription using await context run query internal.subscriptions.get and pass in the org id to be arguments.orgid like this, so let's collapse this so you can see better like that, then we're gonna check if there is no organization subscription we cannot open up a portal so just throw new error no subscription and then let's finally do const session to be await stripe billing portal dot sessions dot create and passing the customer to be organization subscription dot Stripe customer ID and passing the return URL to be the URL. We have the URL already defined right here. It is our next public app URL. All right, this is the pay action and this is our action which we are working with.
And all we have to do is return session.url and put an exclamation point at the end. Great, so now we can go back inside of our organization sidebar here. And in here we can now render a new button here at the bottom after our favorite boards. Let's go ahead and do the following. Let's add a button here and let's check if we are subscribed we're gonna say payment settings otherwise we're gonna write upgrade.
So we want to also allow user to upgrade on their own without hitting that model that needs to open, right? So in here, let's just do bank note from Lucid React, like that, make sure you import bank note from Lucid React And add a class name here to be H4 and with 4 and margin right of 2. And give this a variant of Ghost, a size of large and a class name of very simply font normal, justify start, px2 and full width. And now if you expand, there we go. In the one where you are subscribed you're gonna see payment settings but if you switch to another one is going to say upgrade.
So now let's go ahead and add our portal internal action so we can call this out so we're gonna go ahead and create an onClick method here. It's gonna be an asynchronous method which is also gonna have its pending state. So let's define pending and set pending here to be used state from React. And let's make it false by default. And let me just move this here.
So we're gonna do set pending to be true. And then we're gonna open a try and finally set pending is gonna be false and in the try we're gonna call our portal to be import use action from convex react and make sure you've added, or we already have the API from convex generated API. All right. So then in here, first let's prevent this if we don't have the organization ID. So if we don't have organization.id, simply break this onClick method, because we are not going to be able to neither show the portal or initiate a payment.
So then we're going to do const action to check if we are subscribed, open the portal, otherwise pay. And then const redirect URL is very simply going to come from our await action and passing the organization ID because they have the same prop. Both portal and pay accept organization.id and then do window.location.href to simply be the redirect URL. If you want to you can also add a catch method here in between and do a toast from sonar error something went wrong for example. I imported the toast from sonar If you want to you can add that.
And now that we have this on click method let's go ahead and pass it to the on click here and let's add disabled here to be pending. Like that. There we go. So let me just go ahead and refresh this. And now if I click on payment settings here you can see how it's loading and we get an error that something is wrong and this is because we need to enable this in test environment so let me just see if we can see that error here no we cannot see it here but we can see it in convex So go inside of your convex and inside of logs here you're going to see an error.
There we go. You can't create a portal, sorry, you can't create a portal session in test mode until you save your customer portal settings right here. So dashboardstripe.com slash test settings billing portal. So I'm simply going to go ahead and add that here. And that's going to open up the portal.
And all I have to do is click activate test link. There we go. So let me go ahead and click on payment settings again and this time I will be redirected to my portal page from where the user can successfully cancel their subscription. But here's what I want you to understand about cancelling your subscription. I often get comments in my videos that I didn't do the case for cancelling because they are still in pro mode, right?
And they can still only access this. Well, that's because just because a user canceled doesn't mean they still don't have an active subscription. So this subscription is still active for a month. The only way to test for you locally to change that is to remove that from your organization subscription here. So when you remove this document only then will this become...
You can see how we no longer has the pro badge. Right? Great! So we handled that and let's also confirm that now when I click upgrade here that will open up a checkout portal Perfect. So one more thing that we have to do The main thing actually is allow you to create more boards if you have a subscription, which is very simple So let's revisit our board.ts inside of convex board right here and in here we just throw an error if we hit the limit right so what we're gonna do now is we're also gonna check if we have a subscription so let's do const organization subscription so you have many ways to do this now You can use await context.db.
Which one do we have? We have query. Okay, let's just do query. Very simply, We are going to query by organization subscriptions. We are going to do an index by organization.
And we're very simply going to check if the organization ID matches the arguments organization ID. And let's get the unique one. So the unique is important because otherwise it's an array. This way it's a single object, Right? Let's extract the period end and let's do org subscription question mark stripe current period end.
And then let's do const is subscribed to be period end. And period end is a larger than date dot now. You can of course encapsulate this if you feel like it's repeating too much code, right? It makes sense to do that. And then we are simply going to do if we are not subscribed and if we reached the amount of our boards.
So let me show you that like this. So only if we are not subscribed, only then are we gonna follow the rules for the organization limit. So let's try it out. Right now if I try I need to upgrade. So if I go ahead and upgrade.
Whoops. Let's just wait a second and see this being processed. Of course, make sure you have your Stripe webhook running so you don't run into any issues here. There we go. I'm now on pro and if I try again, there we go.
I can now finally create boards. And you can use this logic to do whatever you want. If you want to cancel or disable some tools, if user is not a pro, you can do that, right? You have the query. All you have to do is go inside of our, what is it, what's the name of the component toolbar.
So in here you would simply fetch that is subscribed and you would give this a disabled button, right? If they're not subscribed, as simple as that. So go ahead and be creative, do whatever you want with this great, great, amazing, amazing job. And thank you so much for purchasing additional content.