feat: toggle pro subscription
This commit is contained in:
@@ -15,6 +15,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import Input from "~/components/Input";
|
import Input from "~/components/Input";
|
||||||
|
import Toggle from "~/components/Toggle";
|
||||||
import { useDebounce } from "~/hooks/useDebounce";
|
import { useDebounce } from "~/hooks/useDebounce";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
@@ -50,6 +51,7 @@ export function NewWorkspaceForm() {
|
|||||||
watch,
|
watch,
|
||||||
trigger,
|
trigger,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
|
setValue,
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -68,6 +70,10 @@ export function NewWorkspaceForm() {
|
|||||||
const [debouncedSlug] = useDebounce(slug, 500);
|
const [debouncedSlug] = useDebounce(slug, 500);
|
||||||
const isTyping = slug !== debouncedSlug;
|
const isTyping = slug !== debouncedSlug;
|
||||||
|
|
||||||
|
// Pro toggle state management
|
||||||
|
const [isProToggleEnabled, setIsProToggleEnabled] = useState(false);
|
||||||
|
const [lastAvailableSlug, setLastAvailableSlug] = useState<string>("");
|
||||||
|
|
||||||
// Validate slug only after debounce
|
// Validate slug only after debounce
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isTyping) {
|
if (isTyping) {
|
||||||
@@ -104,12 +110,8 @@ export function NewWorkspaceForm() {
|
|||||||
role: "admin",
|
role: "admin",
|
||||||
});
|
});
|
||||||
|
|
||||||
// If in cloud and user provided a valid slug, create checkout session for pro
|
// If in cloud and Pro toggle is enabled, create checkout session for pro
|
||||||
if (
|
if (env("NEXT_PUBLIC_KAN_ENV") === "cloud" && isProToggleEnabled) {
|
||||||
env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
|
||||||
slug &&
|
|
||||||
isWorkspaceSlugAvailable?.isAvailable
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"/api/stripe/create_checkout_session",
|
"/api/stripe/create_checkout_session",
|
||||||
@@ -119,9 +121,9 @@ export function NewWorkspaceForm() {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
slug,
|
slug: slug || undefined,
|
||||||
workspacePublicId: values.publicId,
|
workspacePublicId: values.publicId,
|
||||||
cancelUrl: "/boards",
|
cancelUrl: "/settings?upgrade=pro",
|
||||||
successUrl: "/boards",
|
successUrl: "/boards",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -168,12 +170,21 @@ export function NewWorkspaceForm() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!checkWorkspaceSlugAvailability.isPending && isValidSlug) {
|
if (!checkWorkspaceSlugAvailability.isPending && isValidSlug) {
|
||||||
setShouldShowBenefits(isWorkspaceSlugAvailable?.isAvailable === true);
|
const isAvailable = isWorkspaceSlugAvailable?.isAvailable === true;
|
||||||
|
setShouldShowBenefits(isAvailable);
|
||||||
|
|
||||||
|
// Automatically enable Pro toggle when slug is available (only in cloud env)
|
||||||
|
if (isAvailable && isCloudEnv) {
|
||||||
|
setIsProToggleEnabled(true);
|
||||||
|
setLastAvailableSlug(slug);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isValidSlug,
|
isValidSlug,
|
||||||
isWorkspaceSlugAvailable?.isAvailable,
|
isWorkspaceSlugAvailable?.isAvailable,
|
||||||
checkWorkspaceSlugAvailability.isPending,
|
checkWorkspaceSlugAvailability.isPending,
|
||||||
|
slug,
|
||||||
|
isCloudEnv,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Reset benefits when slug becomes invalid
|
// Reset benefits when slug becomes invalid
|
||||||
@@ -183,7 +194,22 @@ export function NewWorkspaceForm() {
|
|||||||
}
|
}
|
||||||
}, [isValidSlug]);
|
}, [isValidSlug]);
|
||||||
|
|
||||||
const showProBenefits = shouldShowBenefits;
|
// Handle Pro toggle changes
|
||||||
|
const handleProToggleChange = () => {
|
||||||
|
if (isProToggleEnabled) {
|
||||||
|
// Turning off: clear the slug and disable Pro
|
||||||
|
setValue("slug", "");
|
||||||
|
setIsProToggleEnabled(false);
|
||||||
|
} else {
|
||||||
|
// Turning on: restore the last available slug if we have one
|
||||||
|
if (lastAvailableSlug) {
|
||||||
|
setValue("slug", lastAvailableSlug);
|
||||||
|
}
|
||||||
|
setIsProToggleEnabled(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showProBenefits = isProToggleEnabled;
|
||||||
|
|
||||||
const onSubmit = (values: FormValues) => {
|
const onSubmit = (values: FormValues) => {
|
||||||
// Don't submit if slug is provided but not available
|
// Don't submit if slug is provided but not available
|
||||||
@@ -278,11 +304,11 @@ export function NewWorkspaceForm() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showProBenefits && (
|
{slug && slug.length >= 3 && shouldShowBenefits && (
|
||||||
<div className="mt-2 flex items-center gap-1">
|
<div className="mt-2 flex items-center gap-1">
|
||||||
<HiInformationCircle className="h-4 w-4 text-dark-900" />
|
<HiInformationCircle className="h-4 w-4 text-dark-900" />
|
||||||
<p className="text-xs text-gray-500 dark:text-dark-900">
|
<p className="text-xs text-gray-500 dark:text-dark-900">
|
||||||
{t`Custom URLs require Pro plan ($29/month)`}
|
{t`Custom URLs require upgrading to a Pro plan`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -331,20 +357,27 @@ export function NewWorkspaceForm() {
|
|||||||
!showProBenefits && "mt-12",
|
!showProBenefits && "mt-12",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2">
|
{/* Pro Toggle - only show in cloud environment */}
|
||||||
|
{isCloudEnv && (
|
||||||
|
<Toggle
|
||||||
|
isChecked={isProToggleEnabled}
|
||||||
|
onChange={handleProToggleChange}
|
||||||
|
label={t`Upgrade to Pro ($29/month)`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={createWorkspace.isPending}
|
isLoading={createWorkspace.isPending}
|
||||||
iconLeft={showProBenefits ? <HiBolt /> : undefined}
|
|
||||||
disabled={
|
disabled={
|
||||||
createWorkspace.isPending ||
|
createWorkspace.isPending ||
|
||||||
(!!isValidSlug &&
|
(!!slug &&
|
||||||
(checkWorkspaceSlugAvailability.isPending ||
|
(checkWorkspaceSlugAvailability.isPending ||
|
||||||
isWorkspaceSlugAvailable?.isAvailable === false ||
|
isWorkspaceSlugAvailable?.isAvailable === false ||
|
||||||
isTyping))
|
isTyping))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{showProBenefits ? t`Create Pro workspace` : t`Create workspace`}
|
{t`Create workspace`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -95,7 +95,12 @@ export function UpgradeToProConfirmation({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
||||||
<Button onClick={() => closeModal()} variant="secondary">
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
closeModal();
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
{t`Cancel`}
|
{t`Cancel`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { HiBolt, HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
|
import { HiBolt, HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
|
||||||
|
|
||||||
import type { Subscription } from "@kan/shared/utils";
|
import type { Subscription } from "@kan/shared/utils";
|
||||||
@@ -36,6 +36,7 @@ export default function SettingsPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workspaceUrlSectionRef = useRef<HTMLDivElement>(null);
|
const workspaceUrlSectionRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [hasOpenedUpgradeModal, setHasOpenedUpgradeModal] = useState(false);
|
||||||
const isCredentialsEnabled =
|
const isCredentialsEnabled =
|
||||||
env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true";
|
env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true";
|
||||||
const { data } = api.user.getUser.useQuery();
|
const { data } = api.user.getUser.useQuery();
|
||||||
@@ -100,11 +101,13 @@ export default function SettingsPage() {
|
|||||||
if (
|
if (
|
||||||
router.query.upgrade === "pro" &&
|
router.query.upgrade === "pro" &&
|
||||||
env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
||||||
!hasActiveSubscription(subscriptions, "pro")
|
!hasActiveSubscription(subscriptions, "pro") &&
|
||||||
|
!hasOpenedUpgradeModal
|
||||||
) {
|
) {
|
||||||
openModal("UPGRADE_TO_PRO");
|
openModal("UPGRADE_TO_PRO");
|
||||||
|
setHasOpenedUpgradeModal(true);
|
||||||
}
|
}
|
||||||
}, [router.query.upgrade, subscriptions, openModal]);
|
}, [router.query.upgrade, subscriptions, openModal, hasOpenedUpgradeModal]);
|
||||||
|
|
||||||
const { mutateAsync: disconnectTrello } =
|
const { mutateAsync: disconnectTrello } =
|
||||||
api.integration.disconnect.useMutation({
|
api.integration.disconnect.useMutation({
|
||||||
|
|||||||
Reference in New Issue
Block a user