So now let's go ahead and let's create a user subscription alongside our refill hearts option for the shop page. So the first thing we have to do is revisit our database schema file. In here let's go all the way to the bottom here and let's export const user subscription. It's a PG table which we are going to store as user underscore subscription in the database table and then let's define its fields. So the ID is going to be a serial ID and primary key.
User ID is going to be text, user underscore ID, not null and unique. We're going to have a stripe customer ID which is going to be text Stripe underscore customer underscore ID not null and unique then we're going to have Stripe subscription ID which is going to be text stripe underscore subscription underscore ID again not null and unique. Let's copy and paste this so this one is going to be stripe price ID with a text of stripe price ID. And this one is just going to be not null, no need for it to be unique. And let's copy the last one, which is going to be stripe current period end and we are going to store it as such so it's going to be stripe underscore current underscore period underscore end and again this one is not going to be unique just not null and all of this so far except the id are going to be serial or so they are going to be text right except the last one which is going to be a timestamp So make sure you import timestamp from pg-core.
So I've just added timestamp alongside my boolean, integer, pg-enum, pg-table, serial and text. Great. So we have that now. Let's just confirm that there are no typos. So I should have stripe one, let's see, one, two, three, four, five, six, seven, eight instances of stripe.
None of them are misspelled. Customer, that seems fine. So let me just try customer. Then let's see if I have any subscription mistypes. I don't.
Price and current period and all of this is fine. Ensure that your Stripe customer ID is unique, your user ID is unique and your Stripe subscription ID is unique as well. Now that you've added that, go inside of your terminal and run npm run database push. And this will push it to Neon database so you have all the new fields here. Let's just wait a second for this.
There we go. Change is applied. So you can do npm run database studio here. And this should show Drizzle having a user subscription. There we go.
So ID, user ID, Stripe customer ID and you should not have any records for that at the moment. Great, so we can shut this down now. And how about we start configuring our Stripe configuration here. So let's do npm install Stripe in our project and let's go ahead and create a lib for that. So I'm going to go inside of the lib folder and create stripe.ts let's import stripe from stripe package which we've just installed let's export const stripe to be new stripe and let's use process.environment.stripe underscore API underscore key like that let's put an exclamation point here so we don't have this type error let's give and yes we don't have this yet we are going to add it in a moment let's add an API version So depending on when you're watching this video, once you add the quotes, you're going to see the auto-completion.
So just use whatever it auto-completes or just use what I have in this video. And TypeScript is true. Now let's go ahead and acquire the Stripe API key. So I'm going to copy that from here. I'm gonna go inside of my .environment file, and I'm going to paste and prepare that inside of here.
And we now have to obtain this by going to Stripe itself so let's go ahead and do that. Go to stripe.com and visit your dashboard here so if you don't have any project it's gonna tell you to create a new one. Otherwise, if you have projects, you can go into the upper right corner, upper left corner and click on a new account. And there we go. So I'm going to call this lingo.
I'm going to create an account. This is all going to be in test mode so you don't have to worry about that. I'm going to click for API keys for developers and in here we should have the secret key so let's click reveal the secret key and let's click again to copy it. Let's go back inside of here and let's paste the key I put it in annotations like that. Now we have our Stripe API key.
Perfect! So what I want to do now is I want to keep the Stripe tab open. So I'm going to go ahead and just keep this in the developers tab. Now I want to head back to my shop page so that I can complete the UI for purchasing a Lingo Pro subscription. But just before we do that, I want to go ahead and find inside of my public folder, in my GitHub, link is in the description, find unlimited.svg image, which we are going to use as a premium icon.
So let's go ahead and drag and drop unlimited.svg inside of my folder here and let's go ahead inside of my shop page.tsx so the page we recently created And in here we can now actually start filling the gaps where we added the to-do, right? So in here we've added a little to-do I believe, add subscription. So this is what I want to do now. I want to create a method called getUserSubscription. So let's go ahead and do this.
Let's go inside of our database, queries. Let's go all the way to the bottom and let's export const getUserSubscription. It's going to be cached, it's going to be an asynchronous error function which will first extract the user ID from await out. You should already have a wait out imported here from clerk. If there is no user ID simply return null.
Otherwise let's get our data and let's await database query user subscription query user subscription which we now have, find first, where equals user subscription that user ID and our user ID. There we go. So make sure you've added an import for user subscription inside of add slash database schema. So you need to add this import so you can use it for querying here. If we don't have subscription, very simply return null.
Otherwise let's see if this subscription is active. So remember, just by having a subscription, that doesn't mean that the user is a pro user. So when we add a functionality to cancel the subscription, There's not going to be a webhook for that, right? Canceling the subscription will simply tell Stripe to not renew the subscription next month. So the only thing that matters for us is, is the Stripe current period end value still valid?
That is the only thing determining whether the user is subscribed or not. Not just their user subscription model. So let's check if we have data stripe price ID and if we have data stripe current period and question mark get time. StripePriceId and if we have data StripeCurrentPeriodEnd?getTime let's put a little exclamation point here at the end plus and now let's go ahead and use a constant day underscore in milliseconds. So let's define that very quickly here.
So const day in milliseconds is going to be 86 underscore 400 underscore zero zero zero. So this is the equivalent of just writing 86 400 000 but this way it's more readable you can do that in javascript you can separate by underscore So we are going to add an extra day of buffering just in case. So if the current period end plus an extra day is still surpassing today's date, that means this subscription has officially expired, right? Sorry, is active, not expired. And then let's go ahead and spread the data and let's turn is active into a Boolean by very simply giving it two exclamation points at the end.
So once the user cancels their subscription, this is active will eventually turn into false, right? When a user cancels, you don't immediately block access to your app. You have to wait until a period they already paid for ends. Cancelling just means they've cancelled a renewal. Now if they refund, well that's a whole another thing to handle.
But in this tutorial we're just gonna handle the purchasing and the canceling of the strike. Great, so now we have a getUserSubscription method. So now we can go back inside of our page here and let's go ahead and add userSubscriptionData to be getUserSubscription from our database queries. So let me move that here. And then let's go ahead and add that for the second item in the array.
And then we have user subscription. There we go. So now let's go ahead and pass that in the has active subscription. So we can already do that here. So I'm gonna go ahead and pass user subscription question mark is active.
And I'm going to turn this entire thing into a Boolean by adding two exclamation points. So if you're wondering, why do I have to add this to little booleans if I already do that here? Well because we are using the exclamation point here because there is a possibility that the user subscription is null. So if it is null the result is going to be null. So I have to nullify boolean that by adding two exclamation points right.
So we solved that and now we have to do the exact same thing here. So let me just copy that here. If you want to, you can also define a constant, isPro. And then you can reuse that instead. There we go.
IsPro, isPro, and remove the to-do. Perfect. And now we can go back inside of the items here and then we can work further on this. So let's go ahead and do the following. I just want to create the UI for now.
So we already have one button. Let's create a second one. So go outside of this div, but still inside of the UL. And inside of here, give this div a class name of flex-items-center full-width, let me just add this to the top here, so full-width, padding is going to be 4 padding top is going to be 8, Gap is going to be 4 on X-axis and Border Top is going to be 2. And in here I will use an image component which we already have imported.
I'm going to give it a source of slash unlimited.svg which is an image we've added recently inside of our public folder here. So you should have unlimited.svg. If you don't, visit my public folder and add it. So we have the source. Let's give it an alt of unlimited.
Let's give it a height of 60 and a width of 60 as well. And that should be it for the image. And now besides the image, add a div with a class name, Lex1 and a paragraph, unlimited hearts. And let's just copy from the text above this class name right here. It's exactly the same.
So let me just paste that here. There we go. And now outside of this div, flex1, we have to add a button element, which will check if we have active subscription. In that case, it's going to say active. Otherwise, it's going to say upgrade.
And now let's go ahead and give this button a disabled on ending or as active subscription. And now we have to give it an on click on upgrade. So now we have to develop the onUpgrade method. So it's going to be similar to onHearts here. OnUpgrade is very simply going to start the transition, right?
And now we have to create a server action which is going to create a Stripe checkout URL. So let's go ahead and do that. I'm gonna go ahead and close everything for now. Let's get inside of actions and let's create our user subscription. My apologies.
So user subscription.ts. Mark this as use server. That is very important. And then in here, let's go ahead and let's just quickly do one thing first. So I want to go inside of my lib utils right here.
So inside of your lib folder, where you have Stripe, you should also have utils. And in here, we're gonna create one reusable function called absolute URL, which will accept the path and which is a string, and it will simply return a combination of an environment variable called next underscore public underscore app underscore url and then it's going to combine that with the path so as simple as that. Now let's go ahead and let's go inside of our dot environment file and let's add the next public app URL. So copy it from here, go inside of your .environment file, paste it here and write http://localhost 3000. So we need to do this this way because we need to pass the absolute URL to the Stripe API so it knows where to redirect because if we just tell Stripe URL hey redirect to slash shop that means nothing to Stripe webhook It doesn't know what is the absolute URL it needs to go to.
So we have to do it that way. So let's head back inside of our... We are inside of the user subscription, right? Right here. Let's go ahead and import out and current user from at clerk next to JS.
Let's import the stripe library from ad slash lib stripe. Let's import the absolute URL from ad slash lib utils and let's import get user subscription from ad slash database queries. Then let's define the return URL to use the absolute URL and simply go to back to slash shop. So this is going to be HTTP localhost 3000 slash shop and then later when we deploy we're going to have to change the environment variable to be HTTPS you know my dummy website blah blah blah so that's how it would look like, right? So let's go ahead and go to export const, create stripe URL.
That's gonna be an asynchronous server action, which is going to serve for both creating the initial checkout URL and also the settings URL for the subscription. So let's extract the user ID from await out. Let's extract the entire user from await current user. If there is no user ID or if there is no user, throw a new error, unauthorized. Now let's get the user subscription by doing await get user subscription, which we have added an import for.
If we already have a user subscription and if we already have usersubscription.stripeCustomerID that means that we, instead of returning a checkout URL we'll return a checkout portal URL. So let's go ahead and write const stripe session to be await stripe billing portal.sessions.create Customer is going to be user subscription dot stripe customer ID and return URL is going to be the return URL which we have created right here So that's why we need to pass that. And then what you're going to return to the front end is an object data which is going to be stripe session dot URL. That's the only thing we need. And this method ends here for users that already have a subscription right here.
So even if their subscription expires, we still don't want to return the checkout URL. For them, we're just going to return a... We're just going to return the... What's the name? The checkout portal, the billing portal, right?
And billing portal doesn't work right away. The first time we are going to try it, we're going to get an error in the terminal because we have to enable it for development, but it's a one-click thing, so don't worry. And now we have to write the code to create the similar stripe session but to give us a checkout URL so let's write const stripe session await stripe checkout sessions create mode is going to be subscription, payment method types can be whatever you want. So in here you have a ton of PayPal, I have no idea what these things are honestly I'm just gonna add credit card right You can go ahead and add whatever you want here. And customer email is going to be user, which we have from our await current user from Perk.
So user email addresses, the first one in the array, email address. Line items are going to be an array quantity is going to be one price data currency is going to be whatever you want, I'm going to use US dollars. Product data is going to be an object with the name of LingoPro and a description of unlimited parts. Unit amount outside of the product data, but inside of the price data object is going to be 2000. So that is equivalent to $20.
And recurring is going to be interval month. So just to repeat this is equivalent of 20 US dollars, right? Great! So we have that and here's a very very important thing. Without this, this payment will not work at all.
So outside of the line items array you need to add metadata and in here pass in the user id which you've extracted from a waitout above. So that's how we are going to know once our webhooks fire which user purchased something. Otherwise we have no way of knowing. So now let's add a success URL to go back to the return URL and cancel URL to go back to return url. It's the same thing.
And then finally return data stripe session dot url. There we go! We are ready to try this out now. Let's go back inside of our shop page. Let's go inside of the items and in here let's use create stripe URL method.
So you can import create stripe URL from user actions, sorry from actions user subscription And then let's go ahead and do the following. So create a stripe URL, dot then, get the response. If we have response data, in that case, do window.location.href and response.data, because that's going to be a URL string. Let's also add a catch with a toast from Sonar. Whoops, so we already have toast error, something went wrong.
Just in case that happens. Let me just align this like that great so that should be it do we already have those we already have those great So let's see if this is working. So right now, if I click upgrade, I should get redirected to a Stripe checkout page. And there we go. I'm redirected to the Stripe checkout page.
But right now, if you try, so with Stripe, you always have test cards, right? So this is an example of one. 424242424242. So just type in 424242424242 in that order until you fill it up. And then you have to enter a date in the future and you can use any three numbers for the CVC, anything for the name.
So this is a test card, which will always succeed. So you can see the test mode being written here and you can see the $20 per month, and you can see unlimited hearts and the name Lingo Pro. When you click pay and subscribe, nothing will happen. So right now this will succeed. This will be a payment for you, and it will just return us back to the shop page.
That's it. But there is something missing here, obviously, and that is to create a webhook, which is going to catch the payment, and then using, where is it? Using our metadata, which we've added here, user ID, we are going to detect which user just successfully initiated the payment and then we are going to create a user subscription data for that user. So the reason this isn't done with a .then promise or a wait is because payments can take some time. There are security checks, there are 3D secures, and sometimes it just takes some time to process it.
But as long as you have your webhooks in place and the correct metadata, no matter how long it takes for the banks to process for your customer, eventually your code will work and create a proper database table for them. So that is the next step. We now have to create a webhook. So let's go ahead and do the following. The webhook is going to be an API route.
So let's go inside of the app folder and create a new folder called API and inside of it we are going to create a folder called webhooks and then inside of that another folder stripe and finally instead of page.dsx, route.dsx so that not dsx, .ds that will represent an API route So now you can visit slash API slash webhooks slash stripe. And that is a request. And which type of request that is defined by this export asynchronous function. And if you want it to be get it would be get. If you want it to be post it will be post, patch, patch.
So in our case, it's post. And let's make the request a type of request here. And now let's destructure the body by using await request dot text. So text is quite important here. And now let's get the signature to ensure that whoever is trying to access this webhook is an actual application that we expect in our case that is stripe so let's use headers from next headers to get the stripe-signature as string.
So that's what we are expecting. Let's go ahead and define the event to be a type of stripe from stripe.event. So we are now importing Stripe from its original package, not from our library. Let's go ahead and open a very simple try and catch here. So let me prepare the catch.
And in here, we are going to attempt to structure, to construct the event. So let's write stripe.webhooks.constructEvent. And I forgot to import our stripe from lib stripe so let's prepare that so stripe.webhooks.constructEvent so this is some additional security checks so using the body the signature from the headers and one last thing process.environment.stripe underscore webhook underscore secret that's what we are going to do to ensure that whoever is trying to access our webhook route is the confirmed user. So there is pretty much no breaking this, right? No way to manipulate this.
Otherwise, let's return a new next response, which you can import from next server. So let me add that here. The next response is going to return a very simple webhook error, error.message. And let's import, let's get the error like this. The error will also be a type of any.
And let's also define the status to be 400. There we go. So the status is quite important in a webhook because a Webhook will know what's going on depending on the status. So make sure you give it a breaking status, right? Now we have to get the Stripe Webhook secret.
So let's go ahead inside of our environment file and let's prepare that Stripe Webhook secret here. And now we have to get it in a very certain way. Since in production it's just a matter of getting it from the console but in development it's a bit harder because we are working on localhost. So our website is not publicly available for Stripe. So they cannot really send a webhook to a random local host computer, right?
Your ports are not exposed to the internet, thankfully, and hopefully they are not. So now what we have to do is we have to go ahead and learn how to set up that. So inside of your Stripe dashboard, click on the webhooks here and click test in a local environment. And first things first, you need to download the CLI. So click on this link here and whatever you are using, Homebrew, APT, yum, scoop, macOS, Linux, Windows or Docker, follow the instructions and make sure you have Stripe available in your terminal.
After you have Stripe available in your terminal, you're going to run the first command, stripe login. Let's go ahead and do that. So I'm going to go ahead and go inside of my terminal here and I'm going to run Stripe login. That will give me an URL link so I can press enter now and you can see how it opened that. And in here I have my code.
Nice, I don't know what it is but it's a code that I just have to confirm is the same, right? It's just a security that no one else is trying to connect but me. So I can see it's the same code so I just click allow access. And then go back in here and you should see that it says completed. And now what you have to do is you have to copy this stripe listened forward to.
So that's going to simulate our webhook. But we have a little mistake here. So first of all we are not using this port and this is not our endpoint. So our endpoint is the following localhost 3000 slash 3000 slash api slash webhooks slash stripe. So that is our endpoint.
Let me close the this running. There we go. So Stripe listened forward to localhost 3000 API webhooks Stripe. That is what we are expecting. So you can confirm that in here.
There we go. App API webhooks Stripe. So that is our endpoint. And now this bold text is your Stripe webhook secret so just paste it inside of quotes like this. Make sure there are no spaces at the end or the beginning of your secret.
So just make sure you are able to select exactly what's bolded here. And there is one thing we have to do before we try out the next thing. So we have to enable this to not be protected by any authentication at all because Stripe webhook has its own authentication. You saw that here. So you see how we construct the event.
This is the authentication for the webhook, right? So let's try it out. We have to go inside of the middleware and alongside having a slash as the public route we also have to enable a slash API slash webhooks slash Stripe to be enabled as a public route. And now what we can finally do is make sure you have your app running, so npm run dev. Make sure you have Stripe listened forward to running.
Make sure you have the webhook key added in your .environment variables right here as a Stripe webhook secret. Make sure that inside of your webhooks post, make sure it's a post method. Your webhook needs to be a post method. Make sure there are no misspellings in the Stripe signature and no misspellings right here. A lot of people make this mistake.
Please copy and paste that from here. That's the only way you can ensure there are no typos. And now let's go ahead and do the following. Let's copy the third step, which is just triggering a random CLI action. So I'm gonna open my third terminal here and I will just paste that here.
And there we go. It says that it succeeded, but if I go into my other terminal there it looks like there are some errors happening here looks like something oh no no that's something else where is my all right so we have an error 500 it seems so something is definitely wrong We should not be getting an error 500. So let's go ahead and read what this is. Oh it's because we did not finish our webhook. Yes, so this is technically correct but let's go ahead and just wrap this up by finishing it correctly.
So we have to go back inside of this route right here and outside of this try and catch event you always have to return a new next response null with a status of 200. So Stripe webhooks will cancel themselves and pause themselves if they receive too many invalid events. So make sure that you always return 200 at the end and be scarce with your errors in the webhook. Let's try this out again. So I'm gonna go ahead and rerun this Stripe trigger payment intent and then I'm going to look at this again.
There we go. This time no errors and we have 200 inside of our post methods so here where I'm running stripe listened forward to the first instance you should have gotten 500 and now the second instance you should be getting 200 meaning this is correct perfect so ensure that you are getting the 200 event And now we are ready to actually write the logic for this. So let's go outside of this try catch event and let's get our session using event.data.object as stripe.checkout.session. So we define the type for this constant and we have Stripe imported here from the main library. Now, if event.type is equal to checkout session completed, in that case, we can create a new subscription.
So first let's extract the user ID metadata so we know for which user is this webhook firing. So const subscription is await stripe subscriptions.retrieve and we are going to pass in the session.subscription as string here. If there is no session, question mark metadata, question mark user id. In that case, we are going to return new next response user ID is required and a status of 400. And inside of this user ID, just confirm that inside of your, what is it?
User subscription actions, metadata, user ID values need to match exactly. So capitalization is important here. That is what we are searching for here. So make sure you don't have, if you have user ID two here, you need to search for that here as well. So just ensure that this is correct.
And now in here, we're going to create our user subscription. So await DB from database drizzle then we are going to dot insert into user subscription from database schema, so make sure you add that import. Values. And they are going to be user ID, which is session.metadata.userID. Stripe subscription ID is going to be subscription.id.
StripeSubscriptionId is going to be subscription dot items dot data first in the array dot price dot ID. So How do I know it's first in the array? Because in my user subscription when I generate the checkout session, my line items has an array and only one element inside. So that's how I know that is the one that I need. And stripe current period end is going to be new date.
We're going to use the subscription.current.periodEnd times a thousand. There we go. So that will successfully create the first time subscription payment. And now we're going to create a different if clause. So outside of this if event checkout session completed we're going to handle if a user is renewing their subscription so if event.type is invoice.payment succeeded that means user is renewing their type so this is if a user is creating the subscription for a first time but now we are renewing it so let's just copy and paste this subscription because it's the same thing for retrieving it there we go but we don't have to check for the metadata now instead we can use await database update user subscription again so we already have all of this dot set this time stripe price ID is going to be subscription items data first in the array dot price dot ID and stripe current period and is going to get updated so that little value of ours which checks whether something is active using the stripe current period and and the day offset will now be valid So very simply we are using again the exact same thing.
Subscription.currentPeriodEnd times a thousand. There we go. And let's use .where and let's import equals from Drizzle ORM. So just ensure that you have that. And pass in the user subscription dot Stripe subscription ID needs to match subscription dot ID.
There we go. So user subscription is imported from database schema and we are comparing a Stripe subscription ID here because in our schema, all the way to the bottom here, Stripe subscription ID is unique. So we can use that without the metadata like user ID here to very simply update an existing subscription and give it a new expiration date. So this is an expiration date right here. Great.
One thing I forgot to mention, this multiplication by a thousand is simply a way to ensure that the timestamps are correctly represented in our JavaScript database by turning them into milliseconds. So that's what the thousand is doing. Just in case you were wondering. And now this should completely work. So let's try it out.
So this is what I'm going to do. I'm going to close everything. I'm going to go inside of my terminals here. Ensure that you have 200 getting requests, ensure that you have npm run dev running, and in my other terminal, I'm gonna run my database studio here. So my database studio in here should not be having any user subscriptions.
There we go. This is no rows. It's completely empty. So now if I go ahead and refresh my app here, going to the shop. Let me just wait a second for my app to completely refresh.
And if I click upgrade right now, let's go ahead and do the following. Let me just prepare my Stripe webhook. So go in your terminal where you have your listened forward to, where you're seeing this 200 events. And let's go ahead and do our dummy card, something in the future, any three numbers, any letter and click pay and subscribe. And if everything is working, This should create our user subscription and you can see a bunch of 200 events, meaning everything is correct and there we go.
We have unlimited hearts, there we go, it's currently active and as you can see We have an infinite sign here in our hearts. Perfect. And if I go inside of my Drizzle Studio, and if I refresh this, there we go. I have a user subscription. Perfect.
So I just want to do one more thing. I want to go inside of the shop here, page.vsx, go inside of the items. And I don't want to disable this button right here if we have an active subscription, right? So instead of this thing, update, I want to say settings, right, because we already have unlimited hearts. So in here I want to get that error for my billing portal if you remember.
So right now if I click on settings here I should get an error. There we go I got an error something went wrong and if you go into your terminal you're going to see why. Internal error. You can't create a portal session in test mode until you save customer portal settings. So you have to find this link and click on it and open it right here.
And that will lead you to the customer portal. So if you want to, I'm pretty sure you can somehow search for this as well. Let me close this. I'm pretty sure that there needs to be a way for you to search this customer portal. There we go.
Settings, billing. There we go. Just search for customer portal if you can find the link and then click activate test link. That's it. And now try and click on settings again without changing any code.
You should now be redirected to your Stripe current plan settings. And there we go. You can see when our plan renews and from here we can cancel the plan. But canceling it will not immediately remove my infinite hearts. It's only going to remove my infinite hearts after my plan is supposed to renew.
So we still have to fulfill the fact that the user paid for this month. That's a lot of confusion that people have in my YouTube videos. They always tell me, oh, you didn't handle the unsubscribe. There is nothing to handle with the unsubscribe. It simply won't renew.
That's the way you handle unsubscribe. I'm not sure if it's even legal to remove user's account, user's pro account during their one month of payment. So I'm pretty sure that is illegal to do. So be careful with that. Not legal advice, of course.
Great, now we have implemented Stripe. Remember, whenever you're testing Stripe, you need to have your Stripe running. Where is it? This. You need to have Stripe listen forward to running.
Otherwise, it's not going to work on localhost. In development, in the production, it's going to be a little bit different. Perfect. So what we have to do now is we have to ensure to fill all the gaps for our to-do lists. So I wrote a bunch of this to-do in our app.
So let me remove everything. There we go. So in here I'm saying to handle subscription. In here I'm saying to handle subscription. So a bunch of places where we need to handle subscription.
And then once we finish that, we're going to add quests and leaderboard. And then we're going to be able... So for example, here's a little bug. Here we display five hearts and I'm pretty sure I know why that is happening so that's what we're gonna do in the next chapter we're also gonna add some new elements here in this tab and then we're gonna create an admin dashboard. Great, great job!