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.

page.tsx 4.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. 'use client'
  2. import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
  3. import { useTranslation } from 'react-i18next'
  4. import { useCallback, useState } from 'react'
  5. import { useRouter, useSearchParams } from 'next/navigation'
  6. import { useContext } from 'use-context-selector'
  7. import Countdown from '@/app/components/signin/countdown'
  8. import Button from '@/app/components/base/button'
  9. import Input from '@/app/components/base/input'
  10. import Toast from '@/app/components/base/toast'
  11. import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
  12. import I18NContext from '@/context/i18n'
  13. import { setAccessToken } from '@/app/components/share/utils'
  14. import { fetchAccessToken } from '@/service/share'
  15. export default function CheckCode() {
  16. const { t } = useTranslation()
  17. const router = useRouter()
  18. const searchParams = useSearchParams()
  19. const email = decodeURIComponent(searchParams.get('email') as string)
  20. const token = decodeURIComponent(searchParams.get('token') as string)
  21. const [code, setVerifyCode] = useState('')
  22. const [loading, setIsLoading] = useState(false)
  23. const { locale } = useContext(I18NContext)
  24. const redirectUrl = searchParams.get('redirect_url')
  25. const getAppCodeFromRedirectUrl = useCallback(() => {
  26. if (!redirectUrl)
  27. return null
  28. const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`)
  29. const appCode = url.pathname.split('/').pop()
  30. if (!appCode)
  31. return null
  32. return appCode
  33. }, [redirectUrl])
  34. const verify = async () => {
  35. try {
  36. const appCode = getAppCodeFromRedirectUrl()
  37. if (!code.trim()) {
  38. Toast.notify({
  39. type: 'error',
  40. message: t('login.checkCode.emptyCode'),
  41. })
  42. return
  43. }
  44. if (!/\d{6}/.test(code)) {
  45. Toast.notify({
  46. type: 'error',
  47. message: t('login.checkCode.invalidCode'),
  48. })
  49. return
  50. }
  51. if (!redirectUrl || !appCode) {
  52. Toast.notify({
  53. type: 'error',
  54. message: t('login.error.redirectUrlMissing'),
  55. })
  56. return
  57. }
  58. setIsLoading(true)
  59. const ret = await webAppEmailLoginWithCode({ email, code, token })
  60. if (ret.result === 'success') {
  61. localStorage.setItem('webapp_access_token', ret.data.access_token)
  62. const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: ret.data.access_token })
  63. await setAccessToken(appCode, tokenResp.access_token)
  64. router.replace(decodeURIComponent(redirectUrl))
  65. }
  66. }
  67. catch (error) { console.error(error) }
  68. finally {
  69. setIsLoading(false)
  70. }
  71. }
  72. const resendCode = async () => {
  73. try {
  74. const ret = await sendWebAppEMailLoginCode(email, locale)
  75. if (ret.result === 'success') {
  76. const params = new URLSearchParams(searchParams)
  77. params.set('token', encodeURIComponent(ret.data))
  78. router.replace(`/webapp-signin/check-code?${params.toString()}`)
  79. }
  80. }
  81. catch (error) { console.error(error) }
  82. }
  83. return <div className='flex w-[400px] flex-col gap-3'>
  84. <div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg'>
  85. <RiMailSendFill className='h-6 w-6 text-2xl text-text-accent-light-mode-only' />
  86. </div>
  87. <div className='pb-4 pt-2'>
  88. <h2 className='title-4xl-semi-bold text-text-primary'>{t('login.checkCode.checkYourEmail')}</h2>
  89. <p className='body-md-regular mt-2 text-text-secondary'>
  90. <span dangerouslySetInnerHTML={{ __html: t('login.checkCode.tips', { email }) as string }}></span>
  91. <br />
  92. {t('login.checkCode.validTime')}
  93. </p>
  94. </div>
  95. <form action="">
  96. <label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
  97. <Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} />
  98. <Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button>
  99. <Countdown onResend={resendCode} />
  100. </form>
  101. <div className='py-2'>
  102. <div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
  103. </div>
  104. <div onClick={() => router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'>
  105. <div className='bg-background-default-dimm inline-block rounded-full p-1'>
  106. <RiArrowLeftLine size={12} />
  107. </div>
  108. <span className='system-xs-regular ml-2'>{t('login.back')}</span>
  109. </div>
  110. </div>
  111. }