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 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. 'use client'
  2. import { useCallback, useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import { useRouter, useSearchParams } from 'next/navigation'
  5. import cn from 'classnames'
  6. import { RiCheckboxCircleFill } from '@remixicon/react'
  7. import { useCountDown } from 'ahooks'
  8. import Button from '@/app/components/base/button'
  9. import { changeWebAppPasswordWithToken } from '@/service/common'
  10. import Toast from '@/app/components/base/toast'
  11. import Input from '@/app/components/base/input'
  12. import { validPassword } from '@/config'
  13. const ChangePasswordForm = () => {
  14. const { t } = useTranslation()
  15. const router = useRouter()
  16. const searchParams = useSearchParams()
  17. const token = decodeURIComponent(searchParams.get('token') || '')
  18. const [password, setPassword] = useState('')
  19. const [confirmPassword, setConfirmPassword] = useState('')
  20. const [showSuccess, setShowSuccess] = useState(false)
  21. const [showPassword, setShowPassword] = useState(false)
  22. const [showConfirmPassword, setShowConfirmPassword] = useState(false)
  23. const showErrorMessage = useCallback((message: string) => {
  24. Toast.notify({
  25. type: 'error',
  26. message,
  27. })
  28. }, [])
  29. const getSignInUrl = () => {
  30. return `/webapp-signin?redirect_url=${searchParams.get('redirect_url') || ''}`
  31. }
  32. const AUTO_REDIRECT_TIME = 5000
  33. const [leftTime, setLeftTime] = useState<number | undefined>(undefined)
  34. const [countdown] = useCountDown({
  35. leftTime,
  36. onEnd: () => {
  37. router.replace(getSignInUrl())
  38. },
  39. })
  40. const valid = useCallback(() => {
  41. if (!password.trim()) {
  42. showErrorMessage(t('login.error.passwordEmpty'))
  43. return false
  44. }
  45. if (!validPassword.test(password)) {
  46. showErrorMessage(t('login.error.passwordInvalid'))
  47. return false
  48. }
  49. if (password !== confirmPassword) {
  50. showErrorMessage(t('common.account.notEqual'))
  51. return false
  52. }
  53. return true
  54. }, [password, confirmPassword, showErrorMessage, t])
  55. const handleChangePassword = useCallback(async () => {
  56. if (!valid())
  57. return
  58. try {
  59. await changeWebAppPasswordWithToken({
  60. url: '/forgot-password/resets',
  61. body: {
  62. token,
  63. new_password: password,
  64. password_confirm: confirmPassword,
  65. },
  66. })
  67. setShowSuccess(true)
  68. setLeftTime(AUTO_REDIRECT_TIME)
  69. }
  70. catch (error) {
  71. console.error(error)
  72. }
  73. }, [password, token, valid, confirmPassword])
  74. return (
  75. <div className={
  76. cn(
  77. 'flex w-full grow flex-col items-center justify-center',
  78. 'px-6',
  79. 'md:px-[108px]',
  80. )
  81. }>
  82. {!showSuccess && (
  83. <div className='flex flex-col md:w-[400px]'>
  84. <div className="mx-auto w-full">
  85. <h2 className="title-4xl-semi-bold text-text-primary">
  86. {t('login.changePassword')}
  87. </h2>
  88. <p className='body-md-regular mt-2 text-text-secondary'>
  89. {t('login.changePasswordTip')}
  90. </p>
  91. </div>
  92. <div className="mx-auto mt-6 w-full">
  93. <div className="bg-white">
  94. {/* Password */}
  95. <div className='mb-5'>
  96. <label htmlFor="password" className="system-md-semibold my-2 text-text-secondary">
  97. {t('common.account.newPassword')}
  98. </label>
  99. <div className='relative mt-1'>
  100. <Input
  101. id="password" type={showPassword ? 'text' : 'password'}
  102. value={password}
  103. onChange={e => setPassword(e.target.value)}
  104. placeholder={t('login.passwordPlaceholder') || ''}
  105. />
  106. <div className="absolute inset-y-0 right-0 flex items-center">
  107. <Button
  108. type="button"
  109. variant='ghost'
  110. onClick={() => setShowPassword(!showPassword)}
  111. >
  112. {showPassword ? '👀' : '😝'}
  113. </Button>
  114. </div>
  115. </div>
  116. <div className='body-xs-regular mt-1 text-text-secondary'>{t('login.error.passwordInvalid')}</div>
  117. </div>
  118. {/* Confirm Password */}
  119. <div className='mb-5'>
  120. <label htmlFor="confirmPassword" className="system-md-semibold my-2 text-text-secondary">
  121. {t('common.account.confirmPassword')}
  122. </label>
  123. <div className='relative mt-1'>
  124. <Input
  125. id="confirmPassword"
  126. type={showConfirmPassword ? 'text' : 'password'}
  127. value={confirmPassword}
  128. onChange={e => setConfirmPassword(e.target.value)}
  129. placeholder={t('login.confirmPasswordPlaceholder') || ''}
  130. />
  131. <div className="absolute inset-y-0 right-0 flex items-center">
  132. <Button
  133. type="button"
  134. variant='ghost'
  135. onClick={() => setShowConfirmPassword(!showConfirmPassword)}
  136. >
  137. {showConfirmPassword ? '👀' : '😝'}
  138. </Button>
  139. </div>
  140. </div>
  141. </div>
  142. <div>
  143. <Button
  144. variant='primary'
  145. className='w-full'
  146. onClick={handleChangePassword}
  147. >
  148. {t('login.changePasswordBtn')}
  149. </Button>
  150. </div>
  151. </div>
  152. </div>
  153. </div>
  154. )}
  155. {showSuccess && (
  156. <div className="flex flex-col md:w-[400px]">
  157. <div className="mx-auto w-full">
  158. <div className="mb-3 flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle font-bold shadow-lg">
  159. <RiCheckboxCircleFill className='h-6 w-6 text-text-success' />
  160. </div>
  161. <h2 className="title-4xl-semi-bold text-text-primary">
  162. {t('login.passwordChangedTip')}
  163. </h2>
  164. </div>
  165. <div className="mx-auto mt-6 w-full">
  166. <Button variant='primary' className='w-full' onClick={() => {
  167. setLeftTime(undefined)
  168. router.replace(getSignInUrl())
  169. }}>{t('login.passwordChanged')} ({Math.round(countdown / 1000)}) </Button>
  170. </div>
  171. </div>
  172. )}
  173. </div>
  174. )
  175. }
  176. export default ChangePasswordForm