feat: ability to update email (#5370)
* chore: update email * wip: verify email endpoint * wip: implement success screen * wip: social warning * Update form for email update * Update email form UI --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>pull/5417/head
parent
4e96a58e54
commit
730af9b973
9 changed files with 430 additions and 67 deletions
@ -0,0 +1,82 @@ |
|||||||
|
import { useEffect, useState } from 'react'; |
||||||
|
import { httpPatch } from '../../lib/http'; |
||||||
|
import { setAuthToken } from '../../lib/jwt'; |
||||||
|
import { Spinner } from '../ReactIcons/Spinner'; |
||||||
|
import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2'; |
||||||
|
import { getUrlParams } from '../../lib/browser'; |
||||||
|
import { CheckIcon } from '../ReactIcons/CheckIcon'; |
||||||
|
|
||||||
|
export function TriggerVerifyEmail() { |
||||||
|
const { code } = getUrlParams() as { code: string }; |
||||||
|
|
||||||
|
// const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [status, setStatus] = useState<'loading' | 'error' | 'success'>( |
||||||
|
'loading', |
||||||
|
); |
||||||
|
const [error, setError] = useState(''); |
||||||
|
|
||||||
|
const triggerVerify = (code: string) => { |
||||||
|
setStatus('loading'); |
||||||
|
|
||||||
|
httpPatch<{ token: string }>( |
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-verify-new-email/${code}`, |
||||||
|
{}, |
||||||
|
) |
||||||
|
.then(({ response, error }) => { |
||||||
|
if (!response?.token) { |
||||||
|
setError(error?.message || 'Something went wrong. Please try again.'); |
||||||
|
setStatus('error'); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setAuthToken(response.token); |
||||||
|
setStatus('success'); |
||||||
|
}) |
||||||
|
.catch((err) => { |
||||||
|
setStatus('error'); |
||||||
|
setError('Something went wrong. Please try again.'); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!code) { |
||||||
|
setStatus('error'); |
||||||
|
setError('Something went wrong. Please try again later.'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
triggerVerify(code); |
||||||
|
}, [code]); |
||||||
|
|
||||||
|
const isLoading = status === 'loading'; |
||||||
|
if (status === 'success') { |
||||||
|
return ( |
||||||
|
<div className="mx-auto flex max-w-md flex-col items-center pt-0 sm:pt-12"> |
||||||
|
<CheckIcon additionalClasses={'h-16 w-16 opacity-100'} /> |
||||||
|
<h2 className="mb-1 mt-4 text-center text-xl font-semibold sm:mb-3 sm:mt-4 sm:text-2xl"> |
||||||
|
Email Update Successful |
||||||
|
</h2> |
||||||
|
<p className="text-sm sm:text-base"> |
||||||
|
Your email has been changed successfully. Happy learning! |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="mx-auto flex max-w-md flex-col items-center pt-0 sm:pt-12"> |
||||||
|
<div className="mx-auto max-w-md text-center"> |
||||||
|
{isLoading && <Spinner className="mx-auto h-16 w-16" />} |
||||||
|
{error && <ErrorIcon2 className="mx-auto h-16 w-16" />} |
||||||
|
<h2 className="mb-1 mt-4 text-center text-xl font-semibold sm:mb-3 sm:mt-4 sm:text-2xl"> |
||||||
|
Verifying your new Email |
||||||
|
</h2> |
||||||
|
<div className="text-sm sm:text-base"> |
||||||
|
{isLoading && <p>Please wait while we verify your new Email..</p>} |
||||||
|
{error && <p className="text-red-700">{error}</p>} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
import { useEffect, useState } from 'react'; |
||||||
|
import { UpdateEmailForm } from '../UpdateEmail/UpdateEmailForm'; |
||||||
|
import UpdatePasswordForm from '../UpdatePassword/UpdatePasswordForm'; |
||||||
|
import { pageProgressMessage } from '../../stores/page'; |
||||||
|
import { httpGet } from '../../lib/http'; |
||||||
|
import { useToast } from '../../hooks/use-toast'; |
||||||
|
|
||||||
|
export function ProfileSettingsPage() { |
||||||
|
const toast = useToast(); |
||||||
|
|
||||||
|
const [authProvider, setAuthProvider] = useState(''); |
||||||
|
const [currentEmail, setCurrentEmail] = useState(''); |
||||||
|
const [newEmail, setNewEmail] = useState(''); |
||||||
|
|
||||||
|
const loadProfile = async () => { |
||||||
|
const { error, response } = await httpGet( |
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-me`, |
||||||
|
); |
||||||
|
|
||||||
|
if (error || !response) { |
||||||
|
toast.error(error?.message || 'Something went wrong'); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const { authProvider, email, newEmail } = response; |
||||||
|
setAuthProvider(authProvider); |
||||||
|
setCurrentEmail(email); |
||||||
|
setNewEmail(newEmail || ''); |
||||||
|
}; |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
loadProfile().finally(() => { |
||||||
|
pageProgressMessage.set(''); |
||||||
|
}); |
||||||
|
}, []); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<UpdatePasswordForm authProvider={authProvider} /> |
||||||
|
<hr className="my-8" /> |
||||||
|
<UpdateEmailForm |
||||||
|
authProvider={authProvider} |
||||||
|
currentEmail={currentEmail} |
||||||
|
newEmail={newEmail} |
||||||
|
key={newEmail} |
||||||
|
onSendVerificationCode={(newEmail) => { |
||||||
|
setNewEmail(newEmail); |
||||||
|
loadProfile().finally(() => {}); |
||||||
|
}} |
||||||
|
onVerificationCancel={() => { |
||||||
|
loadProfile().finally(() => {}); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,245 @@ |
|||||||
|
import { type FormEvent, useState } from 'react'; |
||||||
|
import { httpPatch } from '../../lib/http'; |
||||||
|
import { pageProgressMessage } from '../../stores/page'; |
||||||
|
import { useToast } from '../../hooks/use-toast'; |
||||||
|
import { cn } from '../../lib/classname'; |
||||||
|
import { ArrowUpRight, X } from 'lucide-react'; |
||||||
|
|
||||||
|
type UpdateEmailFormProps = { |
||||||
|
authProvider: string; |
||||||
|
currentEmail: string; |
||||||
|
newEmail?: string; |
||||||
|
onSendVerificationCode?: (newEmail: string) => void; |
||||||
|
onVerificationCancel?: () => void; |
||||||
|
}; |
||||||
|
|
||||||
|
export function UpdateEmailForm(props: UpdateEmailFormProps) { |
||||||
|
const { |
||||||
|
authProvider, |
||||||
|
currentEmail, |
||||||
|
newEmail: defaultNewEmail = '', |
||||||
|
onSendVerificationCode, |
||||||
|
onVerificationCancel, |
||||||
|
} = props; |
||||||
|
const toast = useToast(); |
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false); |
||||||
|
const [isSubmitted, setIsSubmitted] = useState(defaultNewEmail !== ''); |
||||||
|
const [newEmail, setNewEmail] = useState(defaultNewEmail); |
||||||
|
const [isResendDone, setIsResendDone] = useState(false); |
||||||
|
|
||||||
|
const handleSentVerificationCode = async (e: FormEvent<HTMLFormElement>) => { |
||||||
|
e.preventDefault(); |
||||||
|
if (!newEmail || !newEmail.includes('@') || isSubmitted) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setIsLoading(true); |
||||||
|
pageProgressMessage.set('Sending verification code'); |
||||||
|
const { response, error } = await httpPatch( |
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-update-user-email`, |
||||||
|
{ email: newEmail }, |
||||||
|
); |
||||||
|
|
||||||
|
if (error || !response) { |
||||||
|
toast.error(error?.message || 'Something went wrong'); |
||||||
|
setIsLoading(false); |
||||||
|
pageProgressMessage.set(''); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
pageProgressMessage.set(''); |
||||||
|
setIsLoading(false); |
||||||
|
setIsSubmitted(true); |
||||||
|
onSendVerificationCode?.(newEmail); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleResendVerificationCode = async () => { |
||||||
|
if (isResendDone) { |
||||||
|
toast.error('You have already resent the verification code'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setIsLoading(true); |
||||||
|
pageProgressMessage.set('Resending verification code'); |
||||||
|
const { response, error } = await httpPatch( |
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-resend-email-verification-code`, |
||||||
|
{ email: newEmail }, |
||||||
|
); |
||||||
|
|
||||||
|
if (error || !response) { |
||||||
|
toast.error(error?.message || 'Something went wrong'); |
||||||
|
setIsLoading(false); |
||||||
|
pageProgressMessage.set(''); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
toast.success('Verification code has been resent'); |
||||||
|
pageProgressMessage.set(''); |
||||||
|
setIsResendDone(true); |
||||||
|
setIsLoading(false); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleCancelEmailVerification = async () => { |
||||||
|
setIsLoading(true); |
||||||
|
pageProgressMessage.set('Cancelling email verification'); |
||||||
|
const { response, error } = await httpPatch( |
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-cancel-email-verification`, |
||||||
|
{}, |
||||||
|
); |
||||||
|
|
||||||
|
if (error || !response) { |
||||||
|
toast.error(error?.message || 'Something went wrong'); |
||||||
|
setIsLoading(false); |
||||||
|
pageProgressMessage.set(''); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
pageProgressMessage.set(''); |
||||||
|
onVerificationCancel?.(); |
||||||
|
setIsSubmitted(false); |
||||||
|
setNewEmail(''); |
||||||
|
setIsLoading(false); |
||||||
|
}; |
||||||
|
|
||||||
|
if (authProvider && authProvider !== 'email') { |
||||||
|
return ( |
||||||
|
<div className="block"> |
||||||
|
<h2 className="text-xl font-bold sm:text-2xl">Update Email</h2> |
||||||
|
<p className="mt-2 text-gray-400"> |
||||||
|
You have used {authProvider} when signing up. Please set your password |
||||||
|
first. |
||||||
|
</p> |
||||||
|
|
||||||
|
<div className="mt-4 flex w-full flex-col"> |
||||||
|
<label |
||||||
|
htmlFor="current-email" |
||||||
|
className="text-sm leading-none text-slate-500" |
||||||
|
> |
||||||
|
Current Email |
||||||
|
</label> |
||||||
|
<input |
||||||
|
type="email" |
||||||
|
name="current-email" |
||||||
|
id="current-email" |
||||||
|
autoComplete="current-email" |
||||||
|
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||||
|
required |
||||||
|
disabled |
||||||
|
value={currentEmail} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<p className="mt-3 rounded-lg border border-red-600 px-2 py-1 text-red-600"> |
||||||
|
Please set your password first to update your email. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<div className="mb-8 block"> |
||||||
|
<h2 className="text-xl font-bold sm:text-2xl">Update Email</h2> |
||||||
|
<p className="mt-2 text-gray-400"> |
||||||
|
Use the form below to update your email. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<form onSubmit={handleSentVerificationCode} className="space-y-4"> |
||||||
|
<div className="flex w-full flex-col"> |
||||||
|
<label |
||||||
|
htmlFor="current-email" |
||||||
|
className="text-sm leading-none text-slate-500" |
||||||
|
> |
||||||
|
Current Email |
||||||
|
</label> |
||||||
|
<input |
||||||
|
type="email" |
||||||
|
name="current-email" |
||||||
|
id="current-email" |
||||||
|
autoComplete="current-email" |
||||||
|
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||||
|
required |
||||||
|
disabled |
||||||
|
value={currentEmail} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
className={cn('flex w-full flex-col', { |
||||||
|
'rounded-lg border border-green-500 p-3': isSubmitted, |
||||||
|
})} |
||||||
|
> |
||||||
|
<div className="flex items-center justify-between gap-2"> |
||||||
|
<label |
||||||
|
htmlFor="new-email" |
||||||
|
className="text-sm leading-none text-slate-500" |
||||||
|
> |
||||||
|
New Email |
||||||
|
</label> |
||||||
|
|
||||||
|
{isSubmitted && ( |
||||||
|
<div className="flex items-center gap-2"> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
onClick={handleResendVerificationCode} |
||||||
|
disabled={isLoading || isResendDone} |
||||||
|
className="flex items-center gap-1 text-sm font-medium leading-none text-green-600 transition-colors hover:text-green-700" |
||||||
|
> |
||||||
|
<span className="hidden sm:block"> |
||||||
|
Resend Verification Link |
||||||
|
</span> |
||||||
|
<span className="sm:hidden">Resend Code</span> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
<input |
||||||
|
type="email" |
||||||
|
name="new-email" |
||||||
|
id="new-email" |
||||||
|
autoComplete={'new-email'} |
||||||
|
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||||
|
required |
||||||
|
placeholder="Enter new email" |
||||||
|
value={newEmail} |
||||||
|
onChange={(e) => setNewEmail(e.target.value)} |
||||||
|
disabled={isSubmitted} |
||||||
|
/> |
||||||
|
{!isSubmitted && ( |
||||||
|
<button |
||||||
|
type="submit" |
||||||
|
disabled={ |
||||||
|
isLoading || !newEmail || !newEmail.includes('@') || isSubmitted |
||||||
|
} |
||||||
|
className="mt-3 inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400" |
||||||
|
> |
||||||
|
{isLoading ? 'Please wait...' : 'Send Verification Link'} |
||||||
|
</button> |
||||||
|
)} |
||||||
|
{isSubmitted && ( |
||||||
|
<> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
onClick={handleCancelEmailVerification} |
||||||
|
disabled={isLoading} |
||||||
|
className="font-regular mt-4 w-full rounded-lg border border-red-600 py-2 text-sm text-red-600 outline-none transition-colors hover:bg-red-500 hover:text-white focus:ring-2 focus:ring-red-500 focus:ring-offset-1" |
||||||
|
> |
||||||
|
Cancel Update |
||||||
|
</button> |
||||||
|
<div className="mt-3 flex items-center gap-2 rounded-lg bg-green-100 p-4"> |
||||||
|
<span className="text-sm text-green-800"> |
||||||
|
A verification link has been sent to your{' '} |
||||||
|
<span>new email address</span>. Please follow the instructions |
||||||
|
in email to verify and update your email. |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
--- |
||||||
|
import AccountLayout from '../layouts/AccountLayout.astro'; |
||||||
|
import { TriggerVerifyEmail } from '../components/AuthenticationFlow/TriggerVerifyEmail'; |
||||||
|
--- |
||||||
|
|
||||||
|
<AccountLayout title='Verify email' noIndex={true}> |
||||||
|
<div class='container py-16'> |
||||||
|
<TriggerVerifyEmail client:load /> |
||||||
|
</div> |
||||||
|
</AccountLayout> |
Loading…
Reference in new issue