In this chapter, we're going to go ahead and implement payments. So let's go ahead and visit Pollr.sh so we can create our account. There are two websites we're going to use, Pollr.sh for production and Sandbox.Pollr.sh for development. But nevertheless, start with Polar dot sh and go ahead and log in and simply continue with your preferred provider. If it's your first account, you will most likely have to go through an onboarding process where you will just name your organization and maybe add a product.
After you've done that, you will see the name of your organization right here. This is your production, right? This is where you will actually pay out. What I want you to do is I want you to go down here and click go to sandbox. This will redirect you to sandbox.polar.sh right this development instance and I want you to repeat the process here So you will have to create one more account here because these are completely separate instances, which is a good thing, right?
And in here you will see a big banner which says, changes you make here don't affect your live account and exit sandbox. So you are always aware that this is just for development. Now let's go inside of settings here and let's go ahead inside down here my apologies and find developers and click new token. I'm going to call this meet I'm going to call this app development and I will set it to never expire and I will select all scopes and I will click create and then I will simply copy that token. Then I'm gonna go inside of my .environment, I will make sure I'm on the default branch here, and I will add polar access token.
Like that. After I have added the token, I am ready to integrate Polr with my BetterAuth integration. So I'm going to go inside of Polar Documentation and I will go ahead and go inside of Integrate, Framework Adapters, and I will find BetterAuth. In here we have the installation script, but since I already have BetterAuth, I'm just going to install these two so let's go ahead and do npm install and let's add this just in case they fail to install great once we have this installed let's go ahead inside of our app, my apologies, inside of lib auth. And in here, what we're going to do is we're going to add our Polr instance.
So inside of lib here, also make sure to create the polar.ts instance and we can now import polar from polar sh sdk and we can export polar client and make sure you're using your new environment variable here, polar access token, and make sure you put the server to be a type of sandbox, otherwise this access token will not work. And as always let me just show you my polar versions here. So this is my better auth plugin version and the polar sh version. Now that we have the Polar client here I'm going to go back inside of my AuthUtil and I will now import the following from PolarSH BetterAuth. Polar, Checkout and Portal.
And I will also import the actual Polar client from my newly created Polar. And now I'm gonna go ahead and I will add some plugins. So let me add the plugins field and inside of here I will add polar and execute it and open an object inside. Inside of the polar plugin I will add client to be polar line. I will add create customer on sign up to be true and I will add use which will allow me to add even more plugins here.
So let's add checkout and inside of here let's restrict the checkout only for authenticated users and when we successfully buy something we will redirect to forward slash upgrade. And let's also add portal. So we are using a very nice modular way similar to the actual BetterOut plugin to add and enable some things from Polr. So this is probably the most important line in this entire setup, because this will allow us to synchronize our users with Polr customers without us needing to constantly listen to a webhook, we'll simply be able to query by a customer external ID. In order to properly test this out, I highly, highly recommend deleting all of your existing users.
So make sure you are on a clean slate. And now let's go ahead and let's do npm run dev, let's run the webhook and let's run ingest. And let's go ahead and let's go to the localhost 3000 and also open up Polar Sandbox and go inside of your customers here. Right now you can see I have no customers at all. But now, once I create a new account here, I'm going to use Google login here, something different will happen.
So nothing has changed in my app. But if I go inside of customers and refresh here, you will see that I have a new customer with the exact name, exact email, and probably the most important thing here. I have this user's external ID right here. So this external ID matches 100% what I have in my database here. So that is the exact URL, that is the exact ID.
So this will allow me to simply fetch Polar SDK instead of constantly listening and relying to webhooks, which are prone to failing and then restrying and then getting out of sync. It's basically a much, much better experience now. And this will be true for every single new customer that is created. Amazing! This is super, super cool.
So what I want to do now is I want to implement the free tier usage into our app because this user by default is a free user, they haven't upgraded. So let's go ahead and let's create that component here, which will help the user see how many free trials they have, their usage. And in order to do that, we're going to have to implement a router called premium router so let's go inside of modules here and let's create premium module, let's create a server here and inside procedures.es and inside of here I'm going to import base procedure, create DRPC router and protected procedure. I'm going to export premium router here. And inside, the first thing I'm going to do is I'm going to create a get free usage.
So this will be protected procedure query async. They structure the context here. And then I'm going to attempt to get the current customer by using polar client from lib polar and I will simply call get state external and I will call by external ID which is basically querying the customer by this which we already have because it's the ID in our database so that's the magic of this integration that's why I told you that inside of my auth.ts this line is probably the most important. And just before I forget, there is one thing we have to do more. Instead of auth client, we have to go here and we have to add plugins here, polar client, and import this from polar.sh.betterauth.
So don't import it from our polar client here maybe I shouldn't have named it polar client because it's a server thing I'm using client in a different sense here. Just be careful. Import from the actual npm package and save that file. Now let's go back inside of our premium router where we will now look into free usage here. So Once we get our customer, we're going to check if they have any active subscriptions by using customer.activeSubscriptions and simply the first in the array.
Why do I know I can look for the first in the array? Because by default, we are not allowing customers to have several active subscriptions. You can enable this, and then you would have to obviously change your code to support that. But by default, you can see that is turned off. So each customer can only have one active subscription.
And if there is an existing subscription, I'm just not going to show the free usage because there is no free usage. This user is subscribed. Make sure to not accidentally put any exclamation points here. Now what we are going to do is we are going to import the following from our database schema, agents and meetings. And we are also going to import the actual database.
And let's also import equals and count from drizzle ORM. So now what we will be able to do is actually count our usage. So let's start by counting the meetings this user has created. So user meetings, and the only thing we are adding in select is count and we are counting by each meeting ID that we get from this query. Make sure to import count from drizzle ORM and make sure to add the proper where query so you only count by the currently logged in user ID since this is a protected procedure.
Besides the user meetings we are going to use the exact same logic and query to create user agents. So just make sure to change all instances here to agents but you are still querying for user ID like that. And then in here you will be able to return back to the user the meeting count and the agent count. And let's actually remove the base procedure. We are not going to need it.
Great! So now that we have this let's go inside of the RPC Routers app and let's add this to our router. So premium, premium router, like this. And now that we have that, Let's go ahead and let's create a component called dashboard-trial. So I'm going to go inside of source, inside of modules, dashboard, components, and I will create dashboard-trial.dsx.
Inside of here, I'm going to import link from link, rocket icon from Lucid React, use query from 10 stack React query, use TRPC from TRPC client, button from components UI button, progress from components UI progress. We are using this for the first time. So it's a ShadCN component. We've added it in the first chapter by adding all components. And then I'm also going to do one more thing.
I'm going to go inside of premium and I will create a new file called constants.ds and inside of here I will define the maximum number of free meetings and free agents. Let's reduce this number to three for easier testing. Let's go back inside of the dashboard trial, and let's import those two from modules premium constants. Great. Now let's go ahead and let's export const dashboard trial.
And what I want to do here is I want to add trpc from use-trpc, and then I simply want to load my free usage. So let's use our new premium.getFreeUsage procedure which we have just created here. If there is no data, I will simply return null here. Otherwise, let's go ahead and let's return a div with a class name Border, Border, Border with 10% opacity, Rounded, Large, Width, Full, Background, white with 5% opacity, flex, flex column, and gap y of 2. Then let's add another div with padding of 3, flex, flex column, and spacing on the Y-axis.
Inside of here, open up a div, flex, item center, and gap2. And let's go ahead and let's render the rocket icon with size 4, and let's render the free trial text. This is enough for us to actually render this component somewhere. So let's go ahead inside of our dashboard sidebar. It's located in the same folder as dashboard trial in the dashboard components module.
And then let's go ahead and let's import this down here, the same way we import the dashboard user button. And all we have to do is find a place for it. So let's go all the way down to the place where we actually render the user button. And how about we go ahead now and we render this, the dashboard trial. Now let's see how it looks.
Perhaps we will need to modify something. So let's refresh this. And there we go. You can see that I have free trial text right here. What I want to do is I just want to wrap these two and just give them a class name of P2.
Actually, I don't want to do that. So let's just revert to as it was. So let's continue developing the free trial component here. So we are going back instead of dashboard trial. What I'm going to do now is go outside of this div encapsulating the rocket icon and the paragraph, and I will add a new div with flex, flex column, and gap y of 2.
And inside of here, I'm first going to render a paragraph with text extra small, which simply uses data and agent count which we got from here right when we counted our agents and I am comparing it next to the maximum free agents constant so it will be zero out of three agents right here. And then what I'm going to do is I'm going to use the progress component just below this paragraph like this. And I'm going to divide the agent count by the maximum number of free agents and multiply it by a hundred so it becomes a percentage. Right now this is completely empty. Now copy and paste that entire structure and change this to be meeting count and this will be max free meetings and change this to meetings and also change the actual function here to work with meetings and data meeting count just make sure you don't use any agent instances here and now you have the meetings down here And the last thing we have to do is we have to add a button outside of this div here.
So just a button like that. And let's go ahead and let's give it as child prop. And let's give it a class name so it looks good background transparent border top border border with 10 opacity hover background white with 10 and rounded on top will be none and inside upgrade button upgrade text Let's refresh to see this in action. For some reasons, oh my apologies, let's add a link here with an href of forward slash upgrade like this. And I think that now, there we go, now it looks normal.
When you click on it, however, it will lead you to a 404 page. That's fine. We will develop that later. But now go inside of agents, for example, and just try creating a new agent. So test, test.
And you have to do a hard refresh now. And you will now see one out of three agents have been filled. And the same thing will happen for meetings. So if you create a new meeting and do a refresh, the refresh is important because we don't invalidate this query. It's a completely new query, so we didn't know about it before.
Let's hard refresh, and then you will see your meetings increase as well. Now let's go ahead and let's actually throw an error when we hit the limit here because right now nothing would happen. So we can do that by going inside of source, trpc, init. And in here besides protected procedure we are now going to create premium procedure, which will be a function which accepts the entity for which this is a control for, which can either be meetings or agents. And then in here, we are simply going to extend the protected procedure.
So protected procedure dot use asynchronous and the structure the context and the structure next. Like this. Inside of here let's go ahead and let's grab the customer using the Polar client. Make sure to import one from libPolar like this using our instance of it. It's quite important that you don't accidentally import the other one.
And make sure you're using the external user ID here. And now what we can do is we can just do the exact same thing with it in our procedure a moment ago. So we get the user meetings and user agents here let's add them here and we have to import the database we have to import the meetings schema and we have to import the agents schema And now we have to import count from Drizzle ORM and we have to import equals from Drizzle ORM. There we go. So from Drizzle ORM, count and equals and agents and meetings from database schema as well as the actual database.
Perfect. So now we have user meetings and we have user agents here. And now let's go ahead and let's check if this user is premium. So is premium if customer active subscriptions is larger than one. And now let's check if we have reached the free agent limit or a free meeting limit and we can do that by comparing the queries above with the actual constants which we have to import so let's just import them make sure you have added these constants and then we are going to check if we should throw meeting error so should throw meeting error if entity is equal to meetings and if is free meeting limit reached and if we are not premium then the same logic here for agent error.
Just make sure you change the entity to agents and isFreeAgentLimit reached. And then let's go ahead and check. If shouldThrowMeetingError, throw the code forbidden and you have reached the maximum number of free meetings. Like that. And in here, after that, if we should throw an agent error, same thing, just the maximum number of free agents.
And then in here, let's just return next and let's add customer in here simply so we have access to it if we need to. Perfect! Amazing! Now we have our new premium procedure. Let's go ahead and use it.
So we're going to go inside of our agents procedures, so modules agents procedures here. And let's find the create procedure. And instead of using protected procedure, let's use premium procedure from TRPC, and let's select agents here. So make sure you import premium procedure from TRPC in it for the create method. And everything else stays exactly the same.
You don't have to change absolutely anything. And let's make this easier. Let's go inside of our premium constants and I will allow only one free agent for a free tier. So you can see that now immediately it shows one out of one agents. So what happens if I try to create a new agent now?
I'm getting an error. You have reached the maximum number of free agents. Let's double check that this is working correctly by deleting an existing agent. So let me just delete this agent. And let's try again.
And this time, I shouldn't be getting any errors, right? Ignore that this is not updating in real time we will change that as well so only after I have actually reached the limit do I start getting these errors right here. And now let's do the exact same thing but for our meetings procedure. So find the create and instead of protected procedure we are going to use premium procedure and simply select the meetings. Make sure you have imported the premium procedure.
And now the exact same logic is applied to the meetings. Perfect. So what we have to do now is we have to properly resynchronize this every time that we create or delete a meeting or agent. So let's go inside of the agent form. Instead of agent form, I think we already have a to-do here.
Invalidate free tier usage. So let's just copy this, let's paste it, and let's go ahead and add trpc premium get free usage and remove this to do here. And no need to pass any objects inside. And copy this from now on. The second place you want to do this is the agent id view right here.
Let's go inside of the agent id view and find the remove agent use mutation and go ahead and invalidate reusage every time that you delete an agent. Then let's go inside of the meeting form and in here in the create meeting we have the same to do. So let's simply invalidate our free usage and now meeting id view because in here we have the remove meeting mutation so let's go ahead and do this mark this as async and then you can await both of them here So now if you go ahead and for example delete an existing agent, let's go ahead and try it out, you will see this immediately clearing up. So let's click delete, confirm, There we go, it's immediately cleared. And if I go ahead and add a new one, it will immediately be refreshed.
And now it's one out of one. Amazing. What I want to do now is I want to handle the errors. So once I get this error right here I want to be redirected so let's go ahead and do that I'm gonna go ahead and I can actually search for to do probably and in here let's go inside of agent form so go inside of agent form and go inside of create agent here and we have an on error so let's check if error dot data dot code is forbidden Let's go ahead and redirect the user. We need to add the router.
Const router, use router from next navigation. Make sure to add that. And then in here, simply do router.push forward slash upgrade. And then let's go ahead and do this in one more place it seems no we don't have to do it in the update agent that's fine there we go and now let's consider the meeting form so meeting form let's find the create meeting and on error let's do the exact same thing. If the data code is forbidden let's redirect the user.
We need to add the router from use router from next navigation. Just make sure you are doing the proper import here and you can remove the to-do from the update meeting as it's not required here so now let's go ahead and refresh to see this if I go ahead and create a new user and click Create. And I get an error forbidden. I should be redirected. And I am.
It just took a while. And it's a 404 page because we haven't developed it yet. So let's go ahead and let's develop it. In order to develop the upgrade page, we have to add a few more procedures to our premium router. So let's go inside of our modules premium server procedures and above get pre usage let's go ahead and let's add get products.
So it's going to be not a base procedure. Let's change it to be protected procedure like this. And we're simply going to use polar client SDK to list the products which are not archived and which are recurring. And then let's go ahead and let's return product result items. And then let's go ahead and let's do another procedure here called get current subscription.
So in here, we are going to fetch the current user using Polar Client customers get state external and using the external ID. We are going to check if we have an active subscription. If we don't have an active subscription, we will just return null. Otherwise, if we do have it, we will match the associated product. So we can use the subscription to get an individual product using subscription product ID and then we will return that product back.
Perfect. Now let's go ahead and let's go inside of source app folder dashboard and let's create a new upgrade folder inside a page. Let's go ahead and do a default export here. And let's add hydration boundary here. Let's add query client from get query client from 10 stack my apologies from TRPC server and let's do void query client dot prefetch query and inside of here we're going to prefetch our new TRPC procedures So trpc from the server dot premium and let's prefetch current subscriptions.
So get query options for that. And also prefetch get products like that. Make sure you are importing TRPC from its server instance. And let's turn this into an asynchronous method. And let's also add our usual authentication redirect using the out from lib out using headers from next headers and using redirect if there's no session from next navigation.
Perfect. And now let's go ahead and let's add the state to the hydration boundary here. Let's import dehydrate from 10 stack react query. And let's go ahead and let's add suspense from react. Let's add error boundary from react.
And let's render the upgrade view, which does not yet exist. Make sure you have added both of these from their correct imports. Double check the error boundary because the IDE often forces you to import the incorrect one. Let's go ahead and let's add fallback here to be to do for both of them and let's create the upgrade view so we're going to do that inside of modules premium and let's create ui and let's create views and inside let's add upgrade view dot TSX. Now let's go ahead and let's prepare the imports.
Use client is very important and then use suspense query. Then let's add our usual imports. Use DRPC, out client, error state and loading state. Let's go ahead and let's create our upgradeViewLoading and upgradeViewError. So we've already done this a couple of times, we are just reusing our error and loading states with their titles and descriptions.
And now we can go ahead and we can import these things from modules.premium.UI.views.upgradeView. Let's also quickly add export.const.upgradeView. And let's just return a div.upgradeView, just so we have all three. There we go. And let's go ahead now and let's add the suspense to be upgrade view loading and let's change this to be upgrade view error.
There we go. So now when you click on upgrade here you should finally see the text upgrade view. Perfect. Now let's go ahead inside of here and let's actually load the information that we need. So go ahead and add trpc from use-trpc.
Go ahead and get the use-suspense query for trpc-premium get-products and map the data as products. And then do the same thing for the current subscription using getCurrentSubscription. So these two procedures which we have just created here. So we are finally using them here now. And now, when you refresh, you should see the loading state or the error state if something goes wrong.
Now let's go ahead and let's give our first div some class names here flex1 py4 px4 this is unneeded so just a medium px8 flex flex column and gap y of 10 then inside of that let's go ahead and add another div like this, which will have margin top 4, flex 1, flex, flex column, gap y 10, and items center. Now let's add a heading 5 element, which will render the current user's plan. So heading 5 with a class name of font medium text 2xl on medium text 3xl you are on D then we add a space and we add a span with font semi-bold and text primary which attempts to read the current subscription name. Use the question mark here because this can be undefined. And fallbacks, it falls back to free.
And then space here plan. So by default it will say you are on the free plan. Perfect. Now let's go ahead and let's develop a component called pricing card. Let's go inside of UI And let's create the components folder.
Let's create pricing-card.psx. Now in here, I'm going to import CircleCheckIcon from Lucid React and CVA and VariantPropsType from Class Variants Authority. If you're wondering where does this come from, we have it in our package JSON because ShadCNUI added it. We are going to use it to create different variants of this pricing card. Because as you can see here, we will have a light version and kind of like a exclusive highlighted version.
So that's what we are going to use this for. And now let's import all the other imports here. CN from libutils, badge from components badge, button from button, and separator from separator. We already used all of these in this tutorial. So let's create a constant pricing card variants and let's actually use the CVA which we imported.
The first thing we're going to do is give it the default classes. So rounded, large, padding 4, py6, and width full. And now in here we open the variants and we define the prop variant. It can be either default or it can be highlighted. Make sure to properly type it because there is no type safety here.
So for the default, it's going to be white background and text black. But for highlighted, it will be background linear to bottom right from this green color to this dark green color and text white and then we're going to add the default variant down here to be default. And then we're going to do the exact same thing but for the icon variants. So it will be the exact same structure. You can copy this and you can paste it.
I'm going to show you the finished version. So there we go. Pricing card icon variants. Default class name size 5. Variant.
Default and highlighted. In default fill primary and text white. In highlighted fill white and text black with the default variant of default. And then one more variant pricing card secondary text variant with the default class name of text neutral 700. And then in the default variant that very same text and in the highlighted a text neutral 300 and no need to add the default variant here and one more variant my apologies this is the badge variant so this one this small little badge here pricing card badge variants so by default text black text extra small font normal and padding of one And then on default that it will be background primary with 20 opacity, but when highlighted it will use this kind of beige color.
And the default variant is default. Now let's go ahead and let's create an interface which extends variant props which we imported from here and give it a type of pricing card variants. We can use just a single pricing card variants because every single one of these use the exact same variants. So that's why I told you to copy and paste, so you don't add any typos. Perfect.
And now we're going to add all other props here, and the variant props will be automatically added. We don't need to type them manually, but we do have some other components which we need. I mean, some props. Badge, which is an optional string or null. Price which is a number.
Features which is an array of strings. Title which is a string. Description which is a string or null. Price suffix which is a string. Class name which is an optional string.
Button text which is a string and onClick which is void. Now let's export const pricingCard and let's simply destructure all of those elements from the props here. There we go. So all of those elements from above. Now in here, let's go ahead and let's return a div.
And we're going to give it class name of CN and simply add pricing card variants and add variant in here. So where do we get the variant prop from? From here. But we didn't explicitly add it here because it was automatically added thanks to variant props from here because they read pricing card variants, right? And now let's also add class name here and let's just add border.
Then let's add a div here with a class name of FlexItemsAnd, and GapX4 and justify between. Then let's go ahead and Let's add another class name with flex flex column and gap y of two. Now for the title and badge section, we add one more div with flex item center and gap x of two. So this was gap y two, this is gap x two. The first one is a heading six element with font medium and text Excel rendering the title.
And then down here we check if we have the badge passed because badge is optional, it's not required. So in here if we have the badge we render the badge and give it a class name cnPricingCardBadgeVariants and pass in the variant and render the badge text inside. You probably don't have this error. This is just the TypeScript server shutting down. Now outside of this div, which is encapsulating our badge and the title, let's go back inside of this div, and let's add an element below it, which will be our description in a paragraph.
So this right here. This paragraph will have a default class name of TextExtraSmall and then pricingCardSecondaryTextVariant. And passing the variant here and render the description inside. And then we're going to go outside of this div and we're going to get to the body of our pricing card. In here we will format the price.
So let's add a div with flex items and shrinks zero and gap x 0.5 and then we're going to add an h4 element here which will render the current price and format it. So an h4 element with class name text3.excel, font medium, and then we are going to use the native JavaScript intl API and request a number format in NUS style currency with currency US dollar and the minimum fraction digits set to zero and we are simply going to format the price which is a type of number and a required prop. And then below it we're going to add a span element, which will render the price suffix, which will be either per month, per year, or something like that. And we can style it with class name pricingCardSecondaryTextVariance. So we already, I think, used that for the description.
So we are using it the same way here. Great. Now, outside of these two divs, but still inside of our pricing div, let's add a separator. We are going to encapsulate the separator with a py6 div, and a class name, Opacity 10, and text, and this specific color right here. And then let's go ahead and let's add a button which will be our call to action.
So render the button text inside which is a prop. And now let's go ahead and give it some attributes class name full size large and let's go ahead and let's give it a variant which will be if variant is equal to highlighted use default otherwise use outline and on click will simply be the on click prop there we go and now outside of this button we're going to render a div which will hold our last elements, which are the features. So flex-column, gap-wide-2, and margin-top of 6. So inside of here, let's add a paragraph, which will simply say features in medium font and uppercase. And then let's create an unordered list.
So this unordered list will have a class name of flex, flex column, and gap y 2.5, and then pricing card secondary text variants which we've already used two times here and we're using it in the exact same way here as well and in here we are very simply going to iterate over our features Let me just properly add this like this. So features are optional, right? Actually they're not optional, so you can do this. My apologies. Go ahead and put this.
Features.map, feature index, list item with an index as the key element, ClassName with flex item center and gap x 2.5, CircleCheckIcon with pricing card icon variants, and rendering of that feature. That's it. Let's see what we haven't used yet. So let me just see in here. I think I'm doing something incorrectly.
Let me just check. So let me just check this. Let's try reloading the window. Is that the issue? Am I not seeing here?
I think I'm doing some incorrect parsing here so let's see this am I closing this tag anywhere I am am I closing the return tag I am closing that too So I think I have something opened here, but I'm not exactly sure what. Oh, it's the question mark. My apologies. So this will be not feature. This is features, I think.
Is it? Let me go inside of the props here. Feature, I think this should be called features. So let's call this features and let's remove the question mark here. Let's call this features here.
And then down here when I iterate over the features, I think I can just use it like this without the question mark. Great, that is our pricing card. So now let's go back to the upgrade view and what we can do now is go below the heading 5 and we can open up a div here with grid, grid columns 1, on medium grid columns 3 and gap 4 And we can go over our products. So products.map, get the individual product here. And then what we're going to do is the following.
We are first going to check if is current product using current subscription, question mark ID with the product ID. So we are checking if we already have this product purchased. Then we are checking if we actually have any kind of subscription model. And then we are going to define the button text which by default will be upgrade. And the onClick method will by default be AuthClient, which we'll call checkout.
And simply add products, product ID here. So it's that easy to open a checkout. We can use it through AuthClient here because in here we added the PolarClient plugin. So that easy for us to open a checkout page. But now we're going to add some if clauses.
So if we are looking at the current product, the button text here will be manage and on click will instead open customer portal. Else if is premium. So if we are not looking at the current product, but we are looking at a premium product, we will simply add change plan text, which will also open the portal because that's where you can actually change the plans. And in here, let's go ahead and let's return the pricing card component. You can import the pricing card component from here and then let's go ahead and let's just give it all the props.
So let's give it key of product id, let's go ahead and Let's give it button text of button text, onClick to be onClick. Now the variant will be controlled by product metadata variant. And if we explicitly say in the Polar dashboard that this should be highlighted, we will use the highlighted variant for this card. Let's go ahead and give it a title to be product name and let's go ahead and give it a price. So we go inside of this individual product, first price in the prices array and check if the amount type is fixed.
If it is, we go into that price amount and we divide it by a hundred. Because if you tried just doing this, price amount by default is a type of any. So you have to check that the price that you're trying to get is actually fixed because Pauler has options for pay as you want and free. So that's why price amount is only a number if you first check that the amount type is fixed and you have to divide it by 100 because they store the price in cents. Then let's go ahead and let's add product description and let's add product price suffix here by opening backticks, add a forward slash and then access product prices first in the array and then just recurring interval.
And then let's go ahead and let's load the features which we're going to use benefits for. So product.benefits.map and simply load the description of each benefit. And then let's go ahead and let's add a badge here as product.metadata.badge. There we go. And that is actually it for our pricing card component.
What we have to do now is we have to actually create some products. So let's go back inside of our sandbox and let's go inside of products here. So the first product I'm going to add is going to be a monthly product. And the description will be for teams getting started. My pricing will be monthly and it's gonna be a fixed price and then just put whatever you want for example $29 and then I'm gonna go ahead and I will add let's go instead of the metadata here and I have to add some automated benefits here.
So we're going to click custom and click create new. And then in here, go ahead and create a few benefits so actually let's not do it like this let's just create this very basic product now so click create product and now you have the monthly product so right now if you try I'm not sure if it can load or not. There we go. So it loads but not as perfectly. And it's missing something here.
So let me go inside of my pricing card. Okay, this should be Features. Let's refresh. So we are going to showcase this with Features in a second. Now, what we have to do is we have to go inside of here and click on Benefits here.
It's easier to create them from here. And now the first one will be unlimited agents like that and give it a type of custom here and click create. Then let's go ahead and let's create another benefit with Unlimited meetings and create. And now let's go inside of our products here, inside of monthly, and find the automated benefits section, open up custom and simply enable Unlimited meetings and unlimited agents and click Save product. And all existing ones will now see this too.
And there we go. Unlimited meetings and unlimited agents right here. And you can, of course, add as many of these benefits as you want. You can see that in here I added unlimited transcripts, unlimited recording storage, right. And now let's go ahead and let's add the yearly product.
So I will add a new product here and I will add yearly here. And this one will be a yearly subscription, fixed price, and I will put $259 a year here. And what I'm going to do now is I'm going to go inside of metadata and I will give it a key of variant and I will give it a highlighted value. So if you take a look at our code in the upgrade view, if product metadata variant is highlighted, we highlight that product. So make sure to not misspell this.
And I will also add a badge here to be best value like this. So now you can see that when I go into my badge, I'm reading metadata badge. So make sure to add those. And let's enable unlimited meetings, unlimited agents, and let's go ahead and let's also add a new benefit, two months free. Because we are technically giving them two months free if you do the math of monthly subscription and yearly subscription.
So now if you refresh this, oh, did I save it? I did not create the product. So let's create the product here. There we go. Let's refresh.
And you should now see, there we go, the highlighted product with best value text here. Perfect. And let's go ahead and add one more simply so you can see what else you can do. So this one, oh yeah, and one thing I forgot to do for yearly was to add a description. So let me just add for themes that need to scale, and let me just go ahead and save the product.
And now you should see the description here as well. There we go. And now let's go ahead and add one more, which will be called Enterprise. So this I'm doing just for fun, simply so you can see one cool feature that they have. So let's add for teams with special requests.
It's going to be yearly only fixed price and let's put a thousand dollars a year. And what we can now do here is we can enable two months free unlimited meeting unlimited agents and you can also add dedicated Discord support like that. And you could actually set this to be a Discord invite and then actually connect your Discord server and then add the actual role. So while they are subscribed, they will have access to your Discord. So just for now, I'm going to set custom, but the point is to actually put Discord invite here and connect it.
And then whoever subscribes to $1, 000 a year will have access from your group, right? So I will now save this as is. I will refresh. And there we go. Perfect.
So now Let's try and upgrade. So right now, I am on the free plan. And you can see that in here, when I go into Customers, John Doe, currently zero revenue here. I don't have any active subscriptions. But if I go ahead and upgrade, for example, for yearly here, there we go.
I'm just going to go ahead and add. You can add the same dummy information like you do in Stripe. So this is a test card which always succeeds. Let's go ahead and let's select a country and once you select a country, additional tax will be calculated here and let's click subscribe. And once you do that, you don't have to wait for any webhooks.
You don't have to do absolutely anything. The reason you don't have to is because we added the external customer ID, which basically tells us now that we are on the yearly plan and did you notice we no longer have the free usage we can now create as many agents as we want let's go ahead and let's try and create another agent here no error we are able to create that agent. And from here, we can see the text change plan, which takes us where to the customer portal. And from here, we can actually change plan to anything else. And you can see all the grants that we have.
Perfect, So it's that easy to do all of this with Polar and BetterOut. And let's also enable the billing button here as well. So all we have to do is go inside of the dashboard user button. We already have the OutClient import, so we can go down here to the billing and simply do OutClient Customer Portal, like that. And that's it.
Click on the billing, and that will redirect you to the portal. At least it should. Did I do it correctly? So let's try it again. Let's wait a few seconds.
Oh, the reason I think it's not working because I'm doing this in the drawer. I forgot that we do if is mobile. So yes, you have to do it both for mobile on click and in the drop down here. Whoops. So let's just add it here.
Drop down menu item. And now we can confidently try it. Let's click on billing. And this should now open the billing. Let's refresh first.
Oh, okay, it did it. It just took a while. Perfect. Amazing. That's it.
Our app now has billing our app now has absolutely everything. Amazing, amazing job. So let's go ahead now and let's mark all of these as completed. We added Polar SDK. This, this, this, this, and this.
Now let's go ahead and merge this. So I'm gonna go ahead and create a new branch called 27 payments. I'm going to stage all of these changes. 27 payments. Let's commit.
Let's publish the branch. Now let's go ahead and open our GitHub, let's create a pull request and let's review it. And here we have the summary. So we introduced a subscription upgrade page with detailed pricing options and upgrade workflow. We added a sidebar component showing free trial usage and a prompt to upgrade.
We implemented a premium feature gating for creating agents and meetings, with clear upgrade prompts if limits are reached. We added billing portal access from the dashboard and we displayed current subscription status and available plans in the upgrade screen. The free usage limits for agents and meetings are now enforced and visually tracked by the sidebar component. Usage data and trial status refresh automatically after relevant actions such as creating and deleting meetings and agents. And that is actually the cache invalidation here, which updates the UI.
So exactly what I just mentioned. Perfect. So in here, we have amazing diagram, if you need visual explanation of what we just implemented here. So it's so amazing how CodeRabbit constantly has context of everything we do and just builds on top of that. So this is a very, very nice way to summarize what we just did using these visual graphs right here.
And I reviewed the comments here. Most of these are just refactoring issues. And some of it is just older documentation such as this. We do have one potential issue, but I use this completely normally inside of my app so I don't think any problems will arise from this. Same thing with this, it's the same issue.
In here, yeah, this is a little messed up. We could probably improve this somehow, but as long as you created your products exactly as I did you won't have any issues here so let's go ahead and let's merge this all requests and let's go ahead and go back to our main branch and let's go ahead and synchronize the changes. After that, we are going to see 27 payments merged into our app. Perfect! Amazing job and see you in the next chapter.