mirror of
https://github.com/LukeHagar/polar.git
synced 2025-12-10 12:47:47 +00:00
support upgrade, downgrade, cancel
This commit is contained in:
@@ -10,7 +10,8 @@ export const polar = new Polar<DataModel>(components.polar);
|
||||
export const MAX_FREE_TODOS = 3;
|
||||
export const MAX_PREMIUM_TODOS = 6;
|
||||
|
||||
export const { generateCheckoutLink } = polar.checkoutApi({
|
||||
export const { generateCheckoutLink, generateCustomerPortalUrl } =
|
||||
polar.checkoutApi({
|
||||
products: {
|
||||
premium: "5fde8344-5fca-4d0b-adeb-2052cddfd9ed",
|
||||
premiumPlus: "db548a6f-ff8c-4969-8f02-5f7301a36e7c",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowRight, Check, Star, Settings } from "lucide-react";
|
||||
import { CheckoutLink } from "../../src/react";
|
||||
import { CheckoutLink, CustomerPortalLink } from "../../src/react";
|
||||
import { api } from "../convex/_generated/api";
|
||||
|
||||
export function UpgradeCTA({
|
||||
@@ -147,23 +147,24 @@ export function UpgradeCTA({
|
||||
className="w-full text-gray-600 hover:text-indigo-700 dark:text-gray-400 dark:hover:text-indigo-300"
|
||||
asChild
|
||||
>
|
||||
<a href="#">
|
||||
<CustomerPortalLink polarApi={api.example}>
|
||||
Manage Subscription <Settings className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</CustomerPortalLink>
|
||||
</Button>
|
||||
)}
|
||||
{!isPremium && !isPremiumPlus && (
|
||||
<CheckoutLink polarApi={api.example} productKey="premium">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full bg-white text-indigo-700 hover:bg-gray-100 dark:bg-gray-800 dark:text-indigo-300 dark:hover:bg-gray-700"
|
||||
asChild
|
||||
>
|
||||
<CheckoutLink polarApi={api.example} productKey="premium">
|
||||
Upgrade to Premium{" "}
|
||||
<div className="ml-2">
|
||||
<ArrowRight size={16} />
|
||||
</div>
|
||||
</Button>
|
||||
</CheckoutLink>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -258,23 +259,24 @@ export function UpgradeCTA({
|
||||
className="w-full text-gray-600 hover:text-purple-700 dark:text-gray-400 dark:hover:text-purple-300"
|
||||
asChild
|
||||
>
|
||||
<a href="#">
|
||||
<CustomerPortalLink polarApi={api.example}>
|
||||
Manage Subscription <Settings className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</CustomerPortalLink>
|
||||
</Button>
|
||||
)}
|
||||
{!isPremiumPlus && (
|
||||
<CheckoutLink polarApi={api.example} productKey="premiumPlus">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full bg-white/95 backdrop-blur-sm text-purple-700 hover:bg-white dark:bg-white/10 dark:text-purple-200 dark:hover:bg-white/20"
|
||||
asChild
|
||||
>
|
||||
<CheckoutLink polarApi={api.example} productKey="premiumPlus">
|
||||
Upgrade to Premium Plus{" "}
|
||||
<div className="ml-2">
|
||||
<ArrowRight size={16} />
|
||||
</div>
|
||||
</Button>
|
||||
</CheckoutLink>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { WebhookSubscriptionUpdatedPayload } from "@polar-sh/sdk/models/componen
|
||||
import { WebhookProductCreatedPayload } from "@polar-sh/sdk/models/components/webhookproductcreatedpayload.js";
|
||||
import { WebhookProductUpdatedPayload } from "@polar-sh/sdk/models/components/webhookproductupdatedpayload.js";
|
||||
import { Checkout } from "@polar-sh/sdk/models/components/checkout.js";
|
||||
import { CustomerSession } from "@polar-sh/sdk/models/components/customersession.js";
|
||||
import {
|
||||
validateEvent,
|
||||
WebhookVerificationError,
|
||||
@@ -122,6 +123,14 @@ export class Polar<DataModel extends GenericDataModel> {
|
||||
getProduct(ctx: RunQueryCtx, { productId }: { productId: string }) {
|
||||
return ctx.runQuery(this.component.lib.getProduct, { id: productId });
|
||||
}
|
||||
createCustomerPortalSession(
|
||||
ctx: GenericActionCtx<DataModel>,
|
||||
{ customerId }: { customerId: string }
|
||||
): Promise<CustomerSession> {
|
||||
return this.polar.customerSessions.create({
|
||||
customerId,
|
||||
});
|
||||
}
|
||||
checkoutApi(opts: {
|
||||
products: Record<string, string>;
|
||||
getUserInfo: (ctx: RunQueryCtx) => Promise<{
|
||||
@@ -149,6 +158,27 @@ export class Polar<DataModel extends GenericDataModel> {
|
||||
return { url };
|
||||
},
|
||||
}),
|
||||
generateCustomerPortalUrl: actionGeneric({
|
||||
args: {},
|
||||
returns: v.union(v.object({ url: v.string() }), v.null()),
|
||||
handler: async (ctx) => {
|
||||
const { userId } = await opts.getUserInfo(ctx);
|
||||
const customer = await ctx.runQuery(
|
||||
this.component.lib.getCustomerByUserId,
|
||||
{ userId }
|
||||
);
|
||||
|
||||
if (!customer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const session = await this.createCustomerPortalSession(ctx, {
|
||||
customerId: customer.id,
|
||||
});
|
||||
|
||||
return { url: session.customerPortalUrl };
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
registerRoutes(
|
||||
|
||||
@@ -4,6 +4,38 @@ import { CheckoutApi } from "../client";
|
||||
import { GenericDataModel } from "convex/server";
|
||||
import { useAction } from "convex/react";
|
||||
|
||||
export const CustomerPortalLink = <DataModel extends GenericDataModel>({
|
||||
polarApi,
|
||||
children,
|
||||
className,
|
||||
}: PropsWithChildren<{
|
||||
polarApi: CheckoutApi<DataModel>;
|
||||
className?: string;
|
||||
}>) => {
|
||||
const generateCustomerPortalUrl = useAction(
|
||||
polarApi.generateCustomerPortalUrl
|
||||
);
|
||||
const [portalUrl, setPortalUrl] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
void generateCustomerPortalUrl({}).then((result) => {
|
||||
if (result) {
|
||||
setPortalUrl(result.url);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!portalUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a className={className} href={portalUrl} target="_blank">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const CheckoutLink = <DataModel extends GenericDataModel>({
|
||||
polarApi,
|
||||
productKey,
|
||||
|
||||
Reference in New Issue
Block a user