Files
polar/example/convex/example.ts
2025-03-27 15:41:32 -04:00

185 lines
5.2 KiB
TypeScript

import { Polar } from "@convex-dev/polar";
import { api, components } from "./_generated/api";
import { QueryCtx, mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { Id } from "./_generated/dataModel";
// User query to use in the Polar component
export const getUserInfo = query({
args: {},
handler: async (ctx) => {
// This would be replaced with an actual auth query,
// eg., ctx.auth.getUserIdentity() or getAuthUserId(ctx)
const user = await ctx.db.query("users").first();
if (!user) {
throw new Error("User not found");
}
return user;
},
});
export const polar = new Polar(components.polar, {
products: {
premiumMonthly: "5fde8344-5fca-4d0b-adeb-2052cddfd9ed",
premiumYearly: "9bc5ed5f-2065-40a4-bd1f-e012e448d82f",
premiumPlusMonthly: "db548a6f-ff8c-4969-8f02-5f7301a36e7c",
premiumPlusYearly: "9ff9976e-459e-4ebc-8cde-b2ced74f8822",
},
getUserInfo: async (ctx) => {
const user: { _id: Id<"users">; email: string } = await ctx.runQuery(
api.example.getUserInfo
);
return {
userId: user._id,
email: user.email,
};
},
// These can be configured in code or via environment variables
// Uncomment and replace with actual values to configure in code:
// organizationToken: "your_organization_token", // Or use POLAR_ORGANIZATION_TOKEN env var
// webhookSecret: "your_webhook_secret", // Or use POLAR_WEBHOOK_SECRET env var
// server: "sandbox", // "sandbox" or "production", falls back to POLAR_SERVER env var
});
export const MAX_FREE_TODOS = 3;
export const MAX_PREMIUM_TODOS = 6;
export const {
// If you configure your products by key in the Polar constructor,
// this query provides a keyed object of the products.
getConfiguredProducts,
// Lists all non-archived products, useful if you don't configure products by key.
listAllProducts,
// Generates a checkout link for the given product IDs.
generateCheckoutLink,
// Generates a customer portal URL for the current user.
generateCustomerPortalUrl,
// Changes the current subscription to the given product ID.
changeCurrentSubscription,
// Cancels the current subscription.
cancelCurrentSubscription,
} = polar.checkoutApi();
// In a real app you'll set up authentication, we just use a
// fake user for the example.
const currentUser = async (ctx: QueryCtx) => {
const user = await ctx.db.query("users").first();
if (!user) {
throw new Error("No user found");
}
const subscription = await polar.getCurrentSubscription(ctx, {
userId: user._id,
});
const productKey = subscription?.productKey;
const isPremium =
productKey === "premiumMonthly" || productKey === "premiumYearly";
const isPremiumPlus =
productKey === "premiumPlusMonthly" || productKey === "premiumPlusYearly";
return {
...user,
isFree: !isPremium && !isPremiumPlus,
isPremium,
isPremiumPlus,
subscription,
maxTodos: isPremiumPlus
? MAX_PREMIUM_TODOS
: isPremium
? MAX_PREMIUM_TODOS
: MAX_FREE_TODOS,
};
};
// Query that returns our pseudo user.
export const getCurrentUser = query({
handler: async (ctx) => {
return currentUser(ctx);
},
});
export const authorizeTodo = async (ctx: QueryCtx, todoId: Id<"todos">) => {
const user = await currentUser(ctx);
const todo = await ctx.db.get(todoId);
if (!todo || todo.userId !== user._id) {
throw new Error("Todo not found");
}
};
export const listTodos = query({
handler: async (ctx) => {
const user = await currentUser(ctx);
return ctx.db
.query("todos")
.withIndex("userId", (q) => q.eq("userId", user._id))
.collect();
},
});
export const insertTodo = mutation({
args: {
text: v.string(),
},
handler: async (ctx, args) => {
const user = await currentUser(ctx);
const todoCount = (
await ctx.db
.query("todos")
.withIndex("userId", (q) => q.eq("userId", user._id))
.collect()
).length;
const productKey = user.subscription?.productKey;
if (!productKey && todoCount >= MAX_FREE_TODOS) {
throw new Error("Reached maximum number of todos for free plan");
}
if (
(productKey === "premiumMonthly" ||
productKey === "premiumPlusMonthly") &&
todoCount >= MAX_PREMIUM_TODOS
) {
throw new Error("Reached maximum number of todos for premium plan");
}
await ctx.db.insert("todos", {
userId: user._id,
text: args.text,
completed: false,
});
},
});
export const updateTodoText = mutation({
args: {
todoId: v.id("todos"),
text: v.string(),
},
handler: async (ctx, args) => {
await authorizeTodo(ctx, args.todoId);
await ctx.db.patch(args.todoId, { text: args.text });
},
});
export const completeTodo = mutation({
args: {
todoId: v.id("todos"),
completed: v.boolean(),
},
handler: async (ctx, args) => {
await authorizeTodo(ctx, args.todoId);
await ctx.db.patch(args.todoId, { completed: args.completed });
},
});
export const deleteTodo = mutation({
args: {
todoId: v.id("todos"),
},
handler: async (ctx, args) => {
await authorizeTodo(ctx, args.todoId);
await ctx.db.delete(args.todoId);
},
});