Skip to content

Commit b2987d3

Browse files
committed
Working email/password auth
1 parent 1ce458b commit b2987d3

File tree

7 files changed

+561
-3
lines changed

7 files changed

+561
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React, { useState, useEffect } from "react";
2+
import {
3+
Box,
4+
Button,
5+
Container,
6+
Heading,
7+
Text,
8+
VStack,
9+
useToast,
10+
Flex,
11+
} from "@chakra-ui/react";
12+
import { sendEmailVerification } from "firebase/auth";
13+
import { useAuth } from "../../context/auth";
14+
import { useRouter } from "next/router";
15+
import { Pages } from "../../utils/consts";
16+
import { auth } from "../../utils/firebase/firebaseClient";
17+
18+
interface EmailVerificationProps {
19+
onVerificationComplete?: () => void;
20+
}
21+
22+
const EmailVerification: React.FC<EmailVerificationProps> = ({
23+
onVerificationComplete,
24+
}) => {
25+
const { user, loading } = useAuth();
26+
const router = useRouter();
27+
const toast = useToast();
28+
const [isResending, setIsResending] = useState(false);
29+
const [countdown, setCountdown] = useState(0);
30+
31+
useEffect(() => {
32+
// If user is verified, trigger completion callback or redirect
33+
if (!loading && user?.emailVerified) {
34+
if (onVerificationComplete) {
35+
onVerificationComplete();
36+
} else {
37+
router.replace(Pages.FEED);
38+
}
39+
}
40+
}, [user, loading, router, onVerificationComplete]);
41+
42+
useEffect(() => {
43+
// Countdown timer for resend button
44+
if (countdown > 0) {
45+
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
46+
return () => clearTimeout(timer);
47+
}
48+
}, [countdown]);
49+
50+
const handleCheckVerification = async () => {
51+
if (user) {
52+
try {
53+
// Force token refresh to get latest emailVerified status
54+
await user.reload();
55+
const currentUser = auth.currentUser;
56+
57+
if (currentUser?.emailVerified) {
58+
// Force a token refresh to update the ID token with the new emailVerified status
59+
await currentUser.getIdToken(true);
60+
61+
toast({
62+
title: "Email verified!",
63+
description: "Redirecting you to the application...",
64+
status: "success",
65+
duration: 3000,
66+
isClosable: true,
67+
});
68+
69+
if (onVerificationComplete) {
70+
onVerificationComplete();
71+
} else {
72+
router.replace(Pages.FEED);
73+
}
74+
} else {
75+
toast({
76+
title: "Email not verified",
77+
description: "Please check your inbox and verify your email",
78+
status: "info",
79+
duration: 3000,
80+
isClosable: true,
81+
});
82+
}
83+
} catch (error: any) {
84+
toast({
85+
title: "Error checking verification status",
86+
description: error.message,
87+
status: "error",
88+
duration: 3000,
89+
isClosable: true,
90+
});
91+
}
92+
}
93+
};
94+
95+
const handleResendVerification = async () => {
96+
if (!user) return;
97+
98+
setIsResending(true);
99+
try {
100+
await sendEmailVerification(user);
101+
setCountdown(60); // 60 second cooldown
102+
103+
toast({
104+
title: "Verification email sent",
105+
description: "Please check your inbox and spam folder",
106+
status: "success",
107+
duration: 5000,
108+
isClosable: true,
109+
});
110+
} catch (error: any) {
111+
toast({
112+
title: "Error sending verification email",
113+
description: error.message,
114+
status: "error",
115+
duration: 5000,
116+
isClosable: true,
117+
});
118+
} finally {
119+
setIsResending(false);
120+
}
121+
};
122+
123+
if (loading) {
124+
return (
125+
<Container centerContent maxW="container.md" py={10}>
126+
<Text>Loading...</Text>
127+
</Container>
128+
);
129+
}
130+
131+
return (
132+
<Container centerContent maxW="container.md" py={10}>
133+
<Flex direction="column" align="center" w="full">
134+
<Box p={8} shadow="lg" borderRadius="lg" bg="white" w="full" maxW="md">
135+
<VStack spacing={6} align="center">
136+
<Heading size="lg" textAlign="center">
137+
Verify Your Email
138+
</Heading>
139+
140+
<Text textAlign="center">
141+
We&apos;ve sent a verification email to{" "}
142+
<strong>{user?.email}</strong>. Please check your inbox and click
143+
the verification link to continue.
144+
</Text>
145+
146+
<Text fontSize="sm" color="gray.600" textAlign="center">
147+
If you don&apos;t see the email, check your spam folder or request
148+
a new verification email.
149+
</Text>
150+
151+
<VStack spacing={4} w="full">
152+
<Button
153+
colorScheme="blue"
154+
w="full"
155+
onClick={handleCheckVerification}
156+
>
157+
I&apos;ve Verified My Email
158+
</Button>
159+
160+
<Button
161+
variant="outline"
162+
colorScheme="blue"
163+
w="full"
164+
onClick={handleResendVerification}
165+
isLoading={isResending}
166+
isDisabled={countdown > 0}
167+
>
168+
{countdown > 0
169+
? `Resend Email (${countdown}s)`
170+
: "Resend Verification Email"}
171+
</Button>
172+
</VStack>
173+
</VStack>
174+
</Box>
175+
</Flex>
176+
</Container>
177+
);
178+
};
179+
180+
export default EmailVerification;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React, { useState } from "react";
2+
import {
3+
Modal,
4+
ModalOverlay,
5+
ModalContent,
6+
ModalHeader,
7+
ModalBody,
8+
ModalCloseButton,
9+
Button,
10+
FormControl,
11+
FormLabel,
12+
Input,
13+
Stack,
14+
Text,
15+
useToast,
16+
} from "@chakra-ui/react";
17+
import { sendPasswordResetEmail } from "firebase/auth";
18+
import { auth } from "../../utils/firebase/firebaseClient";
19+
20+
interface ForgotPasswordModalProps {
21+
isOpen: boolean;
22+
onClose: () => void;
23+
onBackToLogin: () => void;
24+
}
25+
26+
const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({
27+
isOpen,
28+
onClose,
29+
onBackToLogin,
30+
}) => {
31+
const [email, setEmail] = useState("");
32+
const [isLoading, setIsLoading] = useState(false);
33+
const toast = useToast();
34+
35+
const resetForm = () => {
36+
setEmail("");
37+
setIsLoading(false);
38+
};
39+
40+
const handleClose = () => {
41+
resetForm();
42+
onClose();
43+
};
44+
45+
const handleSubmit = async (e: React.FormEvent) => {
46+
e.preventDefault();
47+
setIsLoading(true);
48+
49+
try {
50+
await sendPasswordResetEmail(auth, email);
51+
toast({
52+
title: "Reset Email Sent",
53+
description: "Check your email for a password reset link",
54+
status: "success",
55+
duration: 5000,
56+
isClosable: true,
57+
position: "top",
58+
});
59+
handleClose();
60+
} catch (error: any) {
61+
setIsLoading(false);
62+
toast({
63+
title: "Error",
64+
description: error.message,
65+
status: "error",
66+
duration: 5000,
67+
isClosable: true,
68+
position: "top",
69+
});
70+
}
71+
};
72+
73+
return (
74+
<Modal isOpen={isOpen} onClose={handleClose} size="md">
75+
<ModalOverlay />
76+
<ModalContent>
77+
<ModalHeader>Reset Password</ModalHeader>
78+
<ModalCloseButton />
79+
<ModalBody>
80+
<Text mb={4}>
81+
Enter your email address and we&apos;ll send you a link to reset
82+
your password.
83+
</Text>
84+
<form onSubmit={handleSubmit}>
85+
<Stack spacing={4}>
86+
<FormControl id="email" isRequired>
87+
<FormLabel>Email</FormLabel>
88+
<Input
89+
type="email"
90+
value={email}
91+
onChange={(e) => setEmail(e.target.value)}
92+
placeholder="your-email@example.com"
93+
/>
94+
</FormControl>
95+
<Button
96+
type="submit"
97+
colorScheme="blue"
98+
isLoading={isLoading}
99+
loadingText="Sending"
100+
width="100%"
101+
>
102+
Send Reset Link
103+
</Button>
104+
<Button variant="ghost" width="100%" onClick={onBackToLogin}>
105+
Back to Login
106+
</Button>
107+
</Stack>
108+
</form>
109+
</ModalBody>
110+
</ModalContent>
111+
</Modal>
112+
);
113+
};
114+
115+
export default ForgotPasswordModal;

0 commit comments

Comments
 (0)