Skip to content

Commit 1a00999

Browse files
authored
feat: add waitlist rejection email (#257)
1 parent 76fdad6 commit 1a00999

2 files changed

Lines changed: 115 additions & 15 deletions

File tree

apps/web/src/app/(dashboard)/admin/waitlist/page.tsx

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ export default function AdminWaitlistPage() {
7878
},
7979
});
8080

81+
const rejectWaitlist = api.admin.rejectWaitlistUser.useMutation({
82+
onSuccess: () => {
83+
toast.success("Rejection email sent");
84+
},
85+
onError: (error) => {
86+
toast.error(error.message ?? "Unable to send rejection email");
87+
},
88+
});
89+
8190
const onSubmit = (values: SearchInput) => {
8291
setHasSearched(false);
8392
setUserResult(null);
@@ -89,6 +98,11 @@ export default function AdminWaitlistPage() {
8998
updateWaitlist.mutate({ userId: userResult.id, isWaitlisted: checked });
9099
};
91100

101+
const handleReject = () => {
102+
if (!userResult) return;
103+
rejectWaitlist.mutate({ userId: userResult.id });
104+
};
105+
92106
if (!isCloud()) {
93107
return (
94108
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
@@ -166,22 +180,47 @@ export default function AdminWaitlistPage() {
166180
</div>
167181
</div>
168182

169-
<div className="flex flex-wrap items-center justify-between gap-4 border-t pt-4">
170-
<div>
171-
<p className="text-sm font-medium">Waitlist access</p>
172-
<p className="text-sm text-muted-foreground">
173-
Toggle to control whether the user remains on the waitlist.
174-
</p>
183+
<div className="space-y-4 border-t pt-4">
184+
<div className="flex flex-wrap items-center justify-between gap-4">
185+
<div>
186+
<p className="text-sm font-medium">Waitlist access</p>
187+
<p className="text-sm text-muted-foreground">
188+
Toggle to control whether the user remains on the waitlist.
189+
</p>
190+
</div>
191+
<div className="flex items-center gap-2">
192+
<Switch
193+
checked={userResult.isWaitlisted}
194+
onCheckedChange={handleToggle}
195+
disabled={updateWaitlist.isPending}
196+
/>
197+
{updateWaitlist.isPending ? (
198+
<Spinner className="h-4 w-4" />
199+
) : null}
200+
</div>
175201
</div>
176-
<div className="flex items-center gap-2">
177-
<Switch
178-
checked={userResult.isWaitlisted}
179-
onCheckedChange={handleToggle}
180-
disabled={updateWaitlist.isPending}
181-
/>
182-
{updateWaitlist.isPending ? (
183-
<Spinner className="h-4 w-4" />
184-
) : null}
202+
203+
<div className="flex flex-wrap items-center justify-between gap-4">
204+
<div>
205+
<p className="text-sm font-medium">Reject waitlist request</p>
206+
<p className="text-sm text-muted-foreground">
207+
Send the applicant a rejection email without changing their waitlist status.
208+
</p>
209+
</div>
210+
<Button
211+
type="button"
212+
variant="destructive"
213+
onClick={handleReject}
214+
disabled={rejectWaitlist.isPending}
215+
>
216+
{rejectWaitlist.isPending ? (
217+
<>
218+
<Spinner className="mr-2 h-4 w-4" /> Sending...
219+
</>
220+
) : (
221+
"Send rejection email"
222+
)}
223+
</Button>
185224
</div>
186225
</div>
187226
</div>

apps/web/src/server/api/routers/admin.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,67 @@ export const adminRouter = createTRPCRouter({
214214
return updatedUser;
215215
}),
216216

217+
rejectWaitlistUser: adminProcedure
218+
.input(
219+
z.object({
220+
userId: z.number(),
221+
})
222+
)
223+
.mutation(async ({ input }) => {
224+
const user = await db.user.findUnique({
225+
where: { id: input.userId },
226+
select: waitlistUserSelection,
227+
});
228+
229+
if (!user) {
230+
throw new Error("User not found");
231+
}
232+
233+
if (!user.email) {
234+
throw new Error("User email is missing");
235+
}
236+
237+
const founderEmail = env.FOUNDER_EMAIL ?? undefined;
238+
const fallbackFrom = env.FROM_EMAIL ?? env.ADMIN_EMAIL ?? undefined;
239+
240+
const replyTo = founderEmail ?? fallbackFrom;
241+
242+
if (!replyTo) {
243+
throw new Error("No sender email configured");
244+
}
245+
246+
const fromOverride = founderEmail ?? undefined;
247+
248+
const text = [
249+
"Hello,",
250+
"",
251+
"Sorry, We cannot proceed with this request at this time, this might affect useSend\u2019s sending reputation.",
252+
"",
253+
"",
254+
"cheers,",
255+
"koushik - useSend.com",
256+
].join("\n");
257+
258+
try {
259+
await sendMail(
260+
user.email,
261+
"useSend: Waitlist request update",
262+
text,
263+
toPlainHtml(text),
264+
replyTo,
265+
fromOverride
266+
);
267+
} catch (error) {
268+
logger.error(
269+
{ userId: user.id, error },
270+
"Failed to send waitlist rejection email"
271+
);
272+
throw new Error("Failed to send waitlist rejection email");
273+
}
274+
275+
return { sent: true };
276+
}),
277+
217278
findTeam: adminProcedure
218279
.input(
219280
z.object({

0 commit comments

Comments
 (0)