In this chapter, we're going to go ahead and implement Stripe Connect so that we can finally start taking a percentage out of each sale that is made on our platform. In order to do that, we're going to have to set up Stripe Connect from our dashboard. Before you start doing that, as always, double check that you are on the master branch and double check that you have merged all of your changes. Now let's go ahead and let's go to our Stripe dashboard here. As you can see, I've been exploring a little bit, but I haven't done anything that we were from where we left off the last time, right?
I just explored my customers, right? So in here, if you go inside of more and you go inside of connect you will be able to see the get started button to power our platform with connect which is something that we need so I'm going to do this along with you we might run into some problems simply because I'm not that entirely experienced with Connect, which is kind of a good thing for you. So you can see me struggle and find things out in real time as you are as well. So let's click on get started here and let's see what we have to do. I'm pretty sure that we're gonna have to set up our guide, but let's just see here.
So tell us about your business, test your connect integration and finish setup. Let's go ahead and click continue here. And in here, it's offering us two options. How will funds flow on your platforms? So sellers can collect payments directly, or you can sell to buyers on their behalf and send payouts.
So you can decide, of course, which one is right for you. And I think that if you click show more, they will give you some examples like Shopify, Squarespace, WooCommerce, whereas Lyft, Kickstarter, and Substack would be something like this, where buyers purchase from you. So in our case, we want sellers to collect payments directly. So this is the one I'm going to select, and I'm going to click save. Now to be honest I'm not sure exactly how much does it matter which one you select and can you change it later on because our code will stay the same.
So right now I'm not exactly sure if this represents my connected account or if it represents the connected accounts that will be added on my platform. So let me just see if I click Create, what happens here? So it's allowing me to create an account here. So let's try and do this together. So go ahead and create an account just to see what happens here.
So I'm not going to change anything in here. And let's Click continue. It's asking if we want to enable OAuth. So they require OAuth, so we have to enable it. Otherwise, we can't continue here.
And it looks like we now have... Yes, so you definitely have to create your connected account, right? And now we are not gonna be sharing this link. Instead, we are going to create this link using code, right? So we are going to allow the user to onboard using this link.
Yes, but we are going to create it programmatically. So that's why I clicked Create here, because I thought that we are required to do that. But looks like when I refresh, nothing is saved here. So I don't know. Okay, so I think that I have enabled Connect now.
So I can click Overview here, and I can see that I have no connected accounts. I have no accounts to review. Everything seems to be okay, I suppose. So I'm going to leave it at here. And then what I'm gonna do is just mark this as completed.
And I'm just going to go ahead and create the verify procedure, which will allow our tenants to verify. So in order to do that, we're going to go, and we first actually have to do this. We have to modify the register procedure to assign the account ID to each tenant. So let me just see inside of my tenants collection, I have Stripe account ID right here. So that's perfect, which means that we can safely go inside of out procedures, inside of my register base procedure.
And in here, you can see that currently, for the Stripe account ID, I just add a test. So instead, let's create an account using await Stripe, which we can import from libstripe here. And let's do stripe.accounts.create. And I think we don't have to pass anything inside, because the account will be set up either way. And in here, we can then pass account and then id.
And if you want to, you can actually, let me just see. So we first create the tenant and then we create the user. Great. So we can do this in case there is no account, right? If it wasn't made for any reason, we can stop the creation here and add a code of bad request and a message, failed to create Stripe account.
Great. So what you should do now is you should also modify your seed script. So inside of here when you create the admin tenant, well it's not really important for the admin, right? So in that case maybe you don't need to modify anything. If you want to, you can add Stripe here.
My apologies. You can add the admin account here, await stripe, which you can import from .libstripe. And I think this should still work just fine so it's stripe dot accounts create and then in here you should be able to add admin account dot id So it's important that you just pass the ID here. So now, since all of our existing tenants are actually invalid because none of them have the Stripe account ID, it would be smart that we go ahead and run the seed script. In order to do that, we first have to do database fresh here.
So let's do bun run database fresh. And after we confirm that we want to remove our database and reset it, We have to wait a couple of seconds because as you might know in chapter 15, I actually got an error because I tried to seed things immediately after running fresh, which obviously wasn't enough time for MongoDB to ensure that there won't be any conflict. So this is taking some time, so I'm just going to pause, and there we go. Yes, and there we go. The database is dropped.
The error is OK because we don't have any migration files. Wait a few seconds and then do bun run database seed. So this should now properly create a tenant admin And that admin tenant should actually have the Stripe ID now. But it's not a problem if it doesn't have, because this is an admin account. But what I want to test out now is if my auth procedures register method is properly creating accounts here.
So let's go ahead and click on start selling here and I'm going to create an Antonio store here. And let's see, are there any errors with my registration? Looks like there are no errors here, which means that I can now go inside of my dashboard and then I can go inside of my tenants, Antonio, and in here, there we go, you can see that I have Stripe account ID right here. Perfect! So we have the account ID, which means that now we can mark this as completed and then create the verify procedure so we're gonna create the verify procedure inside of our checkout procedures I want to call it verify so protected procedure Z dot object I'm not sure if it even matters.
I'm trying to think. No, I don't think it matters. I think we can immediately go for the mutation here. We just have to destructure the context. And in here, the first thing we're going to do is fetch the user using await context database, bind by ID, collection users, ID context session, user ID.
And go ahead and add depth to zero, like this. Let's see, so I'm thinking, actually, what we can do, if we don't do this, I'm just interested, you know, console.log user, I want to see what I receive here. Because if I'm able to access the tenant stripe account ID immediately from the user, something like user.tenants, And then I have to choose the first in the array. And then I have to go to the tenant. Yeah, it's a little bit complicated.
So this is what I'm going to do instead. If I can't find the user, I will throw a new trpc error here with the code of not found and a message of user not found and then I'm gonna go ahead and extract the tenant ID And I can do that by specifying depth 0. What I know this will happen is that user tenants is going to be a string tenant ID. Because we set the depth to zero, which means that we will not populate this. So we can grab the tenant ID by using user tenants.
First in the array, And then this will be a string. This is an ID because of that zero, simply so we know why that is happening. So this dot tenant. And now that we have the tenant ID, we can go ahead and get the tenant using await context database find by ID here, collection tenants, ID, tenant ID. If there is no tenant, we can throw a new trbc error here, code not found and a message tenant not found.
Then finally, what we can do is we can create the verification process. So const account link is await stripe dot account links create passing the account to be tenant stripe account ID add the refresh URL which is going to be our process dot environment, and then the next public app URL, add an exclamation point at the end and add a forward slash, then add return URL like this, and type is going to be account onboarding. So that's going to create that URL for the user. So we can check if account link has no URL. So make sure to put an exclamation point here.
We are going to throw new TRPC error, code bad request, and a message, fail to create verification link. Otherwise, just a URL and account link dot URL. And in here, you can, I'm not sure if we should return the user to the root page, or maybe it's better to return them to the admin, because this is the place where they are going to activate this procedure from. So now that we have this, what we have to do is we have to create a button in the CMS, which is going to show right here. So basically, we can find many ways to put a button.
If you read in the payload CMS they offer to customize a lot of things but it would be nice to put it somewhere visible maybe in the sidebar or somewhere like here. So what I'm going to do is I'm going to go inside of the products collections here. And in the admin, I'm going to add a description here. You must submit your Stripe. Actually, let's do you must verify your account before creating products so that they know they need to do something and that's why they don't have the create new button here now I'm gonna go ahead and I will go inside of source components and in here I'm going to create stripe-verify.tsx and I'm going to import button whoops from .slash UI button actually my apologies button from at payload CMS dash UI and link from payload CMS UI I will export cons stripe verify here and I will return a link with an href stripe verify and a button here verify account like this and now that I have created this component using payload ui I'm gonna go inside of payload.config.ts And what we can do here inside of admin is we can open up components.
And there's a ton of options you can do here. I'm just going to open the documentation for you. There we go. So you can see that you honestly have a ton of options to add components. You have before dashboard, after dashboard, before login, after login, before nav links, after nav links, in the header.
There's just a ton of options you can do here, right? You can replace a lot of things. So what I'm gonna do is add before nav links and I'm going to add at slash components stripe dash details my apologies stripe dash verify And then in order to target a named export, you put a hashtag and then the name like this. So let's see if I'm going to get any errors. This is a Mongo error, so that should be unrelated.
But now when I click here, there we go. You can see that I have a verify account button here and when I click on it it should redirect me to a slash stripe dash verify which in end will just you know throw an error here because this category doesn't really exist. Great, but we now have a verify account button. So I'm not sure about the placement of this button, because it's hidden if you don't have the sidebar opened. But for now, let's just keep it here.
And now let's implement this page, Stripe Verify. So I'm going to go instead of source, app, payload, my apologies, admin, tenants. And inside of here, I'm going to create Stripe-Verify. Make sure it's exactly the same as your redirect here and inside of that route stripe verify go ahead and create a page dot dsx So now when you add something here, page, div verification, and you click here, it should redirect you to that verification page. What we're going to do here is add use client to the top and we are very simply just going to add the trpc mutation here so const verify is going to be use mutation from 10 stack react query trpc checkout verify and add the mutation options here and then we're gonna open up a use effect here which is very simply going to you can extract mutate like this and map it to verify.
And then just call verify here and add verify to the dependency array. There we go. And let's also now add onSuccess here to accept the data, and then do windowLocation.hrefData.url. And onError here is just going to redirect the user back to the root page, regardless of where they come from, because an unauthorized user can attempt to go to slash strive verify, right. So we're just going to throw an error, because verify is a protected procedure.
And then we're just going to redirect the user back to where they come from. And to make things a little bit prettier here, you can add just a simple loading icon here and a flex minimum height screen. You know, just center it. You can add text, muted foreground instead. Just a simple loader here.
And now, if we've done this correctly, There we go. It already works here. So let me close it first. Let's go to localhost 3000. So I'm logged in as Antonio, and now I'm gonna go to my dashboard.
I attempt to create a product and I see, oh, I need to verify my account before creating products. And now I click on verify account and it's loading, it's loading, it's loading, and I get a redirected as you can see. To, connecting my business with, It's called new business sandbox because that's what I called myself here. So if you change this to fun road or your name, it will tell you here, Antonio uses Stripe for secure payments. That's why you see this text here.
And if you click Return, it should redirect you back to Admin here. But the reason I don't want to go through this process yet is because we are missing a webhook, right? We need a way to update this information, Stripe details submitted, right? So let's go ahead and go inside of our webhooks here. And we are going to add one more item inside of our permitted events.
And that's going to be account.updated, like this. And then let's go ahead and add that. So go to the end of this break here and add case account.updated, like this. And I think you should have TypeScript here. Account.updated like this.
Inside, get the data event data object as stripe.account, await, payload, update, collection, tenants, where stripe account ID equals data ID. As simple as that. And something is wrong here. And what's wrong is that I need to pass my data, stripe details submitted is simply going to be whatever the webhook is holding, right? So data, stripe, my apologies, details submitted, Like this.
So even if for whatever reason our user, let's just add break here. Even if our user somehow again updates their account and they lose their verification, this will fire again. And this might be false, right? So we know to no longer allow that user to create new products. Great.
So we now have this and this should now be working, right? So let's just confirm. You can see that I don't have my Stripe details submitted right now. So I'm going to go and verify my account here with my unique user here. So I think I can use a random email address.
I don't think it matters. Let me try a fake password here. Does it matter? Or do I actually need to log in here? Okay, so I'm gonna go ahead and use my actual email address here.
And there you go. Once you are successfully logged in, you need to use your real Stripe account. So I have multiple Stripe accounts, multiple email addresses. So I just use another one, you know, just in case I think you can use the same one that you are actually, you know, logged in in which are, you can see that it actually logged me out from here. So that's important to know.
So I'm just going to go ahead and, I don't know, pick some mock data here, right? This doesn't matter, I think, because this should allow us to just use some mock information, hopefully. So I'm going to fill this out, and hopefully it will allow me to just add gibberish here. After that, go ahead and click Continue. And now it's asking us to add a VAT ID.
And thankfully, it says optional. So I don't think we have to add it here. So I'm just going to select Software here. I will add codewithantonio.com. In my first example, they actually gave me a lot of trouble with this website.
So you can just put code with Antonio.com. They seem to accept that. And in here, it will be I sell high quality courses on building web applications. I don't know, something like that. And let's click Continue here.
And at one point, we will have to add our bank information. And thankfully, we can just click Use test account. And you can see that will invent a random I ban here. There we go. I think see I am now registering my store code with Antonio calm right here.
I'm not sure I need this. I guess I'm just going to fill this out. And then just go ahead and submit. And in here, if you want to, you can select a specific commitment to show the customers that you care about, you know, the climate change. You can select continue with 1% doesn't really matter for this case.
And in here, if you want to, you can opt into Stripe tax. I think at this point, this doesn't really matter for our integration, you can do whatever you want here. And in here, you should see all of your information. I might blur this out just because I had to enter some real information here. And at the bottom, you should see Agree and Submit button.
So just go ahead and press that. And hopefully, that should trigger the webhook now. So let me go ahead and see. Oh, we didn't start the webhook. So we might have to do it all over again, because I forgot to tell you that we have to activate the webhook.
So just a second. So let's just do stripe listened forward to, and we're just going to have to enter a proper URL here. So 3000 API stripe webhooks. You can double check that that is correct. So app folder API stripe webhooks.
There we go. If you want to, you can also confirm that this is still the same. So stripe webhooks secret. Mine is identical, so it doesn't change that often. And now We have to do this again.
The only problem is you might actually receive the event randomly now, because I assume that it has failed. There we go. So you can see that some events are actually firing here, because I think that my previous event actually counted as a failed event. So maybe the event will actually be retried. But I think that if you now click on Verify Account here, you might not be able to submit your details again because it will just associate it with a already finished account here.
I think that it's just going to load. You can see it loads all the information that I already had. So let me try clicking confirm here and maybe the fact that I just submitted again will trigger another endpoint or maybe it doesn't. I think that that does not trigger it here. I'm just going to double check.
We added account updated here. And we added account updated here as well. Double check that you didn't misspell anything. So there's no reason that it should not have been accepted. But I just think that we have to go ahead and create a new account.
So that's going to be easiest to do. Let's just go ahead and log out and simply create a new account here. John, johndemo.com demo. So I could have edited this part out, but I think it's important to show you, you know, what to do if you do a mistake like I did and not open up the webhook. And you can see that it actually fired some connect account here.
So maybe it's actually authorized now. I don't know. Let's try again. So what I'm going to do is I've just created a new account and I'm going to verify my account again. And I'm pretty confident that you can use the exact same email address as before.
So that's what I'm going to do. I'm going to use the exact same information. And cool thing is, it might actually allow you to select your information from before. You can also create a completely new business, but it looks like you can also reuse your information. So I'm going to try reusing it because I think it will just speed this entire thing up, especially because it has already, you know, the website and the type of business and everything here.
There we go. So now I can just choose a different climate commitment. Let's skip it this time and let's skip the text this time. And let's see if that changes anything. So there we go.
We have a summary again. I 100% have my webhook running and I'm going to click agree and submit. And hopefully now this should be registered here in my webhook. I can see a lot of things firing here. I got redirected back and you can see I can now add my products, which means that my John has officially submitted Stripe details.
Amazing, which means that we are ready to create products. And now I can call this John's product and I can put a price here, whatever I want. I can put a category And let me just go ahead and yeah, we can also do this super secret content. And let's click save. And this should now allow users to see that right here.
Perfect. And now what we have to do is we have to modify our code so that we added the verify button as well. So now we have to modify the purchase procedure to take a fee percentage, right, because now we are connected to that business, right? I mean, their business is connected to our platform. So what we're going to do is we're going to go back inside of the checkout procedures.
And let's specifically go to purchase. This is where we are going to have to modify it. So in here we have the tenants data. So it means we have the tenant, which means we have the Stripe account ID. So the first thing we're actually gonna do here is besides checking if there is no tenant, we are also going to double check if tenant is missing Stripe details submitted.
So we can remove this to do here. And this can be a bad request here. And we can just say, tenant not allowed to sell products. So that's going to be the error that happens, right? They need to resubmit their data.
And now, after the line items, we're going to go ahead and do a total amount to be products docs reduce accumulator and the item and return accumulator plus item dot price multiply it by a hundred and set the accumulator to 0. And now we are going to take the platform fee percent and set it to 10. So we are looking for 10%. If you want to, you can put that in a constant here in your source constants, export const platform fee percentage. And you can set it to 10.
And then you can easily modify it whenever you need to. So instead of this constant, you can immediately create platform fee amount math round total amount multiplied by the platform fee percentage divided by 100, like this. And now we have the amount that we are going to take regardless of what was the price. So we are just going to pick 10% from that. So then let's go ahead and do the following.
After metadata, we are going to add payment intent data. Application fee amount will be platform fee amount, like this. And then in here, go ahead and add another set of options, like this. Stripe account will be tenant Stripe account ID. There we go.
So now, if we've done this correctly, we should just get no errors and we should simply start seeing a different type of purchase now. So let's see. What I'm going to do is I'm going to log in into my another account that I have, which is Antonio, right? It doesn't matter if you are verified on that account or not because you don't have to be verified to purchase. So I'm going to go to John's product and I will add John's product to cart.
Make sure you have your webhook running here. Let's go ahead and check out. Let's see if there are any errors. Seems to be no errors at all. And here's a cool thing.
You can see that now in here, it says CodeWithAntonio, because that's the connected business that we are purchasing from. We are no longer purchasing from our business, from our stripe. So let me go ahead and just add some test information here. So the full price is $49. And if we've done everything correctly, I think a 10% fee should go to us, and the rest should go to Colby Antonio store.
So I think everything worked well here. Let me go continue shopping here. Actually I should go to the root page. Let's go to the library here. Let me refresh the library.
Interesting, looks like this wasn't created. So I'm not sure if that's a bug or something else. And I can still add it to cart. So I'm not sure if an error happened. Looks like there was an error here.
I'm just not sure for what event exactly that happened. That's interesting. So I'm going to go ahead and debug a little bit. So I can purchase it again, it seems. It doesn't seem to be causing any problems.
So I need to understand why this is happening. I'm not sure if is it because of that error, or is it something else here? I'm definitely getting the invoice paid and invoice succeeded. So let me just double check to confirm. Maybe it was late.
Looks like it wasn't late. So I'm just going to pause a bit and explore what's going on. Okay, so I'm on track with the error. I found out that when our webhook fires, so you can see I added inside of my checkout session completed, I added a console log here and I found it here, but looks like something happens. Stripe invalid request error.
It is unable to find the checkout session here this could be because of our new stripe connect thing but I didn't actually have these problems in my initial build so it will be interesting for me to explore why this is happening it basically cannot find this so this will be a little bit interesting to debug now so I'm gonna pause and tell you any new information I found out. So I've made some progress, and I've noticed that I can actually access the account using event.account. So not data.account. I was first trying this, but we can actually use the event stripe event dot account and when you hover over this it tells you the connected account that originates the event. So what I think we should do is instead of expanded session you can see I was trying some things here.
Let's try using event.account here. Looks like there are no errors, so I think this actually might be compatible. I'm going to leave a console log here just in case. So I can purchase as many times as I want because none of this is creating any orders. And let's try one more time to see maybe that is the missing piece.
So I'm going to add this and I'm going to click pay and let's follow and see if something will break. And there we go. The account was successful this time and no errors. Does that mean that if I go inside of my library, there we go. I can see the product that I've purchased.
Amazing! And we officially took the 10% fee and you can see that I can now access the super secret content in here. So that was the issue. We needed to add the account from the event in order to expand the session. So we can only retrieve this checkout session if we associate it with the correct Stripe account, which is stored in event.account right here.
So it's the same thing as this right here. Now that I think of it, you know, since... But you know, we are always gonna know the Stripe account, I think. I'm thinking, should I save the Stripe account in the order collection? But I don't think...
Maybe we could do that as well. Go inside of your collections and go inside of orders here. And alongside Stripe checkout session ID, you can also add the Stripe account ID, but you don't have to make it required. So Stripe account associated with the order, like this stripe account ID. So then you are going to know, you know, if your order has a stripe account ID, in order to retrieve the session, you're going to need to append this.
But if you are doing a normal e-commerce without Stripe Connect, then you won't need to hold this inside. So then what you can do is also pass in the Stripe account ID here, event.account. And you can see that This is a type of string. And for me, it's telling me that Stripe account doesn't exist in the orders. But after some time, you can see it loads.
If yours hasn't, you can run the package.json script to restart the types. So if you want to you can add this here. Great! So I think that this is now working. Now I'm just going to visit the Stripe dashboard to confirm.
So now when I went to log in into Stripe you can see that I'm in a completely new account. That is because I used my older Stripe account and in here I am logged in with my new business that I've created, Coded Antonio. And if I go inside of transactions, you can see that I have all of these with $49 inside. And if you take a deeper look and go inside, you will see that there is an actual application fee, which I believe is our platform. Let me just pause.
So this loads. Here we go. So this is definitely that, the Antonio demo here. And you can see the fees here. We have the application fees and we have the Stripe processing fees.
And I think that we are the application fees here. Amazing. So now I want to also, you know, double check this on my actual Stripe sandbox, right? So in here, I think that this is us, I think that this is the percentage that we take. Now let's go ahead and let's confirm that with the other account.
So here I am back into my sandbox here and initially I thought that this isn't working because here everything is zero. But then I went into transactions and I went into collected fees and this is exactly what I saw. It is us taking the fee out of each transaction that we just tested on. So $49 you can see here from that account right we are taking our fee amazing and inside of your connect here you can see more information about your grossing account so you can see I have two of them because the first one was the one I created without the webhook active. So I have two accounts named exactly the same.
But inside, I can see some information about how much they've made. And inside of here, we can probably see the collected fees we take from them. So very, very interesting, definitely. And we can also see that you need to provide some documents. So that's how these things work here.
Amazing, Amazing job. So that's how you implement Stripe Connect. So we now officially take a fee. Amazing amazing job. Now let's go ahead and commit this change.
Of course, I'm going to go throughout the code once more in between my chapters to see, you know, did I miss something? Are there any edge cases? Can we improve something? But I am quite satisfied with this. So I of course forgot the number 25, git checkout branch 25 stripe connect, git add, git commit with a message 25 stripe connect and git push u origin 25 stripe connect there we go we are now on a new branch.
As you can see, we have detached here. So let's go ahead and review our changes here and maybe our reviewer will have something interesting to suggest to us. And here we have a summary. We introduced a user-facing Stripe verification page with a loading indicator and a redirect upon success. We added an admin action that prompts users to verify their account before creating products.
And we enhanced the checkout experience with integrated Stripe account creation and dynamic fee calculations, ensuring orders now capture the associated Stripe account details. As always, in here we have a more detailed walkthrough and two sequence diagrams. The first one is describing how we redirect the user to account verification page and the bottom one here, let me see, So when the user initiates a verification request, oh, I think it's a continuation of this one right here. Great. And we do have some comments left here.
So since this is in a seed script, This is perfectly fine. In here, it's recommending better error handling. I'm fine with just redirecting the user in this case. And in here, it added something interesting. It recommended enhancing the Stripe account with required parameters.
And it especially told me that this is for a production-ready integration. So it's interesting that I can pass the type express here and add these capabilities. So for example, I need to request card payments and transfers. So I will explore this. I know about Stripe Express.
I'm just not sure about the difference between using that for Stripe Connect in comparison to standard or custom. So I will try to explore to tell you if it's worth it changing it or not and you can try it on your own you know. Great and besides that I'm super satisfied with what we've done here. So I'm going to merge this pull request. As always I'm going to confirm that I have Stripe Connect here.
And then I'm just going to go to my last branch and I will pull origin into that branch and I will run git status to confirm everything is fine and there we go everything is merged amazing amazing job you just implemented a stripe connect and you are officially taking a 10% fee out of every purchase. Great, great job.