feat: setup subscriptions
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "pnpm with-env next build",
|
||||
"clean": "git clean -xdf .cache .next .turbo node_modules",
|
||||
"dev": "pnpm with-env next dev | pino-pretty",
|
||||
"dev": "pnpm with-env next dev",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"start": "pnpm with-env next start",
|
||||
|
||||
@@ -42,7 +42,7 @@ export function InviteMemberForm() {
|
||||
|
||||
const refetchBoards = () => utils.board.all.refetch();
|
||||
|
||||
const createBoard = api.member.invite.useMutation({
|
||||
const inviteMember = api.member.invite.useMutation({
|
||||
onSuccess: async () => {
|
||||
closeModal();
|
||||
await utils.workspace.byId.refetch();
|
||||
@@ -68,8 +68,24 @@ export function InviteMemberForm() {
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: InviteMemberInput) => {
|
||||
createBoard.mutate(data);
|
||||
const onSubmit = async (_data: InviteMemberInput) => {
|
||||
const { data, error } = await authClient.subscription.upgrade({
|
||||
plan: "team",
|
||||
// subscriptionId: "sub_123",
|
||||
metadata: { userId: "123" },
|
||||
seats: 1,
|
||||
successUrl: "/members",
|
||||
cancelUrl: "/members",
|
||||
returnUrl: "/members",
|
||||
disableRedirect: true,
|
||||
});
|
||||
|
||||
if (data?.url) {
|
||||
window.location.href = data.url;
|
||||
}
|
||||
|
||||
// console.log(data, error);
|
||||
// inviteMember.mutate(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -117,7 +133,7 @@ export function InviteMemberForm() {
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={createBoard.isPending}
|
||||
isLoading={inviteMember.isPending}
|
||||
className="inline-flex w-full justify-center rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
||||
>
|
||||
{t`Invite member`}
|
||||
|
||||
@@ -31,5 +31,11 @@
|
||||
"turbo": "^2.3.1",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"prettier": "@kan/prettier-config"
|
||||
"prettier": "@kan/prettier-config",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@types/minimatch": "npm:minimatch@*",
|
||||
"@types/glob": "npm:@types/glob@^8.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,19 @@ export const memberRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NEXT_PUBLIC_KAN_ENV === "cloud") {
|
||||
const subscriptions = await ctx.auth.api.listActiveSubscriptions({
|
||||
userId,
|
||||
});
|
||||
|
||||
// get the active subscription
|
||||
const activeSubscription = subscriptions.find(
|
||||
(sub) => sub.status === "active" || sub.status === "trialing",
|
||||
);
|
||||
|
||||
// @todo: update the subscription with the new seat
|
||||
}
|
||||
|
||||
return invite;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
|
||||
@@ -28,10 +28,17 @@ const createAuthWithHeaders = (
|
||||
api: {
|
||||
getSession: () => auth.api.getSession({ headers }),
|
||||
signInMagicLink: (input: { email: string; callbackURL: string }) =>
|
||||
// @ts-expect-error - types need fixing
|
||||
auth.api.signInMagicLink({
|
||||
headers,
|
||||
body: { email: input.email, callbackURL: input.callbackURL },
|
||||
}),
|
||||
listActiveSubscriptions: (input: { userId: string }) =>
|
||||
// @ts-expect-error - types need fixing
|
||||
auth.api.listActiveSubscriptions({
|
||||
headers,
|
||||
query: { referenceId: input.userId },
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
},
|
||||
"prettier": "@kan/prettier-config",
|
||||
"dependencies": {
|
||||
"better-auth": "^1.2.8"
|
||||
"@better-auth/stripe": "^1.3.7",
|
||||
"better-auth": "^1.3.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { stripe } from "@better-auth/stripe";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { createAuthEndpoint, createAuthMiddleware } from "better-auth/api";
|
||||
@@ -153,6 +154,32 @@ export const initAuth = (db: dbClient) => {
|
||||
},
|
||||
plugins: [
|
||||
socialProvidersPlugin(),
|
||||
...(process.env.NEXT_PUBLIC_KAN_ENV === "cloud"
|
||||
? [
|
||||
stripe({
|
||||
stripeClient: createStripeClient(),
|
||||
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
||||
createCustomerOnSignUp: true,
|
||||
subscription: {
|
||||
enabled: true,
|
||||
plans: [
|
||||
{
|
||||
name: "team",
|
||||
priceId: process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID!,
|
||||
annualDiscountPriceId:
|
||||
process.env.STRIPE_TEAM_PLAN_YEARLY_PRICE_ID!,
|
||||
},
|
||||
{
|
||||
name: "pro",
|
||||
priceId: process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID!,
|
||||
annualDiscountPriceId:
|
||||
process.env.STRIPE_PRO_PLAN_YEARLY_PRICE_ID!,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
// @todo: hasing is disabled due to a bug in the api key plugin
|
||||
apiKey({ disableKeyHashing: true }),
|
||||
magicLink({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { BetterAuthClientPlugin } from "better-auth";
|
||||
import type { BetterFetchOption } from "better-auth/react";
|
||||
import { stripeClient } from "@better-auth/stripe/client";
|
||||
import {
|
||||
apiKeyClient,
|
||||
genericOAuthClient,
|
||||
@@ -27,6 +28,9 @@ const socialProvidersPluginClient = {
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
stripeClient({
|
||||
subscription: true,
|
||||
}),
|
||||
magicLinkClient(),
|
||||
apiKeyClient(),
|
||||
genericOAuthClient(),
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE IF NOT EXISTS "subscription" (
|
||||
"id" bigserial PRIMARY KEY NOT NULL,
|
||||
"plan" varchar(255) NOT NULL,
|
||||
"referenceId" uuid,
|
||||
"stripeCustomerId" varchar(255),
|
||||
"stripeSubscriptionId" varchar(255),
|
||||
"status" varchar(255) NOT NULL,
|
||||
"periodStart" timestamp,
|
||||
"periodEnd" timestamp,
|
||||
"cancelAtPeriodEnd" boolean,
|
||||
"seats" integer,
|
||||
"trialStart" timestamp,
|
||||
"trialEnd" timestamp,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "subscription" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_referenceId_user_id_fk" FOREIGN KEY ("referenceId") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
2536
packages/db/migrations/meta/20250901201309_snapshot.json
Normal file
2536
packages/db/migrations/meta/20250901201309_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,13 @@
|
||||
"when": 1755094668761,
|
||||
"tag": "20250813141748_AddChecklistActivityTypes",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "7",
|
||||
"when": 1756757589852,
|
||||
"tag": "20250901201309_concerned_doctor_octopus",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
||||
import { drizzle as drizzlePgLite } from "drizzle-orm/pglite";
|
||||
import { PGlite } from "@electric-sql/pglite";
|
||||
import { uuid_ossp } from "@electric-sql/pglite/contrib/uuid_ossp";
|
||||
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
|
||||
import { drizzle as drizzlePgLite } from "drizzle-orm/pglite";
|
||||
import { migrate } from "drizzle-orm/pglite/migrator";
|
||||
import { Pool } from "pg";
|
||||
|
||||
import * as schema from "./schema";
|
||||
import { PGlite } from "@electric-sql/pglite";
|
||||
import { migrate } from "drizzle-orm/pglite/migrator";
|
||||
|
||||
export type dbClient = NodePgDatabase<typeof schema> & {
|
||||
$client: Pool;
|
||||
@@ -18,7 +18,10 @@ export const createDrizzleClient = (): dbClient => {
|
||||
if (!connectionString) {
|
||||
console.log("POSTGRES_URL environment variable is not set, using PGLite");
|
||||
|
||||
const client = new PGlite({ dataDir: "./pgdata", extensions: { uuid_ossp }});
|
||||
const client = new PGlite({
|
||||
dataDir: "./pgdata",
|
||||
extensions: { uuid_ossp },
|
||||
});
|
||||
const db = drizzlePgLite(client, { schema });
|
||||
|
||||
migrate(db, { migrationsFolder: "../../packages/db/migrations" });
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from "./lists";
|
||||
export * from "./users";
|
||||
export * from "./integrations";
|
||||
export * from "./workspaces";
|
||||
export * from "./subscriptions";
|
||||
|
||||
38
packages/db/src/schema/subscriptions.ts
Normal file
38
packages/db/src/schema/subscriptions.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
bigserial,
|
||||
boolean,
|
||||
integer,
|
||||
pgTable,
|
||||
timestamp,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
import { users } from "./users";
|
||||
|
||||
export const subscription = pgTable("subscription", {
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
plan: varchar("plan", { length: 255 }).notNull(),
|
||||
referenceId: uuid("referenceId").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
stripeCustomerId: varchar("stripeCustomerId", { length: 255 }),
|
||||
stripeSubscriptionId: varchar("stripeSubscriptionId", { length: 255 }),
|
||||
status: varchar("status", { length: 255 }).notNull(),
|
||||
periodStart: timestamp("periodStart"),
|
||||
periodEnd: timestamp("periodEnd"),
|
||||
cancelAtPeriodEnd: boolean("cancelAtPeriodEnd"),
|
||||
seats: integer("seats"),
|
||||
trialStart: timestamp("trialStart"),
|
||||
trialEnd: timestamp("trialEnd"),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
}).enableRLS();
|
||||
|
||||
export const subscriptionsRelations = relations(subscription, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [subscription.referenceId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
@@ -10,8 +10,7 @@ const createStripeClient = () => {
|
||||
}
|
||||
|
||||
const stripe = new Stripe(stripeSecretKey, {
|
||||
apiVersion: "2025-05-28.basil",
|
||||
httpClient: Stripe.createFetchHttpClient(),
|
||||
apiVersion: "2025-08-27.basil",
|
||||
});
|
||||
|
||||
return stripe;
|
||||
|
||||
6783
pnpm-lock.yaml
generated
6783
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user