Now let's go ahead and let's actually set up Stripe. So go ahead and visit stripe.com and create an account. Once you've created an account, you're going to have to create your first store. So what I'm going to do, since I already have a store as you can see, I don't know what you will see but basically you need to find a way to create a new account. So I can click on the upper left corner and I can select create new account and inside of here I'm going to go ahead let me just do this again because by zooming in I close the model so I'm gonna call this image AI and I'm gonna click create and once my store has been created I will get redirected to that new store dashboard and now this is what you should see in front of you And what I recommend you do is you click on the developers right here in the upper tab and inside of here click on API keys and then click reveal test key and then you can click on it to copy it.
So secret key from your newly created account from Stripe is what we need to get. Once you have that, go inside of .environment.local here and add that here. So I'm gonna call this StripeSecretKey and paste the key inside. Now let's go ahead and let's add the Stripe library. So bun add stripe.
Let's do bun run dev. Now let's create the Stripe util. So inside of source, let's go ahead inside of the lib folder and create stripe.ts. Now we're going to import stripe from the new package which we've just installed and then we're going to export our own instance of stripe which will be new stripe and inside of here for the first argument we're going to add our environment key So I recommend that you copy directly from here so you don't misspell it. Like this.
And you can put an exclamation point at the end here. And then we're gonna have to pass some additional instructions here. First of all, the API version. So you can use the version that I'm using, but if you're using TypeScript, you will get auto fill here for the version that is currently on. So just click on whatever pops up here and then for TypeScript, select true as well.
There we go. So now you have the Stripe package. So what we have to do next is that once... Let me just refresh my localhost here because I've shut down the app. What we have to do next is create a mutation so that when a user clicks on the upgrade button, they get redirected to the checkout screen.
So let's do that now. Let's head back inside of our HONO routes. So, source app API route. And let's go ahead and let's create subscriptions.ts let's just not misspell subscriptions like this let's import Hono from Hono let's define the app to be new Hono and let's export default the app Now let's go back inside of route.ds and we can chain slash subscriptions to subscriptions. We of course have to add an import for subscriptions So let's do that as well.
Import subscriptions from subscriptions. There we go. Now let's go ahead inside of the subscriptions and let's add a .post method. Slash checkout. So slash checkout needs to have a verified user.
So we're gonna have to import verify out from HonoAuth.js and then let's go ahead and create an asynchronous controller method inside. So inside of here first let's get the auth using cgetAuthUser then if there is no auth token ID we're gonna return back c.json error unauthorized and a 401 status code. And now let's go ahead and let's actually create the checkout session. So for that, we're gonna have to import our Stripe from here, from libstripe. So let's go ahead and write const session, await, Stripe, checkout, sessions, create.
And then we're gonna go ahead and define the success URL to simply be for now, HTTP localhost 3000 slash, Well, nothing but we're gonna add success to be one like this and For the cancel URL We can add cancel Canceled one like this which will represent which represents like this is true this query and now let's add the payment method types so inside of here you add anything you want to support You can see you have a bunch of options. Alipay, Affirm, all of these things. You have PayPal, you have Klarna. So whatever you want, you can go ahead and add here. What I'm going to select is Card and PayPal.
So I'm just going to keep it simple. The mode for this will be subscription like this. The billing address collection will be auto like that. And then we need the customer email. So the customer email we are going to get from auth token dot email or an empty string if we don't have it like this and now what we have to do is we have to create a product in Stripe so that we can add the Stripe price ID to our environment local and also so that we can add it as a line item here.
So let's go ahead and let's go back inside of Stripe. And now what we have to do is we have to go ahead and create a product. So let's go ahead and see how we can do that. I'm going to click on the product catalog here. I'm going to click add a product.
I'm going to make sure to select the recurring option and I'm going to call this monthly subscription. Like this. And I'm going to select this to be for example 20 euros or US dollars whatever you want to be a part of and let's go ahead and click add product here like this and now you should have monthly subscription here And what I want you to find is the price ID. So click on pricing here and somewhere in here you should find the price ID. There we go.
It's right here at the top. I think there's also an alternative way you can copy that by clicking copy price ID here. So you can do that as well. Great. Now let's go ahead back inside of our .environment.local here and let's add alongside the Stripe secret here key, let's add Stripe price ID and let's paste it here like this.
There we go. Now you can go back inside of the session creation here and we can add line items, which is an array. And we're gonna have just one element with a price, which will be our process.environment.stripe.price.id. So just make sure that you don't misspell stripe price ID here and here and make sure you've copied it correctly and let's add the quantity to be 1 And here's an important part we need to add the metadata. So in order for the webhook later to recognize which user upgraded we need to now secure that.
So we're gonna use the out token ID as the user ID here. So outToken.id. So later in the webhook that's how we are going to recognize for which user is the webhook coming from. Because webhook is not something that will be called by us. Webhook is something that Stripe will call on our API endpoint and will give us the information about the metadata and that's how we're going to recognize which user the payment went through.
And now let's go ahead and get the URL which will be session.url once session is created. If we don't have the URL we can only return back an error that something went wrong. So I'm going to add an error, failed to create session. And a 400 error. Unless that happens, we can go ahead and return data URL.
There we go. So this is our checkout method here. So now what we have to do is we have to create a mutation for that. So just ensure that you save your .environment.local here. And let's go inside of source.
Let's go inside of features. And let's copy something like use create project, for example, and paste that inside of, we're gonna create a new folder inside of subscriptions called API. So you can paste the use create project inside and rename the file to use checkout. Well, we can call it use checkout. Let's do that.
So let's rename this to use checkout, Like this. And now let's change the response type. So the response type here will very simply be API subscriptions and then checkout. And it's gonna be a post method and we're looking for the 200 and same thing is here so API subscriptions checkout post method but we are not looking for JSON here and we actually don't need the request type at all like this and so you can remove the request type here. Great.
And let's change this now. So this will be subscriptions, checkout. And inside of the post here, well, you can leave the JSON technically, but you can also not because we are not passing anything. Right. So instead of here, you can either say something went wrong or fail to create a session.
And on success here, well, we're not gonna do any of these. We can go ahead and say something failed to create session here But what we're gonna do in the on success is a redirect so we're gonna use window location oops Dot href will be the following our data And our data should be a string representing the URL like this. So let's go ahead and remove the trailing comma here. We can remove the use query client. We can remove the infer request type.
We can remove the use query client, we can remove the infer request type, we can remove the use query client. And two things I wanted to check before we do this. So I want to go back inside of my app folder API route subscriptions. So inside of here we kind of hard-coded this success URL and the cancel URLs because I assume that this is your URL but this is not how you would actually do it in production so in production this is what you would do and what we're gonna do now inside of your environment local you will add the... Oh, I think we already have it.
Right? We already have the next public app URL. Excellent. Yes, we have it because in our Hono library we are using it right here. So ensure that you have the next public app URL here and that's what we're going to use.
So go ahead and change this double quotes to be backticks and then you can replace this part of the URL with process.environment next public app URL like this. So I'm just going to copy this and I'm going to paste it for the cancel URL and I'm gonna change this to be cancelled at 1. There we go. Now that we have this working let's go ahead and let's add the use checkout method to the subscription model. So let me close everything here.
Let's go inside of source, features, subscriptions, components, subscription model. And now I'm gonna go ahead and I'm going to add use checkout from API use checkout which I will prefix with features subscriptions like this and let's go ahead and add the mutation use checkout and now we're gonna go ahead and actually use that here so this button right now will call the mutation mutate And disabled will be if mutation is pending. Like this. So let's try it out. I'm gonna go ahead here, I'm gonna refresh my page.
I'm gonna find a way to open the premium model. And once I click upgrade, Let's see if I will get redirected to Stripe checkout page and I am and you can see it recognized the $20 and the name monthly subscription, which is exactly what is in my product catalog here. So just make sure that you copy the correct price ID here either by clicking here and copying it from here or by clicking on this drop down here from here. And let me show you where the products are located right here in case you can't find them. Great!
So this works But now if you fill this in, you will get an active subscription on Stripe, but nothing will change inside of here. And here's one thing I want you to notice. Once I clicked back, my URL now has the canceled one. So we can use that to display feedback to the user like payment didn't go through or you didn't finish your payment. We're gonna also do that.
But what I want to do instead now is I want to go ahead and actually add the database schema needed to create a subscription inside of our project. Let's go back inside of our database schema file and we're going to introduce a new table called subscriptions. So inside of the database schema.ts, let's go ahead and let's export const subscriptions which will be a pgtable subscription to stay consistent as the ones above and Now let's go ahead and define the fields it's going to have. The ID will be text, ID, primary key, and then we're going to add a default function here which we'll call crypto which will generate UUID. And let's just ensure that this is an actual function like this.
Besides the ID we're gonna have the user ID. The user ID will be a text user ID. My apologies, user ID like this in camel case. And user ID will be not null meaning it's required and it's going to have a foreign key relation to users ID. And now we're gonna add a rule set here on delete cascade.
So if the user gets deleted, the subscription will get deleted as well. And the subscription ID here will be the Stripe subscription ID, Right? So let's go ahead and add a text here, subscription ID. And let's just mark it as required. Then below that we're going to have a customer ID.
Which is also going to be required. Then let's go ahead and let's add the price ID. Then we can also add the status. So basically you can choose what you want to add here, right? Besides the status we're gonna have a current period and this will be a timestamp and Current period and and we're going to go ahead and modify the timestamp to have a mode of date.
Besides the current period and we're going to have created at, which will be a timestamp again, created at with the mode date and it's also going to be required. And we're going to have the updated date. So just ensure that all of your fields have different names inside of here. Make sure the names don't repeat because it's going to be a conflict. So this is just for JavaScript but this is what actually gets written in the database here.
Great. So we now have this. And now let's go ahead and let's actually add this to our database. So I'm going to go ahead inside of the terminal here and I'm going to run bun run database generate. This will add a new migration file.
And then bun run database migrate. This will now push it to our remote database. Great! Let's run bun run dev again and you can now take a look inside of your Drizzle migration here we added the subscription table with ID, user ID, subscription ID, customer ID, price ID, status, current period and created and updated ad. And we also created a foreign key policy on user ID and subscription here.
Excellent. So now that we have this, let's go ahead and the next step is to add a webhook, right? So there is a certain way we can add a webhook and for that we have to go inside of our Stripe here, inside of developers, inside of webhooks and you have to select test in a local environment right here. And the first thing you have to do is download the CLI. So you can find the instructions on getting started with Stripe CLI.
You have instructions for Homebrew, for Linux, for Windows, for Docker, basically you need to have the Stripe CLI added inside of your machine. Once you have that you can follow the instructions on how to test out Stripe webhooks locally. So the first step is to do Stripe login. Let's go ahead and do that together. I'm gonna go ahead and set the terminal and you can shut down your app and simply do Stripe login.
And Now you can press enter to open the browser or click on this link. And now inside of here you're going to see this code. So I have this code right here. This is my project and you can also see that this is my pairing code here. So the code matches.
So this is only for you to ensure that you're connecting to a machine you know. So click allow access. There we go. You can now close this window and you can go back inside of here. As you can see now this says completed.
So what we have to do next is we have to forward the Stripe CLI to listen to a specific endpoint in our app. So let's go ahead and do that. So we can now close this, clear this. And we can add Stripe Listen forward to localhost 3000 slash API slash subscriptions slash webhook. So just don't misspell this.
Subscriptions localhost 3000 API webhook like this. And press enter like this. And this will give you your webhook sign-in secret so copy everything that's bolded like this and once you have your secret here go immediately inside of .environment.local and add the StripeWebhookSecret and paste it here. And now you should also have the completed for step 2 and you can now play around and you can try for example step 3. Now we're going to get 404s if we try this.
So you have to have this open. Do not close this. Let's go ahead and open a new terminal and let's attempt to run the stripe trigger command. So right now if you go back inside of here, there we go. You get a bunch of failed connections here.
So there's multiple reasons for that. The first reason is because localhost 3000 is not running. So let's go ahead and do bun run dev and now it should be running and you can open a third terminal to try this again. And this time you should get back 404s because our API subscriptions webhook does not exist but that should be enough for this to trigger a completed event. Perfect!
So we now have local webhooks set up. So what you can do is you can remove one of your terminals but you need to have your BannerOnDev running and you need to have your StripeListen forward to running. So our next step is to create this endpoint. API subscriptions webhook. That is our next step.
Let's go ahead and do that now. So we're gonna head back inside of source app folder API route subscriptions right here and we're gonna go ahead and chain host webhook here. So that's where it's going to go. And this one will be a little bit different. So the first thing you're going to notice is that this one is not going to have the verify auth, right?
Because this will not be accessed by users. This will be accessed by Stripe. So we only care about the controller inside of this first part. And how are we gonna know that it's Stripe who is trying to access the webhook? How can we know this will not be hijacked by some bad user?
Well that's because we're gonna go ahead and construct the event using our Stripe webhook secret. So we're gonna get the signature. So let's go ahead and first turn the body into text. So C.requestText like this. Then let's get the signature.
We can get that from the header so C.requestHeader stripe-signature like this and that's gonna be a string Let's define the event to be a type of Stripe.event. And we have to import the Stripe from the package Stripe itself, like this. And then open a try and catch method here. In the try method, go ahead and attempt to construct the event using all of this info that we have. So, StripeWebhooks.constructEvent() in the first argument passing the body, in the second argument the signature, and in the third argument process.environment() and then you can simply copy the Stripe webhook here which you've named here like this and put an exclamation point at the end.
So just make sure you don't misspell this. So using this we're going to construct the event and in case we catch an error here we can return and Immediately break this method. We're simply gonna say error invalid signature This means that whoever tried to access this is not Stripe and they are not gonna have access of firing any events on Stripe's behalf. So that's how we're gonna handle authentication for our webhook. If this passes this means that the Stripe is the one trying to access our server.
So inside of here, we can get this session by using event data object, and we can define it as stripe.checkout.session. So now we're gonna have some proper types here. So in order to create the first subscription, the event we're gonna be looking for is if session.type, my apologies, we need to get event.type. So the event is what we constructed from here so if event.type is checkout.sessionCompleted this means that the user has for the first time purchased a subscription so what we're gonna do is we're first going to retrieve the subscription from Stripe itself so await stripe and let's just confirm that you have the stripe util here so you need it here. So we're gonna await stripe and we're gonna call retrieve.
Where is my stripe dot retrieve? My apologies. Stripe subscriptions retrieve and inside of here simply passing session.subscription as string. Like this. So in this subscription constant we now have the subscription which is already created on Stripe site.
On the Stripe platform the subscription already exists. So now we're gonna extract the info from that subscription. And the first thing we have to check is if we have the metadata. So if we don't have session?metadata?userid. If we don't have that, there's nothing we can do further because we have no idea for which user is this being processed.
So let's add an error, invalid session and 400. So that's why I emphasized how important it is that in the checkout method here you assign the metadata user auth token ID because later when it receives the webhook we don't have the verify auth. There is no way for us to know for which user this is except if the session has it inside of the metadata user ID. So that's why we need this check here. And if that's okay, let's do await database, which you can import from database drizzle and let's also prepare subscriptions like this make sure you have those, okay we're gonna do the following await database insert into subscriptions the following values status will be subscription.status So we are now reading the stuff from this constant, like that.
User ID will be session, metadata, user ID. Make sure you don't misspell the user ID here or there. And let's also now add subscription ID which will be subscription.id itself. We are then going to have the customer ID which is subscription.customerID. Be careful, don't use subscriptions.
So this is now invalid. It should be subscription.ID. Right? This one. This is the constant where you are reading things from.
And let's define the customer ID as string. And it's actually not a customer ID, it is just customer. So let's be careful here because we can easily mess things up. Now let's add the price ID, so that's gonna be subscription.items.data first in the array.price.product as string. Now let's go ahead and let's add the current period end.
This one is very important. This one will be responsible for detecting whether the subscription is active, inactive, expired, canceled. So let's add new date here. Inside of the new date let's add subscription.current.period.end and let's multiply it by a thousand like this. So we normalize the time here.
And now let's add created at to be new date and let's add updated at to be new date. And there we go. Now the values should be completely fine. So do you really need the status? Do you need the subscription ID and the customer ID?
Well, the more info you have, the better experience you can provide to your users on your platform yourself, right? But the only ones that are absolutely required are the user ID, because they are a foreign key, so you know for which user this subscription is, subscription ID because using the subscription ID you can call this method here and using that you can go ahead and always check it right from the source which is Stripe. The customer ID is not required. The price ID is also not required. But the current period end definitely is.
So yes, if you want to clean it up, if you want the most minimal example, You don't need the status, you don't need the customer ID and you don't need the price ID. So just ensure that your user ID is written correctly, subscription ID is written correctly, current period end is written correctly. Great! So this should handle creating our first subscription. And let's also do one more thing which is very important.
So at the end of your webhook regardless of which event just happened you have to always return something. So in this case, let's return just the status 200. So that's very important. Webhooks need to receive a success event at the end of doing something. Otherwise, If you have too many errors, the webhook will be locked.
So let's go ahead and try this out now. This is how we're going to do it. Make sure that inside of your terminal you have StripeListened forward to and that goes inside of your localhost 3000, API subscriptions and webhook. So that will now target this thing which we've just created, right? Make sure that you have your app running.
And I'm also gonna do another thing is open a third here and do bun run database Studio. So now when I click on here we should be able to look into our subscriptions and they should not exist right? There we go in my subscription here I have none. Perfect. So I'm going to go ahead and make sure that you're looking at this Stripe terminal here because inside of here you can see the success or failure events.
So I'm just gonna go ahead and give it some space here and now let's go ahead inside of localhost here. Let's refresh this entire app and I'm gonna attempt to upgrade now. So I'm gonna select this, I'm going to click upgrade, I'm getting redirected here and you have to enter test information. So that is 42, 42, 42, 42, 42, 42, 42, 42. So as many times as you can type 4-2 basically.
That is the test card for success messages. The only thing that matters with the month and the year is that it's in the future, the CVC can be anything and the name can be anything and go ahead and click pay and subscribe and now if our webhook is correct we should be start seeing events here there we go you can see the 200 successful events here which means that if I go inside of my Drizzle Studio and refresh it means that now I have a subscription successfully synchronized with my stripe. Perfect! So now that we have that we are able to do the following. We can now write the rest of the webhook endpoint.
So just ensure that you are getting 200 events here and that you actually have a subscription inside of your database here. Depending on if you're not getting 200, the way you're gonna solve your error will depend on the error code you are getting right so if it is 404 that means that what you wrote here in the API subscriptions webhook right your initial command listen to is probably incorrect or perhaps you have misspelled webhook here or maybe you have used a wrong method make sure you're using post and not get And if it's 400 it means there is an internal error inside of here somewhere that you're doing perhaps this Great, but if you're getting 200, let's go ahead and let's continue forward so now we have this event so this event is if the if for the first time the user has subscribed so what we're gonna do now is create an another event and this event will be if event.type is invoice payment succeeded. So this will be if user is updated. So you might be thinking do we really need to do anything here? Yes, we do need to do something here.
And we're gonna do the exact same thing. So we can go ahead and fetch the subscription and check whether we have user ID or not again. So you can copy and paste that here. And then we're gonna go ahead and do a wait database dot update subscriptions. And we're simply going to modify a couple of fields so we're going to modify the status if you're using the status, right?
If you're not, you don't have to And we're going to update the current period end to be new date, subscription, current period end, multiplied by a thousand. So now the subscription current period end will get updated and you will no longer display your logic as expired for this user, right? Because that's what we're going to rely on to check whether the subscription is expired or not. So make sure that you always update this field if you get the invoice payment succeeded event. And we can also update the updated ad to a simple new date.
And now we have to define for which subscription we want to do that. Well that's very simple. Let's import equals from Drizzle ORM. So make sure you've added this. So we are updating this for a specific user.
So subscriptions.userid matches... My apologies. It will be subscriptions.id matches subscription.id. So just be careful with this subscriptions and subscription. Perhaps, you know, we can call this external subscription and then, you know, it's gonna be clearer which ones you have to use.
But yeah, just be careful with the instance of subscriptions and subscription. So subscriptions is our scheme and subscription is our loaded subscription in a constant from Stripe. So that's it. We now have the functionality if this is updated. Great!
So now let's go ahead and let's actually add another method here inside of our subscriptions which will very simply return back to the user the current status whether they are subscribed or not so let's write .get slash current which will use the verify out method and it will have an asynchronous controller here so we can go ahead and copy this part from checkout like this and let's go ahead and do the following we're going to get our subscription by using await database select everything from subscriptions where we're going to use the equals subscriptions user ID matches AuthToken ID like this And now we can also create a little help for util to check whether the subscription is active or not. So we can do that by... Let's create that inside of Features, Subscriptions. Let's go ahead and create our own lib.ts file. And inside of here let's import the subscriptions schema let's define a buffer day in milliseconds to be 86 400 and then 000 let's export const check is active which will accept a subscription parameter.
And the parameter of that will be a type of subscriptions from our schema.inferSelect, like this. Now inside of here, by default, we're gonna consider every subscription as inactive until it passes this check. So if we have the subscription that's gonna be the first check And then if subscription has a price ID. And then if subscription.currentPeriodEnd is available, only then can we start checking. So we're gonna modify the active constant very simply by checking if subscription current period end dot get time plus day in milliseconds buffer is larger than date.now like this so we are always going to add one day buffer just in case there we go So this is how we're going to determine whether it's active or not.
There we go. Now let's go back inside of the subscriptions here and let's define the active to be check is active from our features subscription lib and passing the entire subscription method inside. So this is where I've added this import like this and then we can simply return back c.json data and inside of the data we're going to spread the subscription we found in the database but we're also going to return back active. So this can be very helpful for the frontend so the frontend doesn't have to calculate things themselves but we're going to calculate it for them here. Now let's create the useQuery method for this current here so that we can actually display the status.
So I'm gonna go ahead and simply copy one of the existing ones. Let's go inside of source, features. Let's copy something simpler. Or we can just create a new one so let's go ahead and write useGetSubscription.ts let's import useQuery from 10-stack ReactQuery Let's import client from libhono and let's export const useGetSubscription. Let's go ahead and define the query to be usedQuery.
Let's add the query key subscription. Query function will be an asynchronous method in which we are going to get the response using await client API subscriptions current get. If we get an error, we're gonna throw that error. Something went wrong. And otherwise let's destructure the data here.
Await response JSON and let's return back the data. And let's also return the query itself from this hook. There we go. So now let's go ahead and let's actually use this in the places we need to use it and the first place and the most important place we're gonna have to use this is inside of the use paywall hook. So let's go inside of the hooks, use paywall and let's now actually change this to use that new hook of ours.
So let's import use get subscription. You can go ahead and change the import prefix here like I'm going to do. There we go. Now inside of here, let's go ahead and let's get the use get subscription. And we can destructure the data, let's call it subscription and we can destructure isLoading and let's call it isLoadingSubscription here like this so shouldBlock will very simply check if we either don't have the subscription at all but if we do it also needs to be active so that's how we can do that and isLoading can be isLoadingSubscription as simple as that there we go So I believe that already this should be working just fine.
So if I now go ahead for example and click on here, there we go. Project is created. Excellent. But let's now go ahead and let me try and log out and go into another user. So let me just wait for me to get redirected to the editor page here.
So inside of here I should be able to use all features. For example, let's say a 3D wizard. I should be able to generate this no problem and I can already see that I did not get the pop-up meaning that it's working just fine. Perfect we have the wizard here and now let's go ahead and let's try and remove the wizard's background and there we go I also have that feature unlocked as well. Perfect And now let's go ahead and let's actually revisit the places where we need to display some kind of indicators that these things are happening.
So let's go ahead and add a crown to our avatar here if we are upgraded and let's also remove this button upgrade to image AI Pro if we are upgraded. So let's start with the user button component which is located inside of source features alph user button. So we can reuse our hook, use paywall here. And we can destructure the should block method here and trigger paywall. And we can also get the is loading if needed here.
So let's go ahead and do the following. We're going to first use the should block. We're going to use it right here. So let's go ahead and add if not should block. And if we're not loading the subscription status, let's go ahead and render a div here, which will have another div inside, and then it will have the crown icon from Lucid React.
The crown icon itself will have a class name of size 3, text yellow 500 and fill yellow 500 like this. Then the div itself will have a class name of RoundedFull BackgroundWhite FlexItemsCenter JustifyCenter Padding of 1 and dropShadowSmall and the topmost div will be our positioning div so let's give it an absolute class name and for that we also need to give the drop-down menu trigger a class name of outlineNone and relative like this So it will have minus top one like this, minus left one, a Z index of 10, flex item center and justify center. And there we go. As you can see now, my user here has a nice crown because it is a premium user. Great.
Now let's also go inside of our sidebar here and let's actually remove that upgrade to premium button, right, This one, upgrade to image AI pro. We're first going to also make that button open the checkout, but then we're also gonna hide it if we're logged in, if we are already subscribed. So let's go inside of the app folder inside of the dashboard, sidebar routes right here. And let's go ahead and add the should block and is loading from use paywall. And I'm gonna move that here.
And let's also import useCheckout from features subscriptions API useCheckout. So const mutation useCheckout. Great. So this is what we're gonna do. The button itself will have the onclick of mutation mutate so it opens the checkout and it will be disabled if mutation is pending but we're gonna hide the entire thing in case we are already subscribed And let's change the PX just to 3 here because I think we use the 3 above.
Yes. So if shouldBlock and isNotLoading and only then are we going to show this button right here. Like this. So now I should not be able to see that button but I can still see this little separator here. So we can also add that inside But then you just need to wrap the entire thing in a fragment like this.
There we go. Perfect. So now let's go ahead and let's enable the billing and then we're going to test all of this out on a new user which does not have the subscription status. So now we have to enable the billing which is something we don't have. So let's go ahead and do the following.
Let's revisit back our app folder API route subscriptions.ts and now we have to somehow actually trigger the billing. So let's add .post slash billing this will have verify out and not verify from crypto so verify out method and an asynchronous controller here we can go ahead and copy and paste this And we can also copy this as well. From here, like that. And then if we can't find the subscription, we're gonna go ahead and return back an error because we're trying to open the billing page but there's nothing to show the billing for so we're going to say no subscription found and we can add the 404 here. If we have the subscription let's go ahead and create a session which will very simply be await stripe billing portal sessions create.
Customer will be our subscription which we've just fetched above from the database dot customer ID and the return URL will simply be similar to our checkout method here, this one. So you can copy this, you can add it here, but we are not gonna add anything, right? We're just gonna bring the user back to where they came from. If we don't have session URL, it means something went wrong, so we can go ahead and return back an error. Failed to create session and a 400 error here.
Otherwise, let's go ahead and let's return back data to be session.url. There we go. So now we have to create the equivalent hook for this, which we can very easily copy from our existing, where is it, API use checkout. So you can copy use checkout and go ahead and call this one use billing. Rename this to use billing, rename the instance of client API subscriptions checkout to be billing and make sure you change the billing here as well and everything else can stay exactly the same especially the on success method here.
Exactly like this. So now let's go ahead and let's go back inside of our user button here. And let's go ahead and do the following. Let's create a non-click here. And let's also add mutation for our use billing from features subscriptions API use billing.
So what we're gonna do is the following. If should block In that case, we're gonna go ahead and do an early return and we're also going to trigger the paywall. Other than that, we're simply gonna call mutation mutate, meaning that we're gonna open the billing portal. So let's go ahead and use this on click here and let's actually add it for the billing page right here there we go and it can be disabled if mutation is pending like this and I think it will also be a good idea to only block this if it's not loading So what I want to do is just go inside of the paywall and inside of here we can add that if we are loading our subscription, the should block should always be true. Like this.
So then we don't have to manually check, you know, if we are loading then, you know, it might be visible or not. Let's go ahead and try it again. So there we go. It seems that everything here seems to be working just fine, right? I still have my access here, I can create new projects.
Great! And I should now be able to open my billing page. Let's take a look at that. And there we go, I now have an error. So let's go ahead and take a look at what the error says.
So this will always happen. You will also get an error for this and you will find that error where you run your bun run dev or npm run dev. If you scroll all the way down here, you're gonna find one piece of useful information which will tell you, stripe invalid request error. You can't create a portal session in test mode until you save your customer portal settings in test mode. So you can go ahead and use that link and simply open it.
If you can't find the link, don't worry. You can also find this setting simply by, you know, searching for the billing, right? So you have to find the settings, the billing, and then the customer portal, customer portal mode, and click activate test link. That's it. Now I can go back to your app and try and click on billing again.
This time you will not get an error, but instead you will get redirected to this page where you will be able to cancel your plan. Now here's a quick tip. If you click cancel your plan, you will not lose your premium status, right? Because you just paid for a month of use. So you will only lose your premium status once one month passes from the current period.
So that's why we did that logic inside of your lib right here. Check is active, right? So we don't care whether you cancel your plan or not. We only care what Stripe told us when the plan will expire. So don't get that confused.
Great. And now I'm going to log out and I'm going to go ahead and log in with another user. There we go. And now I want to test out the entire experience again, but from a non-authorized user, right? So let's go ahead and click this.
Upgrade to Image AI Pro takes me there. Perfect. Let me try and click on this. This opens a paid plan. Let me try and click on billing here, this opens this.
One thing we don't have solved is this, it seems like. So let's just quickly fix that as well. So let's go inside of sidebar, inside of sidebar routes, and we can go ahead and do the same thing. So I need my billing mutation. Use billing.
From features, subscriptions, API, use billing. And what I'm gonna do is the following I'm gonna go inside of my user button and I will just copy this on click here there we go and the mutation itself here will be the billing mutation And I also need the trigger paywall here. There we go. So now on click here, let me find my billing. There we go.
I will just add the on click here. Perfect. So If I am logged in, I will open the billing. Otherwise, I'm going to get the... Not if I'm logged in, if I am subscribed.
So right now I get the paid plan and I can now upgrade. Perfect. So we now have that solved. And what I want to do now is I want to create the success and the error models. So if I click on upgrade here, and if I go back here, I want to show the cancelled model.
So let's start by copying what we already have for our subscription model. So that's going to be source features subscriptions. Let's go inside of store. And inside of store I want to copy the use subscription model. And I'm going to rename this to use subscription or we can just call it UseFailModel like this let's rename the SubscriptionModel instance to FailModel like this and everything else can stay exactly the same now let's go ahead and let's copy and paste the subscription model and let's rename it fail model let's rename the actual component to fail model as well and it's going to use the use fail model from store use fail model and you can go ahead and change the prefix here like this I'm gonna move this above and I'm gonna remove the use subscription model import and this one will be a little bit different.
So inside of the dialogue title, we are just gonna say something went wrong. In the dialogue description, we're gonna say we could not process your payment. And in the dialogue footer, so we're not gonna have the separator at all nor the unordered list, so none of that. We're just gonna go straight to the dialog footer here and we're just gonna say continue like this. And we're not gonna have the disabled and on click we'll do the handle close method like this.
Let's go ahead and now define the const handleClose to do the following It will call the onClose method but since this model will be triggered by what's in our URL we don't want to create an infinite loop of that. So let's go ahead and do this let's import useRouter from next navigation and let's go ahead and add the router before we call on close we're gonna call router.replace back to the root page and then we're gonna use the handle close inside of here as well just like that now we have this fail model Let's go ahead and go inside of our components models and let's go ahead and let's import the fail model from subscriptions components fail model and simply render it here as well. Now we have to find a way to trigger that fail model whenever we have canceled equals one inside of our URL. Because if you remember inside of our subscriptions, when we work on checkout, if we cancel, that's what we put in the URL. So we now have to read that and open the fail model.
So for that, we can create a complete new component instead of source features. Let's go ahead inside of subscriptions components and let's create subscriptionAlert.tsx like that and it's gonna be used client it's gonna work with useEffect in order to read from the URL. We're also gonna need the use search params not from react use but from next navigation, use search params. We're going to need the use fail model. I'm just gonna change this to be Features -> Subscriptions and then we can export const SubscriptionAlert we can go ahead and define onOpen here from useFailModel and let's remap this to onOpenFail because we're gonna have the success as well later.
Now let's get the cancel it here so params.get.cancelIt and we also have to define the params of course so params use searchParams and let's just not misspell this so basically the same thing that you add here canceled, right? This is what you're going to have to look for here cancelled cancelled like this and then in the use effect we're gonna go ahead and do the following if cancelled on open onOpenFail and let's add Canceled and onOpenFail like this and return null and now we're gonna have to add this to our root layout So let's go inside of app layout here and let's add the subscription alert. From features, subscriptions, components, subscription alert. And you can see how mine is already fired here. So let's try this again properly.
I'm gonna go ahead to the root of my localhost so I'm gonna click on image.ai. If I refresh nothing happens. If I click on upgrade here and if I go back I now have cancelled one in my URL and I have a model we couldn't process your payment and when I click continue I'm redirected back to localhost so if I refresh from here nothing will show. Now let's go ahead and let's create the success message. So this is going to be very very simple to do.
Pretty much exactly the same. So let's go ahead and copy we can close everything. Let's go inside of subscriptions, inside of store we can copy useFailModel and call it useSuccessModel Let's rename the failModel instance to successModel Everything else is the same. Inside of the components, we can copy the fail model and rename it to success model. We're going to use the use success model from use success model.
You can of course find your own way to do this. If you wanna reuse some things more, feel free to do that. We're gonna use that. So use success model. Let's rename this copied thing which is in my success model file to actually be success model.
And we're just now gonna change the text here to say let's say subscription successful. Like this. And in the description you have successfully subscribed to our service like this and the button can do the exact same thing there we go let's go inside of models let's go ahead and copy and paste this so this will be the success model and let's simply add the success model and now let's go inside of the subscription alert inside of here we're now also gonna look for success success const onOpen onOpenSuccess will be useSuccessModel I'm going to change the prefix here like this and I'm going to go ahead and check if success we're gonna call onOpenSuccess and we're gonna add success and onOpenSuccess here let's just double check that inside of our subscriptions we actually add the proper success here, there we go. So that's the parameter we're gonna be looking for. So I'm gonna try both again.
If I go ahead and attempt to upgrade, if I go back, I get something went wrong. If I attempt to upgrade again and if this time it's successful, this time I should get back the success model. And there we go. Subscription successful. And as you can see now, I have a crown and I should be able to visit billing from here as well.
There we go. Amazing, amazing job.