feat: make billing better (#203)

This commit is contained in:
KM Koushik
2025-08-25 22:35:21 +10:00
committed by GitHub
parent 8ce5e4b2dd
commit 3f9094e95d
25 changed files with 956 additions and 360 deletions

View File

@@ -34,6 +34,8 @@ import {
} from "@unsend/ui/src/form";
import { useTeam } from "~/providers/team-context";
import { isCloud, isSelfHosted } from "~/utils/common";
import { useUpgradeModalStore } from "~/store/upgradeModalStore";
import { LimitReason } from "~/lib/constants/plans";
const inviteTeamMemberSchema = z.object({
email: z
@@ -50,6 +52,11 @@ export default function InviteTeamMember() {
const { currentIsAdmin } = useTeam();
const { data: domains } = api.domain.domains.useQuery();
const limitsQuery = api.limits.get.useQuery({
type: LimitReason.TEAM_MEMBER,
});
const { openModal } = useUpgradeModalStore((s) => s.action);
const [open, setOpen] = useState(false);
const form = useForm<FormData>({
@@ -65,6 +72,11 @@ export default function InviteTeamMember() {
const createInvite = api.team.createTeamInvite.useMutation();
function onSubmit(values: FormData) {
if (limitsQuery.data?.isLimitReached) {
openModal(limitsQuery.data.reason);
return;
}
createInvite.mutate(
{
email: values.email,
@@ -82,11 +94,16 @@ export default function InviteTeamMember() {
console.error(error);
toast.error(error.message || "Failed to send invitation");
},
}
},
);
}
async function onCopyLink() {
if (limitsQuery.data?.isLimitReached) {
openModal(limitsQuery.data.reason);
return;
}
createInvite.mutate(
{
email: form.getValues("email"),
@@ -97,7 +114,7 @@ export default function InviteTeamMember() {
onSuccess: (invite) => {
void utils.team.getTeamInvites.invalidate();
navigator.clipboard.writeText(
`${location.origin}/join-team?inviteId=${invite.id}`
`${location.origin}/join-team?inviteId=${invite.id}`,
);
form.reset();
setOpen(false);
@@ -107,16 +124,28 @@ export default function InviteTeamMember() {
console.error(error);
toast.error(error.message || "Failed to copy invitation link");
},
}
},
);
}
function onOpenChange(_open: boolean) {
if (_open && limitsQuery.data?.isLimitReached) {
openModal(limitsQuery.data.reason);
return;
}
setOpen(_open);
}
if (!currentIsAdmin) {
return null;
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<Dialog
open={open}
onOpenChange={(_open) => (_open !== open ? onOpenChange(_open) : null)}
>
<DialogTrigger asChild>
<Button size="sm">
<PlusIcon className="mr-2 h-4 w-4" />
@@ -201,7 +230,7 @@ export default function InviteTeamMember() {
</Button>
{isSelfHosted() ? (
<Button
disabled={createInvite.isPending}
disabled={createInvite.isPending || limitsQuery.isLoading}
isLoading={createInvite.isPending}
className="w-[150px]"
onClick={form.handleSubmit(onCopyLink)}
@@ -212,7 +241,7 @@ export default function InviteTeamMember() {
{isCloud() || domains?.length ? (
<Button
type="submit"
disabled={createInvite.isPending}
disabled={createInvite.isPending || limitsQuery.isLoading}
isLoading={createInvite.isPending}
className="w-[150px]"
>