Skip to content

Commit ee2091b

Browse files
authored
feat: Email change should trigger update on profiles table (#30)
chore: git ignore cache folder feat: implement Dialog UI component feat: show dialog informing user about email confirmation need
1 parent ea4c378 commit ee2091b

File tree

6 files changed

+222
-20
lines changed

6 files changed

+222
-20
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Dependencies
22
node_modules
3+
**/cache/
34

45
# Turbo
56
.turbo

apps/next/components/ui/dialog.tsx

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import * as React from "react"
2+
import * as DialogPrimitive from "@radix-ui/react-dialog"
3+
import { Cross2Icon } from "@radix-ui/react-icons"
4+
5+
import { cn } from "@/lib/utils"
6+
7+
const Dialog = DialogPrimitive.Root
8+
9+
const DialogTrigger = DialogPrimitive.Trigger
10+
11+
const DialogClose = DialogPrimitive.Close
12+
13+
const DialogPortal = DialogPrimitive.Portal
14+
15+
const DialogOverlay = React.forwardRef<
16+
React.ElementRef<typeof DialogPrimitive.Overlay>,
17+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
18+
>(({ className, ...props }, ref) => (
19+
<DialogPrimitive.Overlay
20+
className={cn(
21+
"fixed inset-0 z-50 bg-background/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22+
className
23+
)}
24+
{...props}
25+
ref={ref}
26+
/>
27+
))
28+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29+
30+
interface DialogContentProps
31+
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {}
32+
33+
const DialogContent = React.forwardRef<
34+
React.ElementRef<typeof DialogPrimitive.Content>,
35+
DialogContentProps
36+
>(({ className, children, ...props }, ref) => (
37+
<DialogPortal>
38+
<DialogOverlay />
39+
<DialogPrimitive.Content
40+
ref={ref}
41+
className="fixed z-50 flex h-screen w-screen items-center justify-center"
42+
{...props}
43+
>
44+
<div
45+
className={cn(
46+
"relative rounded-lg border border-gray-100 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
47+
className
48+
)}
49+
>
50+
<DialogClose className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
51+
<Cross2Icon className="size-5" />
52+
</DialogClose>
53+
{children}
54+
</div>
55+
</DialogPrimitive.Content>
56+
</DialogPortal>
57+
))
58+
DialogContent.displayName = DialogPrimitive.Content.displayName
59+
60+
const DialogTitle = React.forwardRef<
61+
React.ElementRef<typeof DialogPrimitive.Title>,
62+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
63+
>(({ className, ...props }, ref) => (
64+
<DialogPrimitive.Title
65+
ref={ref}
66+
className={cn("text-lg font-semibold text-foreground", className)}
67+
{...props}
68+
/>
69+
))
70+
DialogTitle.displayName = DialogPrimitive.Title.displayName
71+
72+
const DialogDescription = React.forwardRef<
73+
React.ElementRef<typeof DialogPrimitive.Description>,
74+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
75+
>(({ className, ...props }, ref) => (
76+
<DialogPrimitive.Description
77+
ref={ref}
78+
className={cn("text-sm text-muted-foreground", className)}
79+
{...props}
80+
/>
81+
))
82+
DialogDescription.displayName = DialogPrimitive.Description.displayName
83+
84+
const DialogHeader = ({
85+
className,
86+
children,
87+
...props
88+
}: React.HTMLAttributes<HTMLDivElement>) => (
89+
<div className={cn("w-full pr-6", className)} {...props}>
90+
<DialogTitle>{children}</DialogTitle>
91+
</div>
92+
)
93+
DialogHeader.displayName = "DialogHeader"
94+
95+
const DialogFooter = ({
96+
className,
97+
...props
98+
}: React.HTMLAttributes<HTMLDivElement>) => (
99+
<div
100+
className={cn(
101+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
102+
className
103+
)}
104+
{...props}
105+
/>
106+
)
107+
DialogFooter.displayName = "DialogFooter"
108+
109+
export {
110+
Dialog,
111+
DialogPortal,
112+
DialogOverlay,
113+
DialogTrigger,
114+
DialogClose,
115+
DialogContent,
116+
DialogHeader,
117+
DialogFooter,
118+
DialogTitle,
119+
DialogDescription,
120+
}

apps/next/components/user/credentials-form.tsx

+64-19
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ import {
1717
CardHeader,
1818
CardTitle,
1919
} from "@/components/ui/card"
20+
import {
21+
Dialog,
22+
DialogClose,
23+
DialogContent,
24+
DialogDescription,
25+
DialogFooter,
26+
DialogHeader,
27+
} from "@/components/ui/dialog"
2028
import {
2129
Form,
2230
FormControl,
@@ -33,34 +41,71 @@ import { updateUser } from "@/modules/user/auth"
3341
export const CredentialsForm: React.FC<{ userEmail: string }> = ({
3442
userEmail,
3543
}) => {
44+
const [emailChangeInfoDialogState, setEmailChangeInfoDialogState] =
45+
React.useState<{ isOpen: boolean; newEmail: string | null }>({
46+
isOpen: false,
47+
newEmail: null,
48+
})
49+
3650
const update = useMutation({
3751
mutationFn: updateUser,
52+
onSuccess: (_, vars) => {
53+
if (vars.email && vars.email !== userEmail)
54+
setEmailChangeInfoDialogState({ isOpen: true, newEmail: vars.email })
55+
},
3856
})
3957

4058
const digest = getDigest(update.error)
4159

4260
return (
43-
<CredentialsFormComponent
44-
userEmail={userEmail}
45-
updateUser={({
46-
email,
47-
password,
48-
}: {
49-
email?: string
50-
password?: string
51-
}) => {
52-
update.mutate({
61+
<>
62+
<CredentialsFormComponent
63+
userEmail={userEmail}
64+
updateUser={({
5365
email,
5466
password,
55-
redirect: {
56-
url: "/settings/credentials",
57-
},
58-
})
59-
}}
60-
isPending={update.isPending}
61-
isError={update.isError}
62-
errorMessage={`Credentials update was not successful, please try again; ref: ${digest}`}
63-
/>
67+
}: {
68+
email?: string
69+
password?: string
70+
}) => {
71+
update.mutate({
72+
email,
73+
password,
74+
redirect: {
75+
url: "/settings/credentials",
76+
},
77+
})
78+
}}
79+
isPending={update.isPending}
80+
isError={update.isError}
81+
errorMessage={`Credentials update was not successful, please try again; ref: ${digest}`}
82+
/>
83+
<Dialog
84+
open={emailChangeInfoDialogState.isOpen}
85+
onOpenChange={(open) =>
86+
setEmailChangeInfoDialogState((prevState) => ({
87+
...prevState,
88+
isOpen: open,
89+
}))
90+
}
91+
>
92+
<DialogContent className="flex max-w-[480px] flex-col gap-4">
93+
<DialogHeader>Email change needs to be confirmed</DialogHeader>
94+
<DialogDescription>
95+
In order to successfully change the email this need to be confirmed
96+
for <b>both</b> email addresses. The confirmation links have been
97+
sent to <b>{emailChangeInfoDialogState.newEmail}</b> and{" "}
98+
<b>{userEmail}</b>. Please check your inboxes and follow the
99+
instructions to confirm the change.
100+
</DialogDescription>
101+
<DialogFooter>
102+
<DialogClose asChild>
103+
<Button variant="secondary">Close</Button>
104+
</DialogClose>
105+
</DialogFooter>
106+
</DialogContent>
107+
</Dialog>
108+
</>
64109
)
65110
}
66111

apps/next/modules/user/migrations.sql

+18-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ AS $function$BEGIN
4141
END;$function$
4242
;
4343

44+
CREATE OR REPLACE FUNCTION public.update_user_email()
45+
RETURNS trigger
46+
LANGUAGE plpgsql
47+
SECURITY DEFINER
48+
AS $function$BEGIN
49+
UPDATE public.profiles
50+
SET email = NEW.email
51+
WHERE id = NEW.id;
52+
53+
RETURN NEW;
54+
END;$function$
55+
;
56+
4457
grant delete on table "public"."profiles" to "anon";
4558

4659
grant insert on table "public"."profiles" to "anon";
@@ -116,4 +129,8 @@ using ((auth.uid() = id))
116129
with check ((auth.uid() = id));
117130

118131

119-
CREATE TRIGGER handle_new_user_trigger AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user();
132+
CREATE TRIGGER handle_new_user_trigger AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user();
133+
134+
CREATE TRIGGER update_email_in_profiles_trigger AFTER UPDATE OF email ON auth.users FOR EACH ROW EXECUTE FUNCTION update_user_email();
135+
136+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
set check_function_bodies = off;
2+
3+
CREATE OR REPLACE FUNCTION public.update_user_email()
4+
RETURNS trigger
5+
LANGUAGE plpgsql
6+
SECURITY DEFINER
7+
AS $function$BEGIN
8+
UPDATE public.profiles
9+
SET email = NEW.email
10+
WHERE id = NEW.id;
11+
12+
RETURN NEW;
13+
END;$function$
14+
;
15+
16+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE TRIGGER update_email_in_profiles_trigger AFTER UPDATE OF email ON auth.users FOR EACH ROW EXECUTE FUNCTION update_user_email();
2+
3+

0 commit comments

Comments
 (0)