From 5f4678ab4ea221a768e7a131e45743f22220993f Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 2 Sep 2025 13:12:43 +0100 Subject: [PATCH] feat: prompt user to create subscription if inactive --- .../members/components/InviteMemberForm.tsx | 102 +++++++++++++++--- apps/web/src/views/members/index.tsx | 29 ++++- packages/auth/src/auth.ts | 20 ++++ ...ql => 20250902085406_AddSubscriptions.sql} | 4 +- ...shot.json => 20250902085406_snapshot.json} | 16 +-- packages/db/migrations/meta/_journal.json | 4 +- packages/db/src/repository/workspace.repo.ts | 10 ++ packages/db/src/schema/subscriptions.ts | 13 ++- packages/db/src/schema/workspaces.ts | 2 + 9 files changed, 162 insertions(+), 38 deletions(-) rename packages/db/migrations/{20250901201309_concerned_doctor_octopus.sql => 20250902085406_AddSubscriptions.sql} (79%) rename packages/db/migrations/meta/{20250901201309_snapshot.json => 20250902085406_snapshot.json} (99%) diff --git a/apps/web/src/views/members/components/InviteMemberForm.tsx b/apps/web/src/views/members/components/InviteMemberForm.tsx index de95eb10..081f4b65 100644 --- a/apps/web/src/views/members/components/InviteMemberForm.tsx +++ b/apps/web/src/views/members/components/InviteMemberForm.tsx @@ -1,11 +1,13 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { t } from "@lingui/core/macro"; +import { env } from "next-runtime-env"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { HiXMark } from "react-icons/hi2"; import { z } from "zod"; import type { InviteMemberInput } from "@kan/api/types"; +import { authClient } from "@kan/auth/client"; import Button from "~/components/Button"; import Input from "~/components/Input"; @@ -15,7 +17,20 @@ import { usePopup } from "~/providers/popup"; import { useWorkspace } from "~/providers/workspace"; import { api } from "~/utils/api"; -export function InviteMemberForm() { +export function InviteMemberForm({ + activeTeamSubscription, +}: { + activeTeamSubscription: + | { + id: string; + plan: string; + status: string; + seats: number; + periodStart: Date; + periodEnd: Date; + } + | undefined; +}) { const utils = api.useUtils(); const [isCreateAnotherEnabled, setIsCreateAnotherEnabled] = useState(false); const { closeModal } = useModal(); @@ -68,24 +83,53 @@ export function InviteMemberForm() { }, }); - 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, - }); + let isYearly = false; + let price = t`$10/month`; + let billingType = t`monthly billing`; - if (data?.url) { - window.location.href = data.url; + if ( + activeTeamSubscription?.periodStart && + activeTeamSubscription?.periodEnd + ) { + const periodStartDate = new Date(activeTeamSubscription.periodStart); + const periodEndDate = new Date(activeTeamSubscription.periodEnd); + const diffInDays = Math.round( + (periodEndDate.getTime() - periodStartDate.getTime()) / + (1000 * 60 * 60 * 24), + ); + + isYearly = diffInDays > 31; + price = isYearly ? t`$8/month` : t`$10/month`; + billingType = isYearly ? t`billed annually` : t`billed monthly`; + } + + const onSubmit = async (member: InviteMemberInput) => { + if (env("NEXT_PUBLIC_KAN_ENV") === "cloud" && !activeTeamSubscription?.id) { + const { data, error } = await authClient.subscription.upgrade({ + plan: "team", + referenceId: workspace.publicId, + metadata: { userId: "123" }, // @todo: add the user id + seats: 1, + successUrl: "/members", + cancelUrl: "/members", + returnUrl: "/members", + disableRedirect: true, + }); + + if (data?.url) { + window.location.href = data.url; + } + + if (error) { + showPopup({ + header: t`Error upgrading subscription`, + message: t`Please try again later, or contact customer support.`, + icon: "error", + }); + } + } else { + inviteMember.mutate(member); } - - // console.log(data, error); - // inviteMember.mutate(data); }; useEffect(() => { @@ -122,6 +166,30 @@ export function InviteMemberForm() { }} errorMessage={errors.email?.message} /> + + {env("NEXT_PUBLIC_KAN_ENV") === "cloud" && ( +
+ {activeTeamSubscription?.id ? ( +
+ + {t`Team Plan`} + +

+ {t`Adding a new member will cost an additional ${price} (${billingType}) per seat.`} +

+
+ ) : ( +
+ + {t`Free Plan`} + +

+ {t`Inviting members requires a Team Plan. You'll be redirected to upgrade your workspace.`} +

+
+ )} +
+ )}
diff --git a/apps/web/src/views/members/index.tsx b/apps/web/src/views/members/index.tsx index a3501122..64d86a26 100644 --- a/apps/web/src/views/members/index.tsx +++ b/apps/web/src/views/members/index.tsx @@ -11,6 +11,7 @@ import FeedbackModal from "~/components/FeedbackModal"; import Modal from "~/components/modal"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { PageHead } from "~/components/PageHead"; +import { env } from "~/env"; import { useModal } from "~/providers/modal"; import { useWorkspace } from "~/providers/workspace"; import { api } from "~/utils/api"; @@ -27,6 +28,14 @@ export default function MembersPage() { // { enabled: workspace?.publicId ? true : false }, ); + const subscription = data?.subscriptions; + + const activeTeamSubscription = subscription?.find( + (sub: any) => + sub.status === "active" || + (sub.status === "trialing" && sub.plan === "team"), + ); + const TableRow = ({ memberPublicId, memberId, @@ -160,7 +169,21 @@ export default function MembersPage() {

{t`Members`}

-
+
+ {env.NEXT_PUBLIC_KAN_ENV === "cloud" && ( +
+ + {activeTeamSubscription ? t`Team Plan` : t`Free Plan`} + +
+ )}