feat: add new subscription tier and update credit configurations

- Introduced a new "max" subscription tier with associated monthly credits and top-up limits.
- Updated existing subscription tiers' monthly credits and top-up limits for "starter", "pro", and "business".
- Enhanced credit display and overview components to reflect the new tier and its attributes.
- Integrated Polar authentication features for improved subscription management and credit handling.
This commit is contained in:
Matthias
2026-03-27 09:47:44 +01:00
parent 0bc4785850
commit cf3a338b9f
16 changed files with 694 additions and 22 deletions

View File

@@ -1,6 +1,10 @@
import { createClient, type GenericCtx } from "@convex-dev/better-auth";
import { convex } from "@convex-dev/better-auth/plugins";
import { requireRunMutationCtx } from "@convex-dev/better-auth/utils";
import { checkout, polar, portal, webhooks } from "@polar-sh/better-auth";
import { Polar } from "@polar-sh/sdk";
import { components } from "./_generated/api";
import { internal } from "./_generated/api";
import { DataModel } from "./_generated/dataModel";
import { query } from "./_generated/server";
import { betterAuth } from "better-auth/minimal";
@@ -10,6 +14,11 @@ import authConfig from "./auth.config";
const siteUrl = process.env.SITE_URL!;
const appUrl = process.env.APP_URL;
const polarClient = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN!,
server: "production",
});
// Component Client — stellt Adapter, Helper und Auth-Methoden bereit
export const authComponent = createClient<DataModel>(components.betterAuth);
@@ -68,6 +77,92 @@ export const createAuth = (ctx: GenericCtx<DataModel>) => {
},
plugins: [
convex({ authConfig }),
polar({
client: polarClient,
createCustomerOnSignUp: true,
use: [
checkout({
successUrl: `${siteUrl}/dashboard?checkout=success`,
authenticatedUsersOnly: true,
}),
portal(),
webhooks({
secret: process.env.POLAR_WEBHOOK_SECRET!,
onCustomerStateChanged: async (payload) => {
const runMutationCtx = requireRunMutationCtx(ctx);
const customerState = payload.data;
const userId = customerState.externalId;
if (!userId) {
console.error("Polar customer.state_changed payload without externalId", {
customerId: customerState.id,
});
return;
}
const subscription = customerState.activeSubscriptions?.[0];
if (!subscription) {
await runMutationCtx.runMutation(internal.polar.handleSubscriptionRevoked, {
userId,
});
return;
}
const tierMetadata = subscription.metadata.tier;
const creditsMetadata = subscription.metadata.credits;
const tier = tierMetadata === "starter" || tierMetadata === "pro" || tierMetadata === "max"
? tierMetadata
: undefined;
const monthlyCredits = Number(creditsMetadata);
if (!tier || !Number.isFinite(monthlyCredits) || monthlyCredits <= 0) {
console.error("Missing or invalid Polar subscription metadata", {
subscriptionId: subscription.id,
tier: tierMetadata,
credits: creditsMetadata,
});
return;
}
await runMutationCtx.runMutation(internal.polar.handleSubscriptionActivated, {
userId,
tier,
polarSubscriptionId: subscription.id,
currentPeriodStart: subscription.currentPeriodStart.getTime(),
currentPeriodEnd: subscription.currentPeriodEnd.getTime(),
monthlyCredits,
});
},
onOrderPaid: async (payload) => {
const runMutationCtx = requireRunMutationCtx(ctx);
const order = payload.data;
const metadata = order.product?.metadata;
const type = metadata?.type;
const credits = Number(metadata?.credits);
if (type !== "topup" || !Number.isFinite(credits) || credits <= 0) {
return;
}
const userId = order.customer.externalId;
if (!userId) {
console.error("Polar order.paid payload without externalId", {
orderId: order.id,
});
return;
}
await runMutationCtx.runMutation(internal.polar.handleTopUpPaid, {
userId,
credits,
polarOrderId: order.id,
amountPaidEuroCents: order.totalAmount,
});
},
}),
],
}),
],
});
};