mirror of
https://github.com/LukeHagar/polar.git
synced 2025-12-10 20:57:46 +00:00
working checkouts
This commit is contained in:
7
example/convex/_generated/api.d.ts
vendored
7
example/convex/_generated/api.d.ts
vendored
@@ -96,7 +96,6 @@ export declare const components: {
|
|||||||
"mutation",
|
"mutation",
|
||||||
"internal",
|
"internal",
|
||||||
{
|
{
|
||||||
callback?: string;
|
|
||||||
subscription: {
|
subscription: {
|
||||||
amount: number | null;
|
amount: number | null;
|
||||||
cancelAtPeriodEnd: boolean;
|
cancelAtPeriodEnd: boolean;
|
||||||
@@ -204,6 +203,12 @@ export declare const components: {
|
|||||||
userId: string;
|
userId: string;
|
||||||
} | null
|
} | null
|
||||||
>;
|
>;
|
||||||
|
insertCustomer: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ id: string; userId: string },
|
||||||
|
string
|
||||||
|
>;
|
||||||
listProducts: FunctionReference<
|
listProducts: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
"internal",
|
"internal",
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ export const MAX_FREE_TODOS = 3;
|
|||||||
export const MAX_PREMIUM_TODOS = 6;
|
export const MAX_PREMIUM_TODOS = 6;
|
||||||
|
|
||||||
export const { generateCheckoutLink } = polar.checkoutApi({
|
export const { generateCheckoutLink } = polar.checkoutApi({
|
||||||
|
products: {
|
||||||
|
premium: "5fde8344-5fca-4d0b-adeb-2052cddfd9ed",
|
||||||
|
premiumPlus: "db548a6f-ff8c-4969-8f02-5f7301a36e7c",
|
||||||
|
},
|
||||||
getUserInfo: async (ctx) => {
|
getUserInfo: async (ctx) => {
|
||||||
const user = await ctx.runQuery(api.example.getCurrentUser);
|
const user = await ctx.runQuery(api.example.getCurrentUser);
|
||||||
return {
|
return {
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
|
email: user.email,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ const seed = internalAction({
|
|||||||
recurringInterval: "month",
|
recurringInterval: "month",
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
|
amountType: "fixed",
|
||||||
priceAmount: 1000,
|
priceAmount: 1000,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -68,6 +69,7 @@ const seed = internalAction({
|
|||||||
recurringInterval: "year",
|
recurringInterval: "year",
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
|
amountType: "fixed",
|
||||||
priceAmount: 10000,
|
priceAmount: 10000,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -78,6 +80,7 @@ const seed = internalAction({
|
|||||||
recurringInterval: "month",
|
recurringInterval: "month",
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
|
amountType: "fixed",
|
||||||
priceAmount: 2000,
|
priceAmount: 2000,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -88,6 +91,7 @@ const seed = internalAction({
|
|||||||
recurringInterval: "year",
|
recurringInterval: "year",
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
|
amountType: "fixed",
|
||||||
priceAmount: 20000,
|
priceAmount: 20000,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -74,10 +74,7 @@ export function UpgradeCTA({
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{!isPremium && !isPremiumPlus && (
|
{!isPremium && !isPremiumPlus && (
|
||||||
<CheckoutLink
|
<CheckoutLink polarApi={api.example} productKey="premium">
|
||||||
polarApi={api.example}
|
|
||||||
productId="2d368710-520b-49b0-ba7b-9c2d60d7b1c2"
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
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"
|
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"
|
||||||
@@ -142,10 +139,7 @@ export function UpgradeCTA({
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{!isPremiumPlus && (
|
{!isPremiumPlus && (
|
||||||
<CheckoutLink
|
<CheckoutLink polarApi={api.example} productKey="premiumPlus">
|
||||||
polarApi={api.example}
|
|
||||||
productId="premium-plus-product-id"
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
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"
|
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"
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import "./polyfill";
|
|||||||
import {
|
import {
|
||||||
ApiFromModules,
|
ApiFromModules,
|
||||||
FunctionReference,
|
FunctionReference,
|
||||||
GenericActionCtx,
|
|
||||||
GenericDataModel,
|
GenericDataModel,
|
||||||
type HttpRouter,
|
type HttpRouter,
|
||||||
WithoutSystemFields,
|
|
||||||
actionGeneric,
|
actionGeneric,
|
||||||
createFunctionHandle,
|
|
||||||
httpActionGeneric,
|
httpActionGeneric,
|
||||||
|
GenericActionCtx,
|
||||||
} from "convex/server";
|
} from "convex/server";
|
||||||
import {
|
import {
|
||||||
type ComponentApi,
|
type ComponentApi,
|
||||||
@@ -18,17 +16,18 @@ import {
|
|||||||
RunQueryCtx,
|
RunQueryCtx,
|
||||||
} from "../component/util";
|
} from "../component/util";
|
||||||
import { Polar as PolarSdk } from "@polar-sh/sdk";
|
import { Polar as PolarSdk } from "@polar-sh/sdk";
|
||||||
import { Doc } from "../component/_generated/dataModel";
|
|
||||||
import { Infer, v } from "convex/values";
|
import { Infer, v } from "convex/values";
|
||||||
import schema from "../component/schema";
|
import schema from "../component/schema";
|
||||||
import { WebhookSubscriptionCreatedPayload } from "@polar-sh/sdk/models/components/webhooksubscriptioncreatedpayload.js";
|
import { WebhookSubscriptionCreatedPayload } from "@polar-sh/sdk/models/components/webhooksubscriptioncreatedpayload.js";
|
||||||
import { WebhookSubscriptionUpdatedPayload } from "@polar-sh/sdk/models/components/webhooksubscriptionupdatedpayload.js";
|
import { WebhookSubscriptionUpdatedPayload } from "@polar-sh/sdk/models/components/webhooksubscriptionupdatedpayload.js";
|
||||||
import { WebhookProductCreatedPayload } from "@polar-sh/sdk/models/components/webhookproductcreatedpayload.js";
|
import { WebhookProductCreatedPayload } from "@polar-sh/sdk/models/components/webhookproductcreatedpayload.js";
|
||||||
import { WebhookProductUpdatedPayload } from "@polar-sh/sdk/models/components/webhookproductupdatedpayload.js";
|
import { WebhookProductUpdatedPayload } from "@polar-sh/sdk/models/components/webhookproductupdatedpayload.js";
|
||||||
|
import { Checkout } from "@polar-sh/sdk/models/components/checkout.js";
|
||||||
import {
|
import {
|
||||||
validateEvent,
|
validateEvent,
|
||||||
WebhookVerificationError,
|
WebhookVerificationError,
|
||||||
} from "@polar-sh/sdk/webhooks";
|
} from "@polar-sh/sdk/webhooks";
|
||||||
|
import { EmptyObject } from "convex-helpers";
|
||||||
|
|
||||||
export const subscriptionValidator = schema.tables.subscriptions.validator;
|
export const subscriptionValidator = schema.tables.subscriptions.validator;
|
||||||
export type Subscription = Infer<typeof subscriptionValidator>;
|
export type Subscription = Infer<typeof subscriptionValidator>;
|
||||||
@@ -45,14 +44,14 @@ export type CheckoutApi<DataModel extends GenericDataModel> = ApiFromModules<{
|
|||||||
|
|
||||||
export class Polar<DataModel extends GenericDataModel> {
|
export class Polar<DataModel extends GenericDataModel> {
|
||||||
private polar: PolarSdk;
|
private polar: PolarSdk;
|
||||||
public onScheduleCreated?: SubscriptionHandler;
|
|
||||||
constructor(
|
constructor(
|
||||||
public component: ComponentApi,
|
public component: ComponentApi,
|
||||||
options: {
|
private options: {
|
||||||
onScheduleCreated?: FunctionReference<
|
getUserInfo?: FunctionReference<
|
||||||
"mutation",
|
"query",
|
||||||
"internal",
|
"internal",
|
||||||
{ subscription: WithoutSystemFields<Doc<"subscriptions">> }
|
EmptyObject,
|
||||||
|
{ userId: string; email?: string }
|
||||||
>;
|
>;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
@@ -61,41 +60,48 @@ export class Polar<DataModel extends GenericDataModel> {
|
|||||||
server:
|
server:
|
||||||
(process.env["POLAR_SERVER"] as "sandbox" | "production") ?? "sandbox",
|
(process.env["POLAR_SERVER"] as "sandbox" | "production") ?? "sandbox",
|
||||||
});
|
});
|
||||||
this.onScheduleCreated = options.onScheduleCreated;
|
|
||||||
}
|
}
|
||||||
getCustomerByUserId(ctx: RunQueryCtx, userId: string) {
|
getCustomerByUserId(ctx: RunQueryCtx, userId: string) {
|
||||||
return ctx.runQuery(this.component.lib.getCustomerByUserId, { userId });
|
return ctx.runQuery(this.component.lib.getCustomerByUserId, { userId });
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
async createCheckoutSession(
|
async createCheckoutSession(
|
||||||
ctx: GenericActionCtx<DataModel>,
|
ctx: GenericActionCtx<DataModel>,
|
||||||
{
|
{
|
||||||
productId,
|
productId,
|
||||||
userId,
|
userId,
|
||||||
email,
|
email,
|
||||||
}: { productId: string; userId: string; email: string }
|
origin,
|
||||||
) {
|
}: { productId: string; userId: string; email: string; origin: string }
|
||||||
const customer =
|
): Promise<Checkout> {
|
||||||
(await ctx.runQuery(this.component.lib.getCustomerByUserId, {
|
const dbCustomer = await ctx.runQuery(
|
||||||
|
this.component.lib.getCustomerByUserId,
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
})) ||
|
}
|
||||||
(await this.polar.customers.create({
|
);
|
||||||
email,
|
const customerId =
|
||||||
metadata: {
|
dbCustomer?.id ||
|
||||||
userId,
|
(
|
||||||
},
|
await this.polar.customers.create({
|
||||||
}));
|
email,
|
||||||
const customerId = customer?.id;
|
metadata: {
|
||||||
if (!customerId) {
|
userId,
|
||||||
throw new Error("Customer not found");
|
},
|
||||||
|
})
|
||||||
|
).id;
|
||||||
|
if (!dbCustomer) {
|
||||||
|
await ctx.runMutation(this.component.lib.insertCustomer, {
|
||||||
|
id: customerId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return this.polar.checkouts.custom.create({
|
return this.polar.checkouts.create({
|
||||||
allowDiscountCodes: true,
|
allowDiscountCodes: true,
|
||||||
productId,
|
products: [productId],
|
||||||
customerId,
|
customerId,
|
||||||
|
embedOrigin: origin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
listProducts(
|
listProducts(
|
||||||
ctx: RunQueryCtx,
|
ctx: RunQueryCtx,
|
||||||
{ includeArchived }: { includeArchived: boolean }
|
{ includeArchived }: { includeArchived: boolean }
|
||||||
@@ -117,15 +123,16 @@ export class Polar<DataModel extends GenericDataModel> {
|
|||||||
return ctx.runQuery(this.component.lib.getProduct, { id: productId });
|
return ctx.runQuery(this.component.lib.getProduct, { id: productId });
|
||||||
}
|
}
|
||||||
checkoutApi(opts: {
|
checkoutApi(opts: {
|
||||||
|
products: Record<string, string>;
|
||||||
getUserInfo: (ctx: RunQueryCtx) => Promise<{
|
getUserInfo: (ctx: RunQueryCtx) => Promise<{
|
||||||
userId: string;
|
userId: string;
|
||||||
email?: string;
|
email: string;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
generateCheckoutLink: actionGeneric({
|
generateCheckoutLink: actionGeneric({
|
||||||
args: {
|
args: {
|
||||||
productId: v.string(),
|
productKey: v.string(),
|
||||||
origin: v.string(),
|
origin: v.string(),
|
||||||
},
|
},
|
||||||
returns: v.object({
|
returns: v.object({
|
||||||
@@ -133,17 +140,13 @@ export class Polar<DataModel extends GenericDataModel> {
|
|||||||
}),
|
}),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const { userId, email } = await opts.getUserInfo(ctx);
|
const { userId, email } = await opts.getUserInfo(ctx);
|
||||||
const { url } = await this.polar.checkouts.create({
|
const { url } = await this.createCheckoutSession(ctx, {
|
||||||
productId: args.productId,
|
productId: opts.products?.[args.productKey],
|
||||||
embedOrigin: args.origin,
|
userId,
|
||||||
customerEmail: email,
|
email,
|
||||||
metadata: {
|
origin: args.origin,
|
||||||
userId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
return {
|
return { url };
|
||||||
url,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -194,9 +197,6 @@ export class Polar<DataModel extends GenericDataModel> {
|
|||||||
event.data.metadata.userId as string,
|
event.data.metadata.userId as string,
|
||||||
event.data
|
event.data
|
||||||
),
|
),
|
||||||
callback:
|
|
||||||
this.onScheduleCreated &&
|
|
||||||
(await createFunctionHandle(this.onScheduleCreated)),
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/component/_generated/api.d.ts
vendored
7
src/component/_generated/api.d.ts
vendored
@@ -82,7 +82,6 @@ export type Mounts = {
|
|||||||
"mutation",
|
"mutation",
|
||||||
"public",
|
"public",
|
||||||
{
|
{
|
||||||
callback?: string;
|
|
||||||
subscription: {
|
subscription: {
|
||||||
amount: number | null;
|
amount: number | null;
|
||||||
cancelAtPeriodEnd: boolean;
|
cancelAtPeriodEnd: boolean;
|
||||||
@@ -185,6 +184,12 @@ export type Mounts = {
|
|||||||
userId: string;
|
userId: string;
|
||||||
} | null
|
} | null
|
||||||
>;
|
>;
|
||||||
|
insertCustomer: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"public",
|
||||||
|
{ id: string; userId: string },
|
||||||
|
string
|
||||||
|
>;
|
||||||
listProducts: FunctionReference<
|
listProducts: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
"public",
|
"public",
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { v, VString } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { mutation, query } from "./_generated/server";
|
import { mutation, query } from "./_generated/server";
|
||||||
import schema from "./schema";
|
import schema from "./schema";
|
||||||
import { asyncMap } from "convex-helpers";
|
import { asyncMap } from "convex-helpers";
|
||||||
import { FunctionHandle, WithoutSystemFields } from "convex/server";
|
|
||||||
import { Doc } from "./_generated/dataModel";
|
|
||||||
|
|
||||||
export const getCustomerByUserId = query({
|
export const getCustomerByUserId = query({
|
||||||
args: {
|
args: {
|
||||||
@@ -25,6 +23,20 @@ export const getCustomerByUserId = query({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const insertCustomer = mutation({
|
||||||
|
args: {
|
||||||
|
id: v.string(),
|
||||||
|
userId: v.string(),
|
||||||
|
},
|
||||||
|
returns: v.id("customers"),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
return ctx.db.insert("customers", {
|
||||||
|
id: args.id,
|
||||||
|
userId: args.userId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const upsertCustomer = mutation({
|
export const upsertCustomer = mutation({
|
||||||
args: {
|
args: {
|
||||||
userId: v.string(),
|
userId: v.string(),
|
||||||
@@ -163,21 +175,12 @@ export const listProducts = query({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
type Subscription = WithoutSystemFields<Doc<"subscriptions">>;
|
|
||||||
const subscriptionCallbackValidator = v.string() as VString<
|
|
||||||
FunctionHandle<"mutation", { subscription: Subscription }>
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const createSubscription = mutation({
|
export const createSubscription = mutation({
|
||||||
args: {
|
args: {
|
||||||
subscription: schema.tables.subscriptions.validator,
|
subscription: schema.tables.subscriptions.validator,
|
||||||
callback: v.optional(subscriptionCallbackValidator),
|
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
await ctx.db.insert("subscriptions", args.subscription);
|
await ctx.db.insert("subscriptions", args.subscription);
|
||||||
if (args.callback) {
|
|
||||||
await ctx.runMutation(args.callback, { subscription: args.subscription });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { useAction } from "convex/react";
|
|||||||
|
|
||||||
export const CheckoutLink = <DataModel extends GenericDataModel>({
|
export const CheckoutLink = <DataModel extends GenericDataModel>({
|
||||||
polarApi,
|
polarApi,
|
||||||
productId,
|
productKey,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: PropsWithChildren<{
|
}: PropsWithChildren<{
|
||||||
polarApi: CheckoutApi<DataModel>;
|
polarApi: CheckoutApi<DataModel>;
|
||||||
productId: string;
|
productKey: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}>) => {
|
}>) => {
|
||||||
const generateCheckoutLink = useAction(polarApi.generateCheckoutLink);
|
const generateCheckoutLink = useAction(polarApi.generateCheckoutLink);
|
||||||
@@ -20,7 +20,7 @@ export const CheckoutLink = <DataModel extends GenericDataModel>({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
PolarEmbedCheckout.init();
|
PolarEmbedCheckout.init();
|
||||||
void generateCheckoutLink({
|
void generateCheckoutLink({
|
||||||
productId,
|
productKey,
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
}).then(({ url }) => setCheckoutLink(url));
|
}).then(({ url }) => setCheckoutLink(url));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -30,7 +30,7 @@ export const CheckoutLink = <DataModel extends GenericDataModel>({
|
|||||||
className={className}
|
className={className}
|
||||||
href={checkoutLink}
|
href={checkoutLink}
|
||||||
data-polar-checkout
|
data-polar-checkout
|
||||||
//data-polar-checkout-theme="light"
|
data-polar-checkout-theme="dark"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user