From 38803f66964275f0020b8296bb3d4a02276d1165 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 8 Sep 2025 22:09:45 +0100 Subject: [PATCH] feat(cloud): show pro workspace features --- .../components/CustomURLConfirmation.tsx | 61 ----------- .../components/UpdateWorkspaceUrlForm.tsx | 2 +- .../components/UpgradeToProConfirmation.tsx | 103 ++++++++++++++++++ apps/web/src/views/settings/index.tsx | 34 +++++- 4 files changed, 134 insertions(+), 66 deletions(-) delete mode 100644 apps/web/src/views/settings/components/CustomURLConfirmation.tsx create mode 100644 apps/web/src/views/settings/components/UpgradeToProConfirmation.tsx diff --git a/apps/web/src/views/settings/components/CustomURLConfirmation.tsx b/apps/web/src/views/settings/components/CustomURLConfirmation.tsx deleted file mode 100644 index 45bec735..00000000 --- a/apps/web/src/views/settings/components/CustomURLConfirmation.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { t } from "@lingui/core/macro"; - -import { authClient } from "@kan/auth/client"; - -import Button from "~/components/Button"; -import { useModal } from "~/providers/modal"; -import { usePopup } from "~/providers/popup"; - -export function CustomURLConfirmation({ - userId, - workspacePublicId, -}: { - userId: string; - workspacePublicId: string; -}) { - const { closeModal, entityId } = useModal(); - const { showPopup } = usePopup(); - - const handleUpgrade = async () => { - const { data, error } = await authClient.subscription.upgrade({ - plan: "pro", - referenceId: workspacePublicId, - metadata: { userId, workspacePublicId, workspaceSlug: entityId }, - successUrl: "/settings", - cancelUrl: "/settings", - returnUrl: "/settings", - 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", - }); - } - }; - - return ( -
-
-

- {t`Confirm URL change`} -

-

- {t`Custom URLs are a premium feature. You'll be directed to upgrade your account.`} -

-
-
- - -
-
- ); -} diff --git a/apps/web/src/views/settings/components/UpdateWorkspaceUrlForm.tsx b/apps/web/src/views/settings/components/UpdateWorkspaceUrlForm.tsx index b71a8839..4205e3c7 100644 --- a/apps/web/src/views/settings/components/UpdateWorkspaceUrlForm.tsx +++ b/apps/web/src/views/settings/components/UpdateWorkspaceUrlForm.tsx @@ -98,7 +98,7 @@ const UpdateWorkspaceUrlForm = ({ if (!isWorkspaceSlugAvailable?.isAvailable) return; if (workspacePlan !== "pro" && env("NEXT_PUBLIC_KAN_ENV") === "cloud") - return openModal("UPDATE_WORKSPACE_URL", data.slug); + return openModal("UPGRADE_TO_PRO", data.slug); updateWorkspaceSlug.mutate({ workspacePublicId, diff --git a/apps/web/src/views/settings/components/UpgradeToProConfirmation.tsx b/apps/web/src/views/settings/components/UpgradeToProConfirmation.tsx new file mode 100644 index 00000000..b25a3980 --- /dev/null +++ b/apps/web/src/views/settings/components/UpgradeToProConfirmation.tsx @@ -0,0 +1,103 @@ +import { t } from "@lingui/core/macro"; +import { HiBolt, HiCheckBadge } from "react-icons/hi2"; + +import { authClient } from "@kan/auth/client"; + +import Button from "~/components/Button"; +import { useModal } from "~/providers/modal"; +import { usePopup } from "~/providers/popup"; + +export function UpgradeToProConfirmation({ + userId, + workspacePublicId, +}: { + userId: string; + workspacePublicId: string; +}) { + const { closeModal, entityId } = useModal(); + const { showPopup } = usePopup(); + + const handleUpgrade = async () => { + const { data, error } = await authClient.subscription.upgrade({ + plan: "pro", + referenceId: workspacePublicId, + metadata: { + userId, + workspacePublicId, + ...(entityId && { workspaceSlug: entityId }), + }, + successUrl: "/settings", + cancelUrl: "/settings", + returnUrl: "/settings", + 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", + }); + } + }; + + return ( +
+
+

+ {t`Upgrade to Pro`} +

+

+ {t`Supercharge your workspace for just $29/month. Here's what you'll get:`} +

+ +
+
+
+ +
+ + {t`Unlimited members`} + + + {t`Launch offer`} + +
+
+
+ + + {t`Custom workspace URL`} + +
+
+ + + {t`Board analytics (coming soon)`} + +
+
+ + + {t`Priority email support`} + +
+
+
+
+
+ + +
+
+ ); +} diff --git a/apps/web/src/views/settings/index.tsx b/apps/web/src/views/settings/index.tsx index 356c886f..10f3459a 100644 --- a/apps/web/src/views/settings/index.tsx +++ b/apps/web/src/views/settings/index.tsx @@ -2,7 +2,10 @@ import { useRouter } from "next/router"; import { t } from "@lingui/core/macro"; import { env } from "next-runtime-env"; import { useEffect, useRef } from "react"; -import { HiMiniArrowTopRightOnSquare } from "react-icons/hi2"; +import { HiBolt, HiMiniArrowTopRightOnSquare } from "react-icons/hi2"; + +import type { Subscription } from "@kan/shared/utils"; +import { hasActiveSubscription } from "@kan/shared/utils"; import Button from "~/components/Button"; import FeedbackModal from "~/components/FeedbackModal"; @@ -17,13 +20,13 @@ import { api } from "~/utils/api"; import Avatar from "./components/Avatar"; import { ChangePasswordFormConfirmation } from "./components/ChangePasswordConfirmation"; import CreateAPIKeyForm from "./components/CreateAPIKeyForm"; -import { CustomURLConfirmation } from "./components/CustomURLConfirmation"; import { DeleteAccountConfirmation } from "./components/DeleteAccountConfirmation"; import { DeleteWorkspaceConfirmation } from "./components/DeleteWorkspaceConfirmation"; import UpdateDisplayNameForm from "./components/UpdateDisplayNameForm"; import UpdateWorkspaceDescriptionForm from "./components/UpdateWorkspaceDescriptionForm"; import UpdateWorkspaceNameForm from "./components/UpdateWorkspaceNameForm"; import UpdateWorkspaceUrlForm from "./components/UpdateWorkspaceUrlForm"; +import { UpgradeToProConfirmation } from "./components/UpgradeToProConfirmation"; export default function SettingsPage() { const { modalContentType, openModal, isOpen } = useModal(); @@ -37,6 +40,14 @@ export default function SettingsPage() { env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true"; const { data } = api.user.getUser.useQuery(); + const { data: workspaceData } = api.workspace.byId.useQuery({ + workspacePublicId: workspace.publicId, + }); + + const subscriptions = workspaceData?.subscriptions as + | Subscription[] + | undefined; + const { data: integrations, refetch: refetchIntegrations, @@ -180,6 +191,18 @@ export default function SettingsPage() { workspacePublicId={workspace.publicId} workspaceDescription={workspace.description ?? ""} /> + + {env("NEXT_PUBLIC_KAN_ENV") === "cloud" && + !hasActiveSubscription(subscriptions, "pro") && ( +
+ +
+ )}
@@ -346,9 +369,12 @@ export default function SettingsPage() { - +