Browse Source

enhance(ui): add confirm code form and enhance authentication flow

charlie 1 month ago
parent
commit
325ae878bd
3 changed files with 103 additions and 14 deletions
  1. 11 5
      packages/ui/examples/index.tsx
  2. 9 0
      packages/ui/src/amplify/lang.ts
  3. 83 9
      packages/ui/src/amplify/ui.tsx

+ 11 - 5
packages/ui/examples/index.tsx

@@ -2,12 +2,12 @@ import '../src/index.css'
 import { setupGlobals } from '../src/ui'
 import * as React from 'react'
 import * as ReactDOM from 'react-dom'
-import { init } from '../src/amplify/core'
+import { init, t } from '../src/amplify/core'
 
 // @ts-ignore
 import { Button } from '@/components/ui/button'
 import { Card, CardContent, CardHeader, CardTitle } from '../@/components/ui/card'
-import { LoginForm, ResetPasswordForm, SignupForm } from '../src/amplify/ui'
+import { LoginForm, ResetPasswordForm, SignupForm, ConfirmWithCodeForm } from '../src/amplify/ui'
 import { AuthFormRootContext } from '../src/amplify/core'
 
 // bootstrap
@@ -16,7 +16,7 @@ init()
 
 function App() {
   const [errors, setErrors] = React.useState<string | null>(null)
-  const [currentTab, setCurrentTab] = React.useState<'login' | 'reset' | 'signup'>('login')
+  const [currentTab, setCurrentTab] = React.useState<'login' | 'reset' | 'signup' | 'confirm-code' | any>('confirm-code')
   const onSessionCallback = React.useCallback((session: any) => {
     console.log('==>>session:', session)
   }, [])
@@ -26,8 +26,11 @@ function App() {
   }, [currentTab])
 
   let content = null
+  // support passing object with type field
+  let _currentTab = currentTab?.type ? currentTab.type : currentTab
+  let _currentTabProps = currentTab?.props || {}
 
-  switch (currentTab) {
+  switch (_currentTab) {
     case 'login':
       content = <LoginForm/>
       break
@@ -37,6 +40,9 @@ function App() {
     case 'signup':
       content = <SignupForm/>
       break
+    case 'confirm-code':
+      content = <ConfirmWithCodeForm {..._currentTabProps}/>
+      break
   }
 
   return (
@@ -47,7 +53,7 @@ function App() {
       }}>
         <Card className={'sm:w-96'}>
           <CardHeader>
-            <CardTitle className={'capitalize'}>{currentTab}</CardTitle>
+            <CardTitle className={'capitalize'}>{t(currentTab)?.replace('-', ' ')}</CardTitle>
           </CardHeader>
           <CardContent>
             {content}

+ 9 - 0
packages/ui/src/amplify/lang.ts

@@ -1,9 +1,14 @@
 export default {
+  'en': {
+    'CODE_ON_THE_WAY_TIP': 'Your code is on the way. To log in, enter the code we sent you. It may take a minute to arrive.',
+  },
   'zh-CN': {
+    'CODE_ON_THE_WAY_TIP': '验证码已发送。请输入我们发送给您的验证码以登录。可能需要一分钟才能收到。',
     'Sign in to your account': '登录到您的账户',
     'Email': '电子邮箱',
     'Password': '密码',
     'Sign in': '登录',
+    'Confirm': '确认',
     'Don\'t have an account?': '还没有账户?',
     'Sign up': '注册',
     'or': '或 ',
@@ -24,10 +29,12 @@ export default {
     'Enter your email': '请输入您的电子邮箱'
   },
   'zh-Han': {
+    'CODE_ON_THE_WAY_TIP': '驗證碼已發送。請輸入我們發送給您的驗證碼以登入。可能需要一分鐘才能收到。',
     'Sign in to your account': '登入到您的帳戶',
     'Email': '電子郵箱',
     'Password': '密碼',
     'Sign in': '登入',
+    'Confirm': '確認',
     'Don\'t have an account?': '還沒有帳戶?',
     'Sign up': '註冊',
     'or': '或 ',
@@ -48,10 +55,12 @@ export default {
     'Enter your email': '請輸入您的電子郵箱'
   },
   'ja': {
+    'CODE_ON_THE_WAY_TIP': 'コードが送信されました。ログインするには、送信したコードを入力してください。届くまでに1分ほどかかる場合があります。',
     'Sign in to your account': 'アカウントにサインイン',
     'Email': 'メール',
     'Password': 'パスワード',
     'Sign in': 'サインイン',
+    'Confirm': '確認',
     'Don\'t have an account?': 'アカウントをお持ちでないですか?',
     'Sign up': 'サインアップ',
     'or': 'または ',

+ 83 - 9
packages/ui/src/amplify/ui.tsx

@@ -4,7 +4,7 @@ import { Label } from '@/components/ui/label'
 import { cn } from '@/lib/utils'
 import { FormHTMLAttributes, useEffect, useState } from 'react'
 import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
-import { AlertCircleIcon, LucideEye, LucideEyeClosed } from 'lucide-react'
+import { AlertCircleIcon, Loader2Icon, LucideEye, LucideEyeClosed } from 'lucide-react'
 import { t, useAuthFormState } from './core'
 import * as Auth from 'aws-amplify/auth'
 import { Skeleton } from '@/components/ui/skeleton'
@@ -37,7 +37,7 @@ function InputRow(
   const [showPassword, setShowPassword] = useState<boolean>(false)
 
   return (
-    <div className={'relative w-full flex flex-col gap-2 pb-1'}>
+    <div className={'relative w-full flex flex-col gap-3 pb-1'}>
       <Label htmlFor={props.id}>{label}</Label>
       <Input type={localType} {...rest as any} />
 
@@ -117,6 +117,7 @@ export function LoginForm() {
 
   return (
     <FormGroup onSubmit={async (e) => {
+      setErrors(null)
       e.preventDefault()
 
       // get submit form input data
@@ -132,7 +133,7 @@ export function LoginForm() {
         if (!nextStep) throw new Error(JSON.stringify(ret))
         loadSession()
       } catch (e) {
-        setErrors({ password: { message: (e as Error).message, title: 'Bad Response.' } })
+        setErrors({ password: { message: (e as Error).message, title: t('Bad Response.') } })
         console.error(e)
       } finally {
         setLoading(false)
@@ -142,7 +143,10 @@ export function LoginForm() {
       <InputRow id="password" type="password" name="password" label={t('Password')}/>
 
       <div className={'w-full'}>
-        <Button type="submit" disabled={loading} className={'w-full'}>{t('Sign in')}</Button>
+        <Button type="submit" disabled={loading} className={'w-full'}>
+          {loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
+          {t('Sign in')}
+        </Button>
         <p className={'pt-4 text-center'}>
 
           <span className={'text-sm'}>
@@ -173,12 +177,32 @@ export function SignupForm() {
   return (
     <>
       <FormGroup 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)
+        const data = Object.fromEntries(formData.entries()) as any
+
+        if (data.password.length < 8) {
+          setErrors({
+            password: {
+              message: t('Password must be at least 8 characters.'),
+              title: t('Invalid Password')
+            }
+          })
+          return
+        }
+
+        if (data.password !== data.confirm_password) {
+          setErrors({
+            confirm_password: {
+              message: t('Passwords do not match.'),
+              title: t('Invalid Password')
+            }
+          })
+          return
+        }
 
         try {
           setLoading(true)
@@ -195,8 +219,14 @@ export function SignupForm() {
           })
 
           console.log(ret)
-        } catch (e) {
-          setErrors({ email: (e as Error).message })
+        } catch (e: any) {
+          console.error(e)
+          const error = { title: t('Bad Response.'), message: (e as Error).message }
+          let k = 'confirm_password'
+          if (e.name === 'UsernameExistsException') {
+            k = 'username'
+          }
+          setErrors({ [k]: error })
         } finally {
           setLoading(false)
         }
@@ -224,7 +254,10 @@ export function SignupForm() {
           </span>
         </div>
         <div className={'w-full'}>
-          <Button type="submit" disabled={loading} className={'w-full'}>{t('Create account')}</Button>
+          <Button type="submit" disabled={loading} className={'w-full'}>
+            {loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
+            {t('Create account')}
+          </Button>
         </div>
 
         <p className={'pt-1 text-center'}>
@@ -306,4 +339,45 @@ export function ResetPasswordForm() {
       )}
     </FormGroup>
   )
+}
+
+export function ConfirmWithCodeForm() {
+  const { setCurrentTab } = useAuthFormState()
+
+  return (
+    <FormGroup
+      autoComplete={'off'}
+      onSubmit={(e) => {
+        e.preventDefault()
+
+        // get submit form input data
+        const formData = new FormData(e.target as HTMLFormElement)
+        const data = Object.fromEntries(formData.entries())
+        console.log(data)
+      }}>
+
+      <p className={'pb-2 opacity-60'}>
+        {t('CODE_ON_THE_WAY_TIP')}
+      </p>
+
+      <InputRow id="code" type="number" name="code" required={true}
+                placeholder={'123456'}
+                autoComplete={'off'}
+                autoFocus={true}
+                label={t('Enter the code sent to your email')}/>
+
+      <div className={'w-full'}>
+        <Button type="submit"
+                className={'w-full'}
+        >{t('Confirm')}</Button>
+
+        <p className={'pt-4 text-center'}>
+          <a onClick={() => setCurrentTab('login')}
+             className={'text-sm opacity-50 hover:opacity-80 underline'}>
+            {t('Back to login')}
+          </a>
+        </p>
+      </div>
+    </FormGroup>
+  )
 }