feat: add waitlist rejection email (#257)

This commit is contained in:
KM Koushik
2025-09-27 09:41:47 +10:00
committed by GitHub
parent 76fdad6c81
commit 1a00999bf0
2 changed files with 115 additions and 15 deletions
@@ -78,6 +78,15 @@ export default function AdminWaitlistPage() {
}, },
}); });
const rejectWaitlist = api.admin.rejectWaitlistUser.useMutation({
onSuccess: () => {
toast.success("Rejection email sent");
},
onError: (error) => {
toast.error(error.message ?? "Unable to send rejection email");
},
});
const onSubmit = (values: SearchInput) => { const onSubmit = (values: SearchInput) => {
setHasSearched(false); setHasSearched(false);
setUserResult(null); setUserResult(null);
@@ -89,6 +98,11 @@ export default function AdminWaitlistPage() {
updateWaitlist.mutate({ userId: userResult.id, isWaitlisted: checked }); updateWaitlist.mutate({ userId: userResult.id, isWaitlisted: checked });
}; };
const handleReject = () => {
if (!userResult) return;
rejectWaitlist.mutate({ userId: userResult.id });
};
if (!isCloud()) { if (!isCloud()) {
return ( return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground"> <div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
@@ -166,22 +180,47 @@ export default function AdminWaitlistPage() {
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center justify-between gap-4 border-t pt-4"> <div className="space-y-4 border-t pt-4">
<div> <div className="flex flex-wrap items-center justify-between gap-4">
<p className="text-sm font-medium">Waitlist access</p> <div>
<p className="text-sm text-muted-foreground"> <p className="text-sm font-medium">Waitlist access</p>
Toggle to control whether the user remains on the waitlist. <p className="text-sm text-muted-foreground">
</p> Toggle to control whether the user remains on the waitlist.
</p>
</div>
<div className="flex items-center gap-2">
<Switch
checked={userResult.isWaitlisted}
onCheckedChange={handleToggle}
disabled={updateWaitlist.isPending}
/>
{updateWaitlist.isPending ? (
<Spinner className="h-4 w-4" />
) : null}
</div>
</div> </div>
<div className="flex items-center gap-2">
<Switch <div className="flex flex-wrap items-center justify-between gap-4">
checked={userResult.isWaitlisted} <div>
onCheckedChange={handleToggle} <p className="text-sm font-medium">Reject waitlist request</p>
disabled={updateWaitlist.isPending} <p className="text-sm text-muted-foreground">
/> Send the applicant a rejection email without changing their waitlist status.
{updateWaitlist.isPending ? ( </p>
<Spinner className="h-4 w-4" /> </div>
) : null} <Button
type="button"
variant="destructive"
onClick={handleReject}
disabled={rejectWaitlist.isPending}
>
{rejectWaitlist.isPending ? (
<>
<Spinner className="mr-2 h-4 w-4" /> Sending...
</>
) : (
"Send rejection email"
)}
</Button>
</div> </div>
</div> </div>
</div> </div>
+61
View File
@@ -214,6 +214,67 @@ export const adminRouter = createTRPCRouter({
return updatedUser; return updatedUser;
}), }),
rejectWaitlistUser: adminProcedure
.input(
z.object({
userId: z.number(),
})
)
.mutation(async ({ input }) => {
const user = await db.user.findUnique({
where: { id: input.userId },
select: waitlistUserSelection,
});
if (!user) {
throw new Error("User not found");
}
if (!user.email) {
throw new Error("User email is missing");
}
const founderEmail = env.FOUNDER_EMAIL ?? undefined;
const fallbackFrom = env.FROM_EMAIL ?? env.ADMIN_EMAIL ?? undefined;
const replyTo = founderEmail ?? fallbackFrom;
if (!replyTo) {
throw new Error("No sender email configured");
}
const fromOverride = founderEmail ?? undefined;
const text = [
"Hello,",
"",
"Sorry, We cannot proceed with this request at this time, this might affect useSend\u2019s sending reputation.",
"",
"",
"cheers,",
"koushik - useSend.com",
].join("\n");
try {
await sendMail(
user.email,
"useSend: Waitlist request update",
text,
toPlainHtml(text),
replyTo,
fromOverride
);
} catch (error) {
logger.error(
{ userId: user.id, error },
"Failed to send waitlist rejection email"
);
throw new Error("Failed to send waitlist rejection email");
}
return { sent: true };
}),
findTeam: adminProcedure findTeam: adminProcedure
.input( .input(
z.object({ z.object({