In this chapter, we're going to go ahead and implement Stripe integration. Before we go ahead and proceed with the steps outlined in the document, I want you to confirm that you are on the master or main branch and that you have merged all the changes from the previous chapter. You can also do that by running git status. Now let's go ahead and let's create the Stripe project. So right here I've created a new Stripe account.
So what I'm going to do for now is just skip this step if you're being prompted here. And we actually might have to run over this wizard later, but for now I'm going to skip this completely. I just want to obtain the API developer key. And this is a completely new account. So basically, we are seeing the same thing now, right?
But in case you aren't, go ahead and create a new account. So in here, it's telling me to verify my business. So I'm just going to click, got it here. And I will just close this for now, right? I don't want it.
And I want you to notice a couple of things going on here. So as I understood, Stripe has changed from their test mode to sandbox, right? So sandbox is different from test mode in a way that I feel it's more isolated, right? So for example, the way test mode works is that you have your account, which is your real business account, and then on that account, you turn on the test mode. Whereas sandbox is like a completely separated instance from your actual account.
And you can see that in here, I can't even exit the sandbox because I need to verify my business before I go live. So I think that you're going to have the same structure here. It will depend on whether you use Stripe before because I know that on my other Stripe accounts, I have a little bit of a different interface, but I'm pretty confident. Click in here and try and create a new account. And then you should basically see the sandbox by default.
So basically what we need here is we need to copy the secret key. You can see how it already knows from the recommendations here, but just in case you can't find your secret key here, I always like to go ahead and try to find it through the dashboard here. So we can access the developers from the settings, but we also have the developers right here in the corner. So I think that's how we can also access that. There we go, developers API keys.
And you can see how in here I can also find those secret keys. In our case we are interested in the secret key. So go ahead and copy it. Make sure that you are in that sandbox or test mode, you know, whatever pops up for you. And then we're gonna go ahead and add Stripe values here.
So that will be Stripe underscore secret key. Make sure that you don't add next public in front of that. Next public basically means that the environment variable will be accessible on the client. We don't want that when it comes to the Stripe key. And once we have that, let's go ahead and do bun at Stripe.
And I'm going to show you what is the latest version. So you don't have to type in at latest if you want to be on the same level as I am. There we go. Not same level, same version, right? So if you want to, you can add Stripe at 18.0.0.
And once you have your Stripe version, let's go ahead and let's install, my apologies, let's create the stripe util. So inside of lib here, create stripe.ts, import stripe from stripe, and export const stripe to be new stripe. And now let's add our environment key I always like to copy it and add an exclamation point at the end so you get rid of the type errors now we have to specify the API version which is always easy to do because you can just select whatever pops up here. So this is the version that I will be using. If yours is different, I don't think it's going to matter that much, except if you're watching after some breaking API change.
But I think that these API versions are fixed to the package version. So if you want to, if you have some other version, you can try installing this one and see if you get this. And let's add the TypeScript set to true. So now we have the Stripe library here. And the next thing I want to do is I want to go inside of my checkout procedures right here.
So, so far, we only have get products here. And now let's add a purchase procedure. And this will actually be a protected procedure. Basically something that I only want logged in users to do. But the problem is we only have base procedure as of now, right?
So let's go inside of at ERPC init here, And let's go ahead and create the proper protected procedure. So the way we're going to do that is by doing export const protected procedure, Base procedure, so we are basically going to extend on top of this one because we want to have the payload inside. Base procedure, use asynchronous, destructure the context and next. And now inside of here, we are first going to get the headers using await get headers. We can get headers from headers, next headers, as get headers.
So in here, I'm awaiting the headers, and then we can get the current session using await context database out and simply passing the headers. I think we actually do that very same thing in the out procedures, right? When I attempt to get the session, we do exactly that. But now we are creating a reusable way of doing it. And now I'm gonna do the following.
If there is no session.user, I'm going to throw new trpcError. You can import the trpcError from at trpcServer. In here, add a code, unauthorized, and a message, not authenticated. Or you can add something like must be logged in if you want to be more friendly to the users. And now what you want to return is a next.
And inside of the context, you want to preserve the context and then add a session. Preserve the session and then add session.user. You might be wondering why am I doing this weird spread thing. I'm doing it because of type interference. I'm not sure what's the correct terminology, my apologies.
But you can see how when I hover over session now, it will tell me that user is a user which exists, which makes sense because we added an if clause. If it doesn't, break the method. But if I were to just do session, session, now it actually wouldn't take in consideration that we added an out check for the user. It would just assume that session.user is something that could potentially be undefined. So basically, I could demonstrate this later in a better way, but for now, trust me, you want it to work like this.
Just spread the user individually, and then this will be a much safer to use field. Safer in a sense that it will have proper types, right? The actual security is the same. Now we have this protected procedure and we can use it every time we need to do something that only an authorized user should do. So instead of base procedure, we now use protected procedure from trpc init.
And inside of the input here, we're gonna go ahead and pass in a Z object, product IDs, array of strings. And we're going to ensure that at least one is passed. And tenant slug here will be minimum of one as well. So basically, we want to ensure that both of these are passed. Then let's go ahead and add mutation here, context and input.
And let's go ahead and get the products using await context dot database and then find collection products like this you can set the depth to two so we load as much info as possible And then where we're going to go ahead and do and open square brackets, and then we're going to do two combinations here. The first one will be if id is in input IDs, product IDs, correct. The second one will be if tenant.slug equals input tenant slug. So we are basically protecting on the ORM level that whatever products we are just about to initiate a checkout with is truly A, an existing product and B, inside of that tenant slug. So even if the user somehow manipulates the local storage, so they add some IDs that they want, we are going to ensure that that cannot happen because nothing will be loaded, right?
And now if you want to, You can go ahead and take a look at, my apologies, let me just go in here. You can do if products total docs does not match input product IDs dot length. And in here, you can throw in your trpc error code not found with a message products not found if you want to right so this will basically mean that something was incorrect with those products incorrect with those products. And now what I want to do is I basically want to, I want to find the tenant that we are loading the products from. So let's do const tenants data and let's call that await context database find collection tenants limit one agenation false where slug equals input tenant slug.
Now let's do tenant to be tenants data docs first in the array. If there is no such tenant, we're going to throw a new PRPC error, code, bad request, message, tenant not found. Actually, it can be not found as well because It is as required that we find the tenant as it is that we find the product. And I'm going to add a little to do here. Actually, I think, yeah, I'm going to add a to do.
Throw error if Stripe detail is not submitted. So I'm not going to do that yet because I want us to just see Stripe working. But what we can do now is we can create line items, which are a type of Stripe, which we can now import from Stripe. And you can go ahead and import type specifically. So it's a type of stripe.checkout.session, create params.lineitem.
And it's going to be an array of those. So what I'm going to do is I'm going to go new line products.docs.map and then get the product. Quantity of each product will be just one because these are digital products, right? Price data will be unit amount product dot price. But keep in mind that Stripe does those prices in cents.
So we have to multiply our price by 100. Stripe handles prices in cents. The reason they do that is because it helps with calculation And using milli units or cents like this is a correct way to handle money amount in your database, right? Because if you do something like 0.1 plus 0.2 in JavaScript, not just in JavaScript, right? But in basically all programming languages because of the IEEE standard, you would get something like this.
Basically, in not exactly precise amount. So what they do instead is they multiply this by 100 and they multiply this by 100. And then you're working with milli units, which in turn give you a more precise result. So that's why You always have to multiply here by 100, otherwise you're going to get the wrong price at checkout. So that's how you have to handle that.
After unit amount, set the currency to US dollars, and then the product data. Set the name to product.name. And the metadata here will be the following. So Stripe account ID. Actually, we don't need a Stripe account ID here yet.
So the metadata doesn't have any predefined types. You can pass in whatever you want. So let me just check my tenants collection quickly. We added Stripe account ID, so yes, we can actually use it. We have both Stripe details submitted and Stripe account ID ready, so let's do it.
Stripe account ID is tenant.stripeAccountID. ID is product ID. Name is product.name. Price is product.price. Currency will be US dollars.
Actually, I don't think we need the currency here at all. I think this should be enough. Just keep in mind that this isn't strictly typed. So what I want you to do is I want you to create a type called product metadata. So inside of the checkout module, go ahead and create types.ts.
Let me just properly name this types. And in here, I want you to export type product metadata. And you can remove currency, actually. We don't need currency. So just product metadata.
And then you can do as product metadata here. This way, you will ensure that you don't add any mistakes, because the metadata object is not typed. You can add whatever you want here. This is just so you can access it later on through a webhook. Great.
So now that we have that, let's go ahead and just create the checkout. I think that's pretty much all we need. So we have the line items now. Let me just see where does the line item code end. So now what we're gonna do is const checkout, await stripe, We can import stripe from our libutil.
There we go. Let me just go down here. So stripe.checkout.sessions.create. Customer underscore email will be context.session.user.email. And I think now is the perfect time to show you that potential error.
If it happens, maybe it won't. Maybe I did something wrong the first time. Basically, you can see how now we have no errors here at all. Basically, this exists, right? It's a string.
But if I go inside of the protected procedure here. And instead of doing this, I just spread the session here. My apologies, I just do session like this. Then you will notice that in here, it says that context session user is possibly null, which makes no sense because we protect against that here. So that's why I told you to do it like this, because this way, the types get inferred properly.
There we go. Now let's go ahead and do success URL. Let's do cancel URL. Basically, the success and cancel URL are both going to be the following. So I'm going to do process.environment.nextPublicAppURL.
You can always double check in your .environment that you have next public app URL. So just copy it here. And you can copy the same thing for the success URL. And it will depend where you want to redirect your user. So in my case, what I'm going to do now is I'm going to append the generate tenant URL, basically.
Actually, for now, the way I'm going to do it is by manually writing my tenant URL here. So tenants, input tenant slug, checkout. And I'm going to append success equals true. So basically, I will copy the same thing for the lower one, besides except the cancel will be set to true. The reason I'm not going to use that, usually will make a bit more sense later when we actually implement those subdomains.
So now we have to add mode and set it to payment. Line items will just be my line items constant, which we have defined right here, which basically goes over all the products which we have in our cart. Now, I want you to enable invoice creation, so just enable true. This will allow us to later look at the invoice if we need it. Metadata, this will be important so we know what user actually did the purchase.
So in here, add context session user.id. And perhaps we could add a type here, export type checkout metadata. And we're just going to set user ID to be a type of string, so that you never forget to do this, right? Check out metadata from types. And besides the metadata, actually, that's going to be it for now Because I'm not yet going to implement the whole taking 10% off the fee, right?
We first need to enable proper Stripe Connect for that. In this chapter, I'm just trying to do a normal purchase, a normal checkout. So let's do if there is no checkout.url, we're going to throw new TRPC error with a code internal server error and the message fail to create checkout session otherwise let's return an object of URL and checkout.URL. Now let's go ahead and go back inside of the checkout view right here. And we're now going to add besides the use query, we're also going to add a purchase using use mutation from 10 stack react query.
So just make sure you have added the this. Let me just the this. Let me just move it here and add trpc.checkout.purchase. And let's do mutation options inside. And in here, we go ahead and we pass everything we need.
In this case, that would be product IDs. Let me see. Mutation options. Actually, we don't need to pass anything in the mutation options because that's not how... The options are not the same like here.
I was expecting the need to pass the ID here, but we don't have to do it like that. I'm going to show you why in a second. So basically, now I want you to go in here in the checkout sidebar and on checkout do purchase.mutate and then in here you pass in the tenant slug and the product IDs. There we go. And in here, you have the onSuccess and onError.
So you can handle all of these events here. Great. So let me just rename this to on purchase in this case, and go inside of the checkout sidebar, and replace that here, and here as well. And we won't actually be needing to go inside of the checkout sidebar, because we're going to be able to do everything from here. So this will be if purchase is pending, in that case, this will be is pending.
And let's make this disabled instead. I think that makes just more sense, even though I just said that we're not going to have to go in here anymore. I just think it makes more sense this way. The only thing we are missing is the canceled state. So what I want to do is I want to go inside of checkout hooks and I want to create use checkout states dot DS and I'm gonna make use of our good old NUX with parse as Boolean and use query states and we're going to export use checkout states which will return the use query states and return success or cancel.
And both of them are Booleans with defaults set to false and options clear on default. So now, let's go ahead and introduce those new states here. So what I'm going to do is right here, introduce states and set states, use checkout states. And you can use the relative import like this. And now we're gonna go inside of the useEffect here, and I will just have a separate useEffect for this.
So useEffect. We're specifically gonna focus on states.success. And yeah, states.success here. So what I'm going to do is just if states.success. In that case, what I will do is clear my cart.
Not clear all carts, just clear this current cart. And now when I think of it, remember that Code Rabbit suggestion. If we notice any invalid products in this card, I think it makes sense to just clear this card, not all cards. I think it just makes more sense, right? So The upper use effect, you can also add clear cart here.
I will add to do, invalidate library, which we don't yet have. And we will also get const router to be use router here from Next Navigation. So after the user successfully purchases something, we're going to redirect that user to slash products. Or maybe we are going to call that library. I'm going to see.
But for now, let's go ahead and just add the router here. Great. So that's if we receive back states.success. But if we receive states.cancel, we don't have to do anything inside of the useEffect. We just have to detect it here, states.cancel, like this.
And now what I want to do is I want to go back inside of my purchase mutation here, and also add onMutate. In the onMutate, I'm going to call setStates, and I'm just going to reset them. So success is set to false, and cancel is set to false. So I want to completely reset those states. When success happens here, we actually have to open up.
So this isn't when I say on success here, I don't mean user successfully purchased. I just mean the checkout link was successfully created, which means that we first have to obtain the link, and then we have to do window location.href data.url. In the error here, I'm going to check if error.data?code is unauthorized. In that case, I want to do router.push. And for now, I'm just going to redirect the user to slash sign in.
And I'm going to add a little to do here, modify when subdomains enabled, because we're going to have to handle that later. And let's add toast from Sonar. I already have it. Great. So let's add toast error, error.message here, just so the user knows something's going on.
Even though if the error is that the user is unauthorized, they are going to be redirected, right? Great. So I think that we are now ready to try this. So what should happen now? Go ahead inside of a checkout and make sure that you have some products added inside of your cart.
I'm just going to do another refresh here. There we go. So I have something here. And when I click checkout, you can see that it says not authenticated and it redirects me here. So I'm going to log in.
I'm going to go back inside of my Antonio here in the shop. And when I try now, if everything's done correctly, I should get redirected and I do get redirected to the checkout screen. And you can see the price is correct, even though we're multiplied it by 100, that's because it accepts cents. And let's go ahead and check. You can see how if I leave, the state turns into cancel.
Go look in your URL. So if you check out, click check out again, now it clears. So if you click back, that counts as cancel equals true in the URL, and then this gets triggered. So I want you to go back to Antonio's shop here and just add another product. There we go.
Add to cart. So now I have two products here, 29 and 59. And let's click checkout. And let's see. There we go.
88. Perfect. The only problem now is nothing will really happen if we actually purchase this. So we can of course try, because we want to test this out. We have the use effect here, which clears the cart if it receives the success state.
So basically, let's go ahead and do that. You can use test card information. So that's 42424242 and nothing else matters. It just needs to be in the future, right? And go ahead and click Pay.
And after this, you are redirected to Success, True. And it looks like I got something here, maximum update that exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array or the dependencies change on every render. So that's what happened when I called my clear cart here. So that is interesting.
I will explore why that happened. Yes, and it happens every time. Okay. So I'm going to pause a bit and see why it's happening. One idea that I have is that if I enter state.success, the first thing I should do is change set states here, success, false, and cancel false as well.
So it doesn't immediately go into a loop. And let me just add set states here. I'm not sure if that will maybe improve it, but you can see that now my products are definitely cleared here. So I think that... It could be that maybe adding a clear cart, which is a function which is not memoized, causes it to go into an infinite loop.
And because I didn't prevent it, it went into the maximum update depth, which does make me wonder if this function is doing something dangerous as well. I will research about the clear cart and how I can add it to use state, use effect. So now that we know that we can purchase, we can checkout, and we have successfully cleared our cart once we purchase, which, by the way, we can try again. So let me just add something to the cart. It's now in my cart.
I'm going to go ahead and do the checkout now. Let me see if the error will appear again. Sometimes this actually happens in development because of the redirect, but let's see. Looks like now it's okay. I believe that it was because states.success must have constantly retriggered this for some reason.
But when we immediately return it back to false, it doesn't go in here again, and thus it doesn't cause problems. So for now, this seems to be working just fine. The problem is nothing really happens, right? We don't know who purchased and we don't know what exactly to give to the user who purchased it. So in order to do that, we are going to have to create a new collection called orders.
Let's go inside of collection and let's create orders.ts. So the orders collection, we can go ahead and import the collection config from payload. And we can go ahead and add const orders collection config. Now in here let's add a slug orders admin use as title we can set the name. And in here, let's add some fields.
The first field will be the name of the order, which will be a type of text and required true. Now, obviously, it is not important for an order to have a name, but it will be easier to work with it later on if we actually have some useful information to show, right? So now let's add user, which is a type of relationship, and its relation to user. It's required and has many will be set to false. So one order per user.
And now we're going to do the same thing, but for the product. So this will be product, relationship, relation to products. And the same rules here. And then let's also add name, stripe, checkout, session ID. And let's pass in the type to be text, required to be true.
There we go. Now that we have that, let's go inside of the payload.config and let's add the orders from the collection orders. Great. Now what we have to do is we have to add a webhook. So how does a webhook work?
In order to obtain the webhook in the first place, we have to go inside of Developers here and select the webhooks. Looks like they definitely changed this since the last time I was here, so let me just... Yeah, it looks like when I clicked close, I got access to this. So feel free to close that pop-up. And in here, it offers you to add a destination.
But we're not going to do that yet. We're going to test with a local listener. So yes, the first thing you need to do is download Stripe CLI. They have support for all operating systems. You can do it using brew, yum, apt.
They even give you some common mistakes here. Or you can use normal installation files, right? So make sure that you have installed Stripe to your CLI. And then let's follow the instructions here. I'm going to zoom in so that you can see.
Let's start with Stripe login. So I'm going to open a separate terminal here, and the first thing I'm going to do is Stripe login, which will give me the pairing code, and it will give me the link. So I'm going to open this. Let me see if it opened here or not. Command click.
There we go. And the only thing you ought to do here is just confirm that you have a matching pairing code just to ensure that it's you who is trying to connect and select your business sandbox. So I will just click allow access. And now you can see that I am officially logged in. So we have to do that.
That's the first step. And usually, this shows you like a little tick like, hey, you've done that successfully. But now I can't see it. The second thing we have to do is we have to add stripe listen forward to and then our URL. So let's go ahead and add this, but we have to modify it a bit.
So first of all, we are using port 3000. And then the second thing is that we're going to go, whoops, we're going to go to slash API, and then slash Stripe, and then slash Webhooks, like this. So localhost 3000, API, Stripe, Webhooks, and press Enter. Now, this will be a local listener and you're going to get this important signing secret which you have to copy once you have copied it you have to go inside of here and add stripe webhook secret and add it here. Now that we have this set up, we are ready to actually create our webhook.
So why do we even need a webhook? Well, take a look at this method here, checkout procedures. So once we actually create the checkout URL and then inside of the checkout view, What we do here is we just open up the checkout, right? We don't really have a way of knowing when the user purchased something. Sure, we could rely on state.success, But you already saw how flaky that is, right?
I mean, I got a maximum update depth there. Imagine your purchases depending on the client. The connection can be slow, something can override the URL. It's absolutely not recommended or reliable to do that. But what we can do instead is wait and listen on our server until Stripe tells us, hey, the user successfully purchased.
So that's what webhooks are for. Now let's go ahead inside of source app folder, app route group, and in here we have an API folder, and in here I will just create a new folder called stripe, and inside I will create another folder called webhooks. So this now matches exactly our structure that we listened to, API, Stripe, webhooks. Inside of webhooks, create a route.ts. Let's start by importing the Stripe type.
Then let's import getPayload from payload, then the config from payload.config, next response from next server, Stripe from lib.stripe. And that's going to be it for now. Let's do export asynchronous function post. And let's accept the request here. Now what we have to do is we have to validate this webhook, because anyone can contact this endpoint.
We have to make sure that it's actually Stripe who is doing that. And we can do that with the StripeWebhooks secret key that only we know. We and Stripe, that is. So let event stripe.event. And then we're going to open a try and catch block here.
Inside of the try block, we're going to assign the event to be stripe.webhooks.constructEvent. And inside of here, we're going to await, open parentheses, await again, request.blob, execute it, .text, and execute that. That's the first argument. The second argument is request.headers.get stripe-signature. Be careful, you have to write this correctly.
As string. And the last one is process.environment and I recommend that you don't type it, instead copy it so you know it's the correct one. And add the exclamation point at the end or simply as string. So this will either succeed or it's going to throw an error if any of these are incorrect. So basically, the request that is trying to access this endpoint will need to include the Stripe signature in the headers.
And if the Stripe signature can be decrypted with our Stripe webhook secret, and well, whatever is being done in the construct event, we are going to know that it is Stripe who is trying to tell us something. But in case an error happens, we're going to go ahead and just do a return next response.json with the message webhook error, like this. And let's add a status of 400. And you can make a pretty error message like this. So error message, error, error.message, or unknown error, like that.
And in here, you can do if error exclamation point is instance of error. In that case, do a console log of the error. So basically, we're just giving us as much info as possible about what's going on. And now let's go ahead and change this to Vectix and just render the error message here like that. And instead of this console log here, You can also make it a bit prettier.
I would recommend adding an emoji like this so you see it in your terminal. It's going to be useful, trust me. And add the error message. Do it like this, console log, and in here, log the entire error. Basically, if it's compatible, we can log the entire error, but we are definitely always going to log the message.
So now this will just help you see it more clearly if something wrong happens. Now, if you manage to pass the try catch block, what you can do is you can add a success here just so you can see that in your terminal as well with the event.id. And now let's do const permittedEvents, which is a type of array of strings. So for now, all I'm going to look for is checkout.session.completed. Always double check that you have written this correctly.
So checkout dot session dot completed. Otherwise, it will not be able to recognize it. Great. This is also a good time to add our payload. So await getPayload and in here just pass the config.
So the config is using a shorthand operator because we named it config here. There we go. Now let's do if permitted events includes event.type. Let's go ahead and set let data and then in here open a try and catch block here. And inside of try, open a switch case, event.type.
And if case is checkout session completed. So you can see how it offers me proper type here. So you can now double check that you have written it correctly here. So if that is completed, so if this is the event, what we are going to do is we are going to change the data to be event.data.object as stripe.checkout.session. And now we're going to check if there is no data.metadata?userid.
So basically, instead of our procedures, if we forgot to add the user ID in the metadata, there's no way we can know who purchased this. In that case, we have to throw a new error here. User ID is required. Now let's go ahead and attempt to fetch that user using payload. So await payload find by ID and in here users and you no longer have to use the question mark here.
And now the same thing. If the user in the payload isn't found, we have to break this method. And now what we can do here is we can expand the session. So const expandedSession here will be await stripe.checkout.sessions.retrieve by data ID. And in here expand line underscore items dot data dot price dot product.
We are basically trying to see the information of the product that someone just purchased or multiple products. Because in here, you can see that we have the name of the product, we have the Stripe account ID, we have inside, Again, name of the product, perhaps we don't need that as well. But we also have the price, the ID, which is what we are looking for. We are looking for the ID of the product from our database that the user just purchased. And this is how we are going to find it.
So now we have to check if the expanded session is missing something. So if there is no expanded session.line underscore items question mark dot data, or if there is no expanded session dot line underscore items dot data dot length. So if any of that is not true, or in this case if it is zero, we're going to throw new error here, no line items found. It basically means we couldn't load what you've purchased. But if it can, let's do const lineItems here, expandedSession.line underscore items dot data.
And now I want to give this a proper type. So let's go inside of our modules, checkout, types, and let's do export type expanded line item to be stripe.lineItem. You can import the type stripe from stripes. So stripe.lineItem, but we are going to expand it. So we know that we expect the price inside, which is stripe.price.
And inside, we have product, which is stripe.product. All of that is something we already know and something that we already have a type for. But what we don't have the type for is the metadata, which is a type of product metadata. There we go. Or you can use semicolons.
So now that we have the expanded line item, just make sure the order here is correct. You can go back here and you can mark this as expanded line item. Make sure you import it from the modules here. And it's going to be an array of those, like this. There we go.
So now lineItems has the correct type and it's going to be easier for us to work with them. So what we can now do is do a loop. For const item of lineItems, we can do await payload create collection orders. And now we can create an order for each product. But since obviously it will be useful for us to know if some products were bought together, we can still do that using the Stripe checkout session ID.
That's why we added this field. In case your types are not working or you can't select orders as always, you can do manual run of generate types for them to be added. So also ensure that you have actually added your orders. Right, you need to have this, you need to have the correct slug, and ensure that you have added them in the config here, orders. Let's go back inside of the route here.
So this will be data.id, user will be user.id, product will be item.price.product, and then .id. And in here, name will be item.price.product.name, like this. And I think I've made a mistake here so I think that this product ID is the Stripe product ID, not what we want to associate our order with. We want to use the metadata ID. That is the product ID.
We know that because, let me go to our procedures here. So we know that because in here is where we create it in the metadata, right? We don't add it here. The same thing with the name. So the name can exist both in the product data and the metadata.
It's completely the same if you use it right here or if you use metadata.name. So this should definitely exist at this point. That's why I don't know if you... Maybe you can throw an error here if it doesn't exist, right? But it should exist, otherwise you shouldn't be able to even create this.
So that's how we associate an order with a newly created product. Great. And now let's go ahead and simply break the method here. And what we have to do is add a default here. Throw new error, unhandled event, event.type.
And in the catch here, let's get the error. Let's console.log the error and let's return next response dot json here message webhook handler failed And a status of 500. And now, a very important thing that I personally often forget. You always have to return something for the webhook. The webhook needs to know if the event was successful or not.
So make sure that at the end, you always return 200 OK and only throw errors when they are actually errors. Always keep in mind that if the webhook receives too many errors, it will shut down that webhook. That's how Stripe webhooks work. So you have to be careful and always give it proper information. So I think that now, once you purchase an item, you should have a product associated with an order or an order associated with the product and with the user that did the purchase.
So let's go ahead and try it out. In here I have my webhook running, so you need to have this. Every time you want to test it out, you need to have this running. Obviously, in production, you're not going to need it. This is just for local development.
So what I'm going to do, right now I have no orders whatsoever. And I think that you can see that if I go inside of the dashboard, since we didn't restrict the access to the super admin, everyone can see orders at the moment. Obviously, we're going to change that later. But right now, Let's just wait for this to order. Everyone can access anyone's orders, and there are no orders here to be found.
So let's go ahead now and let's go back to the store, and let me buy this product. So I will click Add to cart here. I'm going to check out and I'm going to go ahead and purchase. And let's see if that will work or not. So I'm going to purchase and we can Now go inside of the terminal and you can start seeing the events going here.
You can see how we have all 200 successful events. Inside of this one, I can see the success messages. This gives me confidence that the order was actually created. I think the event was handled properly. And we can easily look to confirm that by going back to our dashboard.
Let's just wait for my loading state to appear. Let's go inside of orders. And there we go. You can see that I have... Let me just see.
Can I untitled ID? I just want to confirm that this is associated with the correct order. And I think it is because it noticed that this is another Antonia's product. I think what's missing here is in my products collection, admin, use as title name. I think we need to add this and then refresh this.
Yes, looks like this is still showing like this. Obviously, you wouldn't be able to modify this at all, any of these, right? Looks like it is able. So let me just double check here. Metadata, we do the product ID, right, which is definitely what we need.
In this metadata, we do context session user ID. So I think that all of these are completely correct. I don't think there's anything wrong here. And in the route, let's see, we do the exact same way of assigning these. So let me just see, can I maybe copy this somehow?
Let's just remember six, seven, and two, two. So if I go inside of products here, I think there is another way I can do it. I can go here and look at the... It is 6, 7 and the last two numbers are 2, 2. So I can see that in my URL up there.
So It's definitely the correct URL. It just seems to, it looks like it's not properly related. I'm not sure if that's the case, untitled ID, because it's definitely titled here. So that's something I'm going to explore. But nevertheless, I think we can end the chapter here because we did, in essence, what we wanted to do.
We have created the Stripe project. We added the SDK. We implemented the checkout procedure. We created the orders collection. And we implemented the webhook route, which creates the order associated with the user and the product.
That's what we wanted. So I'm going to explore this in the meantime for the next chapter and tell you my conclusions about why this looks like it's not associated but I'm pretty confident that it is I just think something's wrong with my setup here now let's go ahead and push this to github so 20 stripe integration I'm going to go ahead and do git checkout-b 20 stripe integration. Git add, git commit 20 stripe integration. And now git push uorigin 20 stripe integration. If you want to, you can shut down the webhook, but I will remind you in the next chapter that you need to have it on if you want to test out your app.
So now confirm that you are on your new branch here and in here confirm that you are detached and let's go inside of e-commerce github here create a pull request and let's review our changes and there we go. So the walkthrough This update integrates Stripe payment processing and enriches the checkout experience. A new Stripe dependency is added along with a webhook handler to manage Stripe events. A dedicated orders collection is introduced along with appropriate types and configuration updates in payload. In the checkout module, a new hook, purchase mutation and UI modifications streamline the purchase flow.
Additionally, a protected procedure is established in the TRPC initialization for authenticated API requests. And in here we have some sequence diagrams describing in here our purchase mutation, which returns the checkout URL. And in here, if you are interested, you can pause the screen. So you will see how the webhook actually works. So from Stripe, we receive a post event payload.
We validate event using the secret. And after that, we go ahead and handle the case if the event is valid and the type is recognized. And then depending on what the event is, in our case, only one event, we retrieve the session and create orders and order creation confirmation and then we'll return 200 OK. But in case an error occurs, we throw back errors. It has some recommendations for us here.
This one I like is basically recommending to wrap variable declarations, basically opening them up as if they were if clauses using curly brackets. I like that. Yes, we could do that. I'm going to do that separately in the next chapter. This is the to do, that's fine.
And in here, we know we have the Stripe secret, this is okay. Let's go ahead and merge this pull request. And after you have merged the pull request, as always, we're gonna go ahead and synchronize our main branch. As always, I'm just going to confirm that I have my branch here there we go and then let's do git checkout main or master git pull origin main or master and after that git status there we go and ensure that your graph has merged it as well. That's it!
That is our chapter and see you