You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mail-and-password-auth.tsx 5.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. 'use client'
  2. import Link from 'next/link'
  3. import { useCallback, useState } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import { useRouter, useSearchParams } from 'next/navigation'
  6. import { useContext } from 'use-context-selector'
  7. import Button from '@/app/components/base/button'
  8. import Toast from '@/app/components/base/toast'
  9. import { emailRegex } from '@/config'
  10. import { webAppLogin } from '@/service/common'
  11. import Input from '@/app/components/base/input'
  12. import I18NContext from '@/context/i18n'
  13. import { noop } from 'lodash-es'
  14. import { setAccessToken } from '@/app/components/share/utils'
  15. import { fetchAccessToken } from '@/service/share'
  16. type MailAndPasswordAuthProps = {
  17. isEmailSetup: boolean
  18. }
  19. const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
  20. export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAuthProps) {
  21. const { t } = useTranslation()
  22. const { locale } = useContext(I18NContext)
  23. const router = useRouter()
  24. const searchParams = useSearchParams()
  25. const [showPassword, setShowPassword] = useState(false)
  26. const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
  27. const [email, setEmail] = useState(emailFromLink)
  28. const [password, setPassword] = useState('')
  29. const [isLoading, setIsLoading] = useState(false)
  30. const redirectUrl = searchParams.get('redirect_url')
  31. const getAppCodeFromRedirectUrl = useCallback(() => {
  32. if (!redirectUrl)
  33. return null
  34. const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`)
  35. const appCode = url.pathname.split('/').pop()
  36. if (!appCode)
  37. return null
  38. return appCode
  39. }, [redirectUrl])
  40. const handleEmailPasswordLogin = async () => {
  41. const appCode = getAppCodeFromRedirectUrl()
  42. if (!email) {
  43. Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
  44. return
  45. }
  46. if (!emailRegex.test(email)) {
  47. Toast.notify({
  48. type: 'error',
  49. message: t('login.error.emailInValid'),
  50. })
  51. return
  52. }
  53. if (!password?.trim()) {
  54. Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') })
  55. return
  56. }
  57. if (!passwordRegex.test(password)) {
  58. Toast.notify({
  59. type: 'error',
  60. message: t('login.error.passwordInvalid'),
  61. })
  62. return
  63. }
  64. if (!redirectUrl || !appCode) {
  65. Toast.notify({
  66. type: 'error',
  67. message: t('login.error.redirectUrlMissing'),
  68. })
  69. return
  70. }
  71. try {
  72. setIsLoading(true)
  73. const loginData: Record<string, any> = {
  74. email,
  75. password,
  76. language: locale,
  77. remember_me: true,
  78. }
  79. const res = await webAppLogin({
  80. url: '/login',
  81. body: loginData,
  82. })
  83. if (res.result === 'success') {
  84. localStorage.setItem('webapp_access_token', res.data.access_token)
  85. const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: res.data.access_token })
  86. await setAccessToken(appCode, tokenResp.access_token)
  87. router.replace(decodeURIComponent(redirectUrl))
  88. }
  89. else {
  90. Toast.notify({
  91. type: 'error',
  92. message: res.data,
  93. })
  94. }
  95. }
  96. finally {
  97. setIsLoading(false)
  98. }
  99. }
  100. return <form onSubmit={noop}>
  101. <div className='mb-3'>
  102. <label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
  103. {t('login.email')}
  104. </label>
  105. <div className="mt-1">
  106. <Input
  107. value={email}
  108. onChange={e => setEmail(e.target.value)}
  109. id="email"
  110. type="email"
  111. autoComplete="email"
  112. placeholder={t('login.emailPlaceholder') || ''}
  113. tabIndex={1}
  114. />
  115. </div>
  116. </div>
  117. <div className='mb-3'>
  118. <label htmlFor="password" className="my-2 flex items-center justify-between">
  119. <span className='system-md-semibold text-text-secondary'>{t('login.password')}</span>
  120. <Link
  121. href={`/webapp-reset-password?${searchParams.toString()}`}
  122. className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}
  123. tabIndex={isEmailSetup ? 0 : -1}
  124. aria-disabled={!isEmailSetup}
  125. >
  126. {t('login.forget')}
  127. </Link>
  128. </label>
  129. <div className="relative mt-1">
  130. <Input
  131. id="password"
  132. value={password}
  133. onChange={e => setPassword(e.target.value)}
  134. onKeyDown={(e) => {
  135. if (e.key === 'Enter')
  136. handleEmailPasswordLogin()
  137. }}
  138. type={showPassword ? 'text' : 'password'}
  139. autoComplete="current-password"
  140. placeholder={t('login.passwordPlaceholder') || ''}
  141. tabIndex={2}
  142. />
  143. <div className="absolute inset-y-0 right-0 flex items-center">
  144. <Button
  145. type="button"
  146. variant='ghost'
  147. onClick={() => setShowPassword(!showPassword)}
  148. >
  149. {showPassword ? '👀' : '😝'}
  150. </Button>
  151. </div>
  152. </div>
  153. </div>
  154. <div className='mb-2'>
  155. <Button
  156. tabIndex={2}
  157. variant='primary'
  158. onClick={handleEmailPasswordLogin}
  159. disabled={isLoading || !email || !password}
  160. className="w-full"
  161. >{t('login.signBtn')}</Button>
  162. </div>
  163. </form>
  164. }