feat(cloud): enable seat based pricing (#161)
* feat: setup subscriptions * feat: prompt user to create subscription if inactive * feat: update subscription when increasing/decreasing workspace members * feat: rework upgrade * chore: remove pricing notice * chore: add translations * chore: update proration comment
This commit is contained in:
@@ -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,24 @@ import { usePopup } from "~/providers/popup";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export function InviteMemberForm() {
|
||||
export function InviteMemberForm({
|
||||
numberOfMembers,
|
||||
activeTeamSubscription,
|
||||
userId,
|
||||
}: {
|
||||
numberOfMembers: number;
|
||||
activeTeamSubscription:
|
||||
| {
|
||||
id: number | null;
|
||||
plan: string;
|
||||
status: string;
|
||||
seats: number | null;
|
||||
periodStart: Date | null;
|
||||
periodEnd: Date | null;
|
||||
}
|
||||
| undefined;
|
||||
userId: string | undefined;
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const [isCreateAnotherEnabled, setIsCreateAnotherEnabled] = useState(false);
|
||||
const { closeModal } = useModal();
|
||||
@@ -42,7 +61,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 +87,53 @@ export function InviteMemberForm() {
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: InviteMemberInput) => {
|
||||
createBoard.mutate(data);
|
||||
let isYearly = false;
|
||||
let price = t`$10/month`;
|
||||
let billingType = t`monthly billing`;
|
||||
|
||||
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 = (member: InviteMemberInput) => {
|
||||
inviteMember.mutate(member);
|
||||
};
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
const { data, error } = await authClient.subscription.upgrade({
|
||||
plan: "team",
|
||||
referenceId: workspace.publicId,
|
||||
metadata: { userId },
|
||||
seats: numberOfMembers,
|
||||
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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -97,6 +161,10 @@ export function InviteMemberForm() {
|
||||
<Input
|
||||
id="email"
|
||||
placeholder={t`Email`}
|
||||
disabled={
|
||||
env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
||||
!activeTeamSubscription?.id
|
||||
}
|
||||
{...register("email", { required: true })}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter") {
|
||||
@@ -106,22 +174,63 @@ export function InviteMemberForm() {
|
||||
}}
|
||||
errorMessage={errors.email?.message}
|
||||
/>
|
||||
|
||||
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && (
|
||||
<div className="mt-3 rounded-md bg-light-100 p-3 text-xs text-light-900 dark:bg-dark-200 dark:text-dark-900">
|
||||
{activeTeamSubscription?.id ? (
|
||||
<div>
|
||||
<span className="font-medium text-emerald-500 dark:text-emerald-400">
|
||||
{t`Team Plan`}
|
||||
</span>
|
||||
<p className="mt-1">
|
||||
{t`Adding a new member will cost an additional ${price} (${billingType}) per seat.`}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<span className="font-medium text-light-950 dark:text-dark-950">
|
||||
{t`Free Plan`}
|
||||
</span>
|
||||
<p className="mt-1">
|
||||
{t`Inviting members requires a Team Plan. You'll be redirected to upgrade your workspace.`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||
<Toggle
|
||||
label={t`Invite another`}
|
||||
isChecked={isCreateAnotherEnabled}
|
||||
onChange={() => setIsCreateAnotherEnabled(!isCreateAnotherEnabled)}
|
||||
/>
|
||||
{activeTeamSubscription?.id &&
|
||||
env("NEXT_PUBLIC_KAN_ENV") === "cloud" && (
|
||||
<Toggle
|
||||
label={t`Invite another`}
|
||||
isChecked={isCreateAnotherEnabled}
|
||||
onChange={() =>
|
||||
setIsCreateAnotherEnabled(!isCreateAnotherEnabled)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={createBoard.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`}
|
||||
</Button>
|
||||
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
||||
!activeTeamSubscription?.id ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleUpgrade}
|
||||
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`Upgrade to Team Plan`}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={inviteMember.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`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user