feat(cloud): improved upgrade journey

This commit is contained in:
Henry
2026-05-18 23:20:11 +01:00
parent 6ea622e7f7
commit cf566e04c6
15 changed files with 262 additions and 200 deletions

View File

@@ -7,6 +7,7 @@ import { withApiLogging } from "@kan/api/utils/apiLogging";
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createLogger } from "@kan/logger";
import { getActiveSubscriptions } from "@kan/shared/utils";
import { tierConfig } from "./_utils";
@@ -122,17 +123,18 @@ export default withApiLogging(
license_key,
);
if (sub) {
await subscriptionRepo.updateById(db, sub.id, {
plan: "free",
status: "inactive",
});
const [, allSubs] = await Promise.all([
subscriptionRepo.updateById(db, sub.id, {
plan: "free",
status: "inactive",
}),
sub.referenceId
? subscriptionRepo.getByReferenceId(db, sub.referenceId)
: Promise.resolve([]),
]);
if (sub.referenceId) {
const allSubs = await subscriptionRepo.getByReferenceId(
db,
sub.referenceId,
);
const hasActiveSub = allSubs.some(
(s) => s.id !== sub.id && s.status === "active",
const hasActiveSub = getActiveSubscriptions(allSubs).some(
(s) => s.id !== sub.id,
);
if (!hasActiveSub) {
await workspaceRepo.update(db, sub.referenceId, { plan: "free" });

View File

@@ -64,7 +64,10 @@ export default async function handler(
meta.userId &&
meta.userEmail
) {
const existing = await workspaceRepo.getByPublicId(db, meta.workspacePublicId);
const existing = await workspaceRepo.getByPublicId(
db,
meta.workspacePublicId,
);
if (!existing) {
const slug = meta.workspaceSlug ?? meta.workspacePublicId;
@@ -90,10 +93,10 @@ export default async function handler(
});
}
} else {
// Existing workspace upgrade — update plan (and slug for pro)
await workspaceRepo.update(db, meta.workspacePublicId, {
plan,
...(plan === "pro" && meta.workspaceSlug && { slug: meta.workspaceSlug }),
...(plan === "pro" &&
meta.workspaceSlug && { slug: meta.workspaceSlug }),
});
}

View File

@@ -2,6 +2,7 @@ import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { env } from "next-runtime-env";
import { useEffect, useState } from "react";
import { authClient } from "@kan/auth/client";
@@ -16,6 +17,8 @@ export default function PartnerActivatePage() {
const licenseKey = searchParams.get("license_key");
const error = searchParams.get("error");
const partnerName = env("NEXT_PUBLIC_PARTNER_NAME");
const { data: session, isPending } = authClient.useSession();
const [isMagicLinkSent, setIsMagicLinkSent] = useState(false);
const [magicLinkRecipient, setMagicLinkRecipient] = useState("");
@@ -55,6 +58,11 @@ export default function PartnerActivatePage() {
<p className="mb-10 text-sm text-light-800 dark:text-dark-800">
{isMagicLinkSent ? (
<Trans>We sent a link to {magicLinkRecipient}</Trans>
) : partnerName ? (
<Trans>
Sign in or create an account to activate your {partnerName}{" "}
license
</Trans>
) : (
t`Sign in or create an account to activate your license`
)}

View File

@@ -0,0 +1,31 @@
import { useRouter } from "next/navigation";
import { env } from "next-runtime-env";
import { useEffect } from "react";
import { authClient } from "@kan/auth/client";
import { PageHead } from "~/components/PageHead";
import SelectPlanView from "~/views/onboarding/select-plan";
export default function UpgradeSelectPlanPage() {
const router = useRouter();
const { data: session, isPending } = authClient.useSession();
useEffect(() => {
if (!isPending && !session?.user) {
router.push("/login");
}
if (!isPending && env("NEXT_PUBLIC_KAN_ENV") !== "cloud") {
router.push("/boards");
}
}, [session, isPending, router]);
if (isPending || !session?.user) return null;
return (
<>
<PageHead title="Upgrade | kan.bn" />
<SelectPlanView />
</>
);
}