diff --git a/apps/web/package.json b/apps/web/package.json index 55dad1d..318a881 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -36,7 +36,6 @@ "@trpc/server": "^11.1.1", "@unsend/email-editor": "workspace:*", "@unsend/ui": "workspace:*", - "jsx-email": "^2.7.1", "bullmq": "^5.51.1", "chrono-node": "^2.8.0", "date-fns": "^4.1.0", @@ -45,6 +44,7 @@ "hono": "^4.7.7", "html-to-text": "^9.0.5", "ioredis": "^5.6.1", + "jsx-email": "^2.7.1", "lucide-react": "^0.503.0", "mime-types": "^3.0.1", "nanoid": "^5.1.5", @@ -67,7 +67,8 @@ "ua-parser-js": "^2.0.3", "unsend": "workspace:*", "use-debounce": "^10.0.4", - "zod": "^3.24.3" + "zod": "^3.24.3", + "zustand": "^5.0.8" }, "devDependencies": { "@next/eslint-plugin-next": "^15.3.1", diff --git a/apps/web/prisma/migrations/20250824131647_add_price_ids_array/migration.sql b/apps/web/prisma/migrations/20250824131647_add_price_ids_array/migration.sql new file mode 100644 index 0000000..f136c1c --- /dev/null +++ b/apps/web/prisma/migrations/20250824131647_add_price_ids_array/migration.sql @@ -0,0 +1,8 @@ +-- CreateEnum +CREATE TYPE "SendingDisabledReason" AS ENUM ('FREE_LIMIT_REACHED', 'BILLING_ISSUE', 'SPAM'); + +-- AlterTable +ALTER TABLE "Subscription" ADD COLUMN "priceIds" TEXT[]; + +-- Populate priceIds array with existing priceId values +UPDATE "Subscription" SET "priceIds" = ARRAY["priceId"] WHERE "priceId" IS NOT NULL; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index ffa2f4e..8d380de 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -138,6 +138,7 @@ model Subscription { teamId Int status String priceId String + priceIds String[] currentPeriodEnd DateTime? currentPeriodStart DateTime? cancelAtPeriodEnd DateTime? diff --git a/apps/web/src/app/(dashboard)/contacts/add-contact-book.tsx b/apps/web/src/app/(dashboard)/contacts/add-contact-book.tsx index 7c005ee..e9ccdfd 100644 --- a/apps/web/src/app/(dashboard)/contacts/add-contact-book.tsx +++ b/apps/web/src/app/(dashboard)/contacts/add-contact-book.tsx @@ -26,6 +26,8 @@ import { FormLabel, FormMessage, } from "@unsend/ui/src/form"; +import { useUpgradeModalStore } from "~/store/upgradeModalStore"; +import { LimitReason } from "~/lib/constants/plans"; const contactBookSchema = z.object({ name: z.string({ required_error: "Name is required" }).min(1, { @@ -38,6 +40,11 @@ export default function AddContactBook() { const createContactBookMutation = api.contacts.createContactBook.useMutation(); + const limitsQuery = api.limits.get.useQuery({ + type: LimitReason.CONTACT_BOOK, + }); + const { openModal } = useUpgradeModalStore((s) => s.action); + const utils = api.useUtils(); const contactBookForm = useForm>({ @@ -48,6 +55,11 @@ export default function AddContactBook() { }); function handleSave(values: z.infer) { + if (limitsQuery.data?.isLimitReached) { + openModal(limitsQuery.data.reason); + return; + } + createContactBookMutation.mutate( { name: values.name, @@ -59,14 +71,23 @@ export default function AddContactBook() { setOpen(false); toast.success("Contact book created successfully"); }, - } + }, ); } + function onOpenChange(_open: boolean) { + if (_open && limitsQuery.data?.isLimitReached) { + openModal(limitsQuery.data.reason); + return; + } + + setOpen(_open); + } + return ( (_open !== open ? setOpen(_open) : null)} + onOpenChange={(_open) => (_open !== open ? onOpenChange(_open) : null)} > diff --git a/apps/web/src/app/(dashboard)/settings/team/invite-team-member.tsx b/apps/web/src/app/(dashboard)/settings/team/invite-team-member.tsx index 24f6fb1..62cbb06 100644 --- a/apps/web/src/app/(dashboard)/settings/team/invite-team-member.tsx +++ b/apps/web/src/app/(dashboard)/settings/team/invite-team-member.tsx @@ -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({ @@ -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 ( - + (_open !== open ? onOpenChange(_open) : null)} + > {isSelfHosted() ? (