Browse Source

enhance(ui): implement countdown timer for password reset and enhance login flow

charlie 3 weeks ago
parent
commit
4decd81725
2 changed files with 142 additions and 36 deletions
  1. 1 1
      packages/ui/examples/index.tsx
  2. 141 35
      packages/ui/src/amplify/ui.tsx

+ 1 - 1
packages/ui/examples/index.tsx

@@ -15,7 +15,7 @@ init()
 
 function App() {
   const [errors, setErrors] = React.useState<string | null>(null)
-  const [currentTab, setCurrentTab] = React.useState<'login' | 'reset' | 'signup' | 'confirm-code' | any>('confirm-code')
+  const [currentTab, setCurrentTab] = React.useState<'login' | 'reset' | 'signup' | 'confirm-code' | any>('login')
   const onSessionCallback = React.useCallback((session: any) => {
     console.log('==>>session callback:', session)
   }, [])

+ 141 - 35
packages/ui/src/amplify/ui.tsx

@@ -80,13 +80,37 @@ function InputRow(
 function FormGroup(props: FormHTMLAttributes<any>) {
   const { className, children, ...reset } = props
   return (
-    <form className={cn('flex flex-col justify-center items-center gap-4 w-full', className)}
+    <form className={cn('relative flex flex-col justify-center items-center gap-4 w-full', className)}
           {...reset}>
       {children}
     </form>
   )
 }
 
+function useCountDown() {
+  const [countDownNum, setCountDownNum] = useState<number>(0)
+  const startCountDown = () => {
+    setCountDownNum(60)
+    const interval = setInterval(() => {
+      setCountDownNum((num) => {
+        if (num <= 1) {
+          clearInterval(interval)
+          return 0
+        }
+        return num - 1
+      })
+    }, 1000)
+  }
+
+  useEffect(() => {
+    return () => {
+      setCountDownNum(0)
+    }
+  }, [])
+
+  return { countDownNum, startCountDown, setCountDownNum }
+}
+
 export function LoginForm() {
   const { setErrors, setCurrentTab, onSessionCallback } = useAuthFormState()
   const [loading, setLoading] = useState<boolean>(false)
@@ -237,8 +261,6 @@ export function SignupForm() {
 
         try {
           setLoading(true)
-          await new Promise(resolve => { setTimeout(resolve, 500) })
-
           const ret = await Auth.signUp({
             username: data.username as string,
             password: data.password as string,
@@ -251,7 +273,14 @@ export function SignupForm() {
 
           if (ret.isSignUpComplete) {
             // TODO: auto sign in
-            console.log(ret)
+            if (ret.nextStep?.signUpStep === 'COMPLETE_AUTO_SIGN_IN') {
+              const { nextStep } = await Auth.autoSignIn()
+              if (nextStep.signInStep === 'DONE') {
+                // signed in
+                setCurrentTab('login')
+              }
+            }
+
             setCurrentTab('login')
             return
           } else {
@@ -320,27 +349,106 @@ export function SignupForm() {
 
 export function ResetPasswordForm() {
   const [isSentCode, setIsSentCode] = useState<boolean>(false)
-  const { setCurrentTab } = useAuthFormState()
+  const [sentUsername, setSentUsername] = useState<string>('')
+  const { setCurrentTab, setErrors } = useAuthFormState()
+  const { countDownNum, startCountDown } = useCountDown()
+  const [loading, setLoading] = useState<boolean>(false)
+
+  useEffect(() => {
+    setErrors({})
+  }, [isSentCode])
 
   return (
     <FormGroup
       autoComplete={'off'}
-      onSubmit={(e) => {
+      onSubmit={async (e) => {
+        setErrors(null)
         e.preventDefault()
 
         // get submit form input data
         const formData = new FormData(e.target as HTMLFormElement)
         const data = Object.fromEntries(formData.entries())
-        console.log(data)
 
-        setIsSentCode(true)
+        if (!isSentCode) {
+          try {
+            setLoading(true)
+
+            const username = (data.email as string)?.trim()
+            // send reset code
+            const ret = await Auth.resetPassword({ username })
+            console.debug('[Auth] reset pw code sent: ', ret)
+            setSentUsername(username)
+            startCountDown()
+            setIsSentCode(true)
+          } catch (error) {
+            console.error('Error sending reset code:', error)
+            setErrors({ email: { message: (error as Error).message, title: t('Bad Response.') } })
+          } finally {
+            setLoading(false)
+          }
+        } else {
+          // confirm reset password
+          if ((data.password as string)?.length < 8) {
+            setErrors({
+              password: {
+                message: t('Password must be at least 8 characters.'),
+                title: t('Invalid Password')
+              }
+            })
+            return
+          } else if (data.password !== data.confirm_password) {
+            setErrors({
+              confirm_password: {
+                message: t('Passwords do not match.'),
+                title: t('Invalid Password')
+              }
+            })
+            return
+          } else {
+            try {
+              setLoading(true)
+              const ret = await Auth.confirmResetPassword({
+                username: sentUsername,
+                newPassword: data.password as string,
+                confirmationCode: data.code as string
+              })
+
+              console.debug('[Auth] confirm reset pw: ', ret)
+              setCurrentTab('login')
+            } catch (error) {
+              console.error('Error confirming reset password:', error)
+              setErrors({ 'confirm_password': { message: (error as Error).message, title: t('Bad Response.') } })
+            } finally {
+              setLoading(false)
+            }
+          }
+        }
       }}>
       {isSentCode ? (
         <>
+          <div className={'w-full opacity-60 flex justify-end relative h-0 z-[2]'}>
+            {countDownNum > 0 ? (
+              <span className={'text-sm opacity-50 select-none absolute top-3 right-0'}>
+                {countDownNum}s
+              </span>
+            ) : (<a onClick={async () => {
+              startCountDown()
+              try {
+                const ret = await Auth.resetPassword({ username: sentUsername })
+                console.debug('[Auth] reset pw code re-sent: ', ret)
+              } catch (error) {
+                console.error('Error resending reset code:', error)
+                setErrors({ email: { message: (error as Error).message, title: t('Bad Response.') } })
+              } finally {}
+            }} className={'text-sm opacity-70 hover:opacity-90 underline absolute top-3 right-0 select-none'}>
+              {t('Resend code')}
+            </a>)}
+          </div>
           <InputRow id="code" type="text" name="code" required={true}
                     placeholder={'123456'}
                     autoComplete={'off'}
                     label={t('Enter the code sent to your email')}/>
+
           <InputRow id="password" type="password" name="password" required={true}
                     placeholder={t('New Password')}
                     label={t('New Password')}/>
@@ -352,13 +460,16 @@ export function ResetPasswordForm() {
           <div className={'w-full'}>
             <Button type="submit"
                     className={'w-full'}
-                    variant={'secondary'}
-            >{t('Reset password')}</Button>
+                    disabled={loading}
+            >
+              {loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
+              {t('Reset password')}
+            </Button>
 
             <p className={'pt-4 text-center'}>
-              <a onClick={() => setIsSentCode(false)}
+              <a onClick={() => setCurrentTab('login')}
                  className={'text-sm opacity-50 hover:opacity-80 underline'}>
-                {t('Resend code')}
+                {t('Back to login')}
               </a>
             </p>
           </div>
@@ -372,8 +483,11 @@ export function ResetPasswordForm() {
           <div className={'w-full'}>
             <Button type="submit"
                     className={'w-full'}
-                    variant={'secondary'}
-            >{t('Send code')}</Button>
+                    disabled={loading}
+            >
+              {loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
+              {t('Send code')}
+            </Button>
 
             <p className={'pt-3 text-center'}>
               <a onClick={() => setCurrentTab('login')}
@@ -395,30 +509,13 @@ export function ConfirmWithCodeForm(
   const [loading, setLoading] = useState<boolean>(false)
   const isFromSignIn = props.user?.hasOwnProperty('isSignedIn')
   const signUpCodeDeliveryDetails = props.user?.nextStep?.codeDeliveryDetails
-  const [countDownNum, setCountDownNum] = useState<number>(0)
-  const startCountDown = () => {
-    setCountDownNum(60)
-    const interval = setInterval(() => {
-      setCountDownNum((num) => {
-        if (num <= 1) {
-          clearInterval(interval)
-          return 0
-        }
-        return num - 1
-      })
-    }, 1000)
-  }
-
-  useEffect(() => {
-    return () => {
-      setCountDownNum(0)
-    }
-  }, [])
+  const { countDownNum, startCountDown, setCountDownNum } = useCountDown()
 
   return (
     <FormGroup
       autoComplete={'off'}
       onSubmit={async (e) => {
+        setErrors(null)
         e.preventDefault()
 
         // get submit form input data
@@ -433,13 +530,22 @@ export function ConfirmWithCodeForm(
               confirmationCode: data.code as string,
             })
 
-            console.log('===>>', ret)
+            if (ret.nextStep?.signUpStep === 'COMPLETE_AUTO_SIGN_IN') {
+              const { nextStep } = await Auth.autoSignIn()
+              if (nextStep.signInStep === 'DONE') {
+                // signed in
+                setCurrentTab('login')
+                return
+              }
+            }
+
+            setCurrentTab('login')
           } else {
             const ret = await Auth.confirmSignIn({
               challengeResponse: data.code as string,
             })
 
-            console.log('===>>', ret)
+            console.log('===>> confirmSignIn: ', ret)
           }
         } catch (e) {
           setErrors({ code: { message: (e as Error).message, title: t('Bad Response.') } })