Merge pull request #208 from everysingletear/i18n/auth-v2
i18n: extract Authorization screens into a new `auth` namespace (Part of #145)
This commit is contained in:
111
public/locales/en/auth.json
Normal file
111
public/locales/en/auth.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"validationError": "Validation Error",
|
||||
"primaryUsernameRequired": "Primary username is required for sub account login",
|
||||
"subNameRequired": "Sub account name is required for sub account login",
|
||||
"usernameRequired": "Username is required",
|
||||
"passwordRequired": "Password is required",
|
||||
"loginFailed": "Login Failed",
|
||||
"genericError": "An error occurred, please try again",
|
||||
"providerLoginFailed": "{{provider}} Login Failed",
|
||||
"providerLoginFailedMsg": "Couldn't log in with {{provider}}, please try again",
|
||||
"providerLoginError": "{{provider}} Login Error",
|
||||
"networkError": "Network error occurred, please try again",
|
||||
"mfaFailed": "Two-Factor Authentication Failed",
|
||||
"oauthError": "OAuth Error",
|
||||
"oauthBrowserFailed": "Failed to open authentication browser",
|
||||
"primaryAccount": "Primary Account",
|
||||
"subAccount": "Sub Account",
|
||||
"username": "Username",
|
||||
"primaryUsernamePlaceholder": "Enter primary account username",
|
||||
"subNamePlaceholder": "Enter sub account name",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"or": "or",
|
||||
"email": "Email",
|
||||
"displayNamePlaceholder": "How others see your name",
|
||||
"signupFailed": "Signup Failed",
|
||||
"signupDisabled": "Signup disabled, please contact admin",
|
||||
"usernameMinLength": "Username must be at least 4 characters",
|
||||
"invalidEmail": "Invalid email address",
|
||||
"passwordLength": "Password must be between 8 and 64 characters",
|
||||
"displayNameRequired": "Display name is required",
|
||||
"displayNameChars": "Display name can only contain letters and numbers",
|
||||
"resetEmailSent": "Reset Email Sent",
|
||||
"resetEmailSentMsg": "Check your email for password reset instructions",
|
||||
"resetFailed": "Reset Failed",
|
||||
"resetFailedMsg": "Failed to send reset email, please try again later",
|
||||
"emailPlaceholder": "you@example.com",
|
||||
"passwordUpdated": "Password Updated",
|
||||
"passwordUpdatedMsg": "Your password has been updated successfully. Redirecting to login...",
|
||||
"passwordUpdateFailed": "Password Update Failed",
|
||||
"passwordUpdateFailedMsg": "Failed to update password, please try again later",
|
||||
"savePassword": "Save password",
|
||||
"server": {
|
||||
"url": "Server URL",
|
||||
"dnsFailed": "Hostname could not be resolved. Check the URL for typos or verify DNS.",
|
||||
"unreachable": "Unable to reach the server. Check the URL, port, and network connection.",
|
||||
"invalidUrl": "Invalid URL format. Include the protocol (http:// or https://) and port if needed."
|
||||
},
|
||||
"mfaModal": {
|
||||
"title": "Two-factor authentication",
|
||||
"codeRequired": "Please enter a verification code",
|
||||
"verifyFailed": "Failed to verify code. Please try again.",
|
||||
"backupHint": "Enter one of the backup codes you saved when setting up two-factor authentication.",
|
||||
"codeHint": "Enter the 6-digit code from your authenticator app.",
|
||||
"verify": "Verify & Sign In",
|
||||
"backupLabel": "Backup code",
|
||||
"codeLabel": "Verification code",
|
||||
"backupPlaceholder": "Enter backup code",
|
||||
"useAuthenticator": "Use authenticator app instead",
|
||||
"useBackup": "Use a backup code instead",
|
||||
"backupOnce": "Each backup code can only be used once.",
|
||||
"invalidCode": "Invalid verification code. Please try again."
|
||||
},
|
||||
"appleLoginFailed": "Apple Login Failed",
|
||||
"googleLoginFailed": "Google Login Failed",
|
||||
"createOne": "Create one",
|
||||
"primaryAccountUsernameLabel": "Primary account username",
|
||||
"subAccountNameLabel": "Sub account name",
|
||||
"serverSettings": "Server settings",
|
||||
"useDifferentAccount": "Use a different account",
|
||||
"yourUsername": "Your username",
|
||||
"backToSignIn": "Back to sign in",
|
||||
"confirmNewPassword": "Confirm new password",
|
||||
"newPassword": "New password",
|
||||
"reenterPassword": "Re-enter your password",
|
||||
"requestNewLink": "Request a new link",
|
||||
"setNewPassword": "Set a new password",
|
||||
"linkInvalid": "This link is not valid",
|
||||
"createAccountButton": "Create account",
|
||||
"createYourAccount": "Create your account",
|
||||
"displayNameLabel": "Display name",
|
||||
"usernameHint": "lowercase letters, numbers, dot and dash",
|
||||
"termsShort": "By creating an account you agree to our Terms of Service and Privacy",
|
||||
"checkYourEmail": "Check your email",
|
||||
"resetYourPassword": "Reset your password",
|
||||
"serverChangeWarning": "Changing the server clears locally cached data on this device.",
|
||||
"serverConnected": "Connected. Taking you to sign in...",
|
||||
"passwordLabel": "Password",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"loginPasswordPlaceholder": "Enter your password",
|
||||
"signupPasswordPlaceholder": "At least 8 characters",
|
||||
"signupPasswordHelper": "Use 8 to 64 characters.",
|
||||
"orContinueWith": "or continue with",
|
||||
"signupSignInFailed": "Your account was created, but signing in failed. Please sign in.",
|
||||
"authenticating": {
|
||||
"signingIn": "Signing you in",
|
||||
"signingInSub": "This will only take a moment.",
|
||||
"unknownProvider": "Unknown sign-in provider",
|
||||
"contactSupport": "Please contact support.",
|
||||
"signInFailed": "Sign-in failed",
|
||||
"mfaCancelled": "Two-factor authentication was cancelled.",
|
||||
"requestNotVerified": "The sign-in request could not be verified.",
|
||||
"tryAgain": "Please try again.",
|
||||
"mfaSessionMissing": "The MFA session is missing. Please try again.",
|
||||
"twoFactor": "Two-factor authentication",
|
||||
"verifyToContinue": "Verify your login to continue.",
|
||||
"noToken": "No valid authentication token was returned.",
|
||||
"backToSignIn": "Back to sign in",
|
||||
"mfaFailed": "Two-factor authentication failed. Please try again."
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ i18n
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
|
||||
ns: ['common', 'settings', 'chores'],
|
||||
ns: ['common', 'settings', 'chores', 'auth'],
|
||||
defaultNS: 'common',
|
||||
|
||||
detection: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { authButtonSx, authInputSx } from './authStyles'
|
||||
|
||||
const labelSx = { fontSize: '0.875rem', fontWeight: 600, mb: 0.75 }
|
||||
@@ -40,12 +41,13 @@ export const AuthTextField = ({ label, error, helper, sx, ...inputProps }) => (
|
||||
)
|
||||
|
||||
export const AuthPasswordField = ({
|
||||
label = 'Password',
|
||||
label,
|
||||
error,
|
||||
helper,
|
||||
sx,
|
||||
...inputProps
|
||||
}) => {
|
||||
const { t } = useTranslation('auth')
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
return (
|
||||
@@ -60,7 +62,7 @@ export const AuthPasswordField = ({
|
||||
color='neutral'
|
||||
size='sm'
|
||||
tabIndex={-1}
|
||||
aria-label={visible ? 'Hide password' : 'Show password'}
|
||||
aria-label={visible ? t('hidePassword') : t('showPassword')}
|
||||
onClick={() => setVisible(v => !v)}
|
||||
sx={{ borderRadius: '8px' }}
|
||||
>
|
||||
@@ -110,27 +112,31 @@ export const SocialButton = ({ icon, children, sx, ...props }) => (
|
||||
</Button>
|
||||
)
|
||||
|
||||
export const AuthDivider = ({ children = 'or' }) => (
|
||||
<Box
|
||||
role='separator'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
my: 2.5,
|
||||
'&::before, &::after': {
|
||||
content: '""',
|
||||
flex: 1,
|
||||
height: '1px',
|
||||
bgcolor: 'divider',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
{children}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
export const AuthDivider = ({ children }) => {
|
||||
const { t } = useTranslation('auth')
|
||||
|
||||
return (
|
||||
<Box
|
||||
role='separator'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
my: 2.5,
|
||||
'&::before, &::after': {
|
||||
content: '""',
|
||||
flex: 1,
|
||||
height: '1px',
|
||||
bgcolor: 'divider',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
{children ?? t('or')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export const LegalLinks = () => (
|
||||
<Typography
|
||||
|
||||
@@ -10,16 +10,18 @@ import { apiClient } from '../../utils/ApiClient'
|
||||
import { endOAuthExchange } from '../../utils/OAuthExchangeState'
|
||||
import { GetUserProfile } from '../../utils/Fetcher'
|
||||
import { saveTokens } from '../../utils/TokenStorage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AuthShell from './AuthShell'
|
||||
import { authButtonSx } from './authStyles'
|
||||
import MFAVerificationModal from './MFAVerificationModal'
|
||||
|
||||
const AuthenticationLoading = () => {
|
||||
const { t } = useTranslation('auth')
|
||||
const { refetch: refetchUserProfile } = useUserProfile()
|
||||
const Navigate = useNavigate()
|
||||
const hasCalledHandleOAuth2 = useRef(false)
|
||||
const [message, setMessage] = useState('Signing you in')
|
||||
const [subMessage, setSubMessage] = useState('This will only take a moment.')
|
||||
const [message, setMessage] = useState(t('authenticating.signingIn'))
|
||||
const [subMessage, setSubMessage] = useState(t('authenticating.signingInSub'))
|
||||
const [status, setStatus] = useState('pending')
|
||||
const [mfaModalOpen, setMfaModalOpen] = useState(false)
|
||||
const [mfaSessionToken, setMfaSessionToken] = useState('')
|
||||
@@ -31,8 +33,8 @@ const AuthenticationLoading = () => {
|
||||
// suppress a genuine session expiry later on.
|
||||
handleOAuth2().finally(endOAuthExchange)
|
||||
} else if (provider !== 'oauth2') {
|
||||
setMessage('Unknown sign-in provider')
|
||||
setSubMessage('Please contact support.')
|
||||
setMessage(t('authenticating.unknownProvider'))
|
||||
setSubMessage(t('authenticating.contactSupport'))
|
||||
setStatus('error')
|
||||
}
|
||||
return endOAuthExchange
|
||||
@@ -71,8 +73,8 @@ const AuthenticationLoading = () => {
|
||||
const handleMFAClose = () => {
|
||||
setMfaModalOpen(false)
|
||||
setMfaSessionToken('')
|
||||
setMessage('Sign-in failed')
|
||||
setSubMessage('Two-factor authentication was cancelled.')
|
||||
setMessage(t('authenticating.signInFailed'))
|
||||
setSubMessage(t('authenticating.mfaCancelled'))
|
||||
setStatus('error')
|
||||
}
|
||||
|
||||
@@ -85,8 +87,8 @@ const AuthenticationLoading = () => {
|
||||
const storedState = localStorage.getItem('authState')
|
||||
|
||||
if (returnedState !== storedState) {
|
||||
setMessage('Sign-in failed')
|
||||
setSubMessage('The sign-in request could not be verified.')
|
||||
setMessage(t('authenticating.signInFailed'))
|
||||
setSubMessage(t('authenticating.requestNotVerified'))
|
||||
setStatus('error')
|
||||
return
|
||||
}
|
||||
@@ -112,8 +114,8 @@ const AuthenticationLoading = () => {
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Authentication failed')
|
||||
setMessage('Sign-in failed')
|
||||
setSubMessage('Please try again.')
|
||||
setMessage(t('authenticating.signInFailed'))
|
||||
setSubMessage(t('authenticating.tryAgain'))
|
||||
setStatus('error')
|
||||
return
|
||||
}
|
||||
@@ -122,22 +124,22 @@ const AuthenticationLoading = () => {
|
||||
|
||||
if (data.mfaRequired) {
|
||||
if (!data.sessionToken) {
|
||||
setMessage('Sign-in failed')
|
||||
setSubMessage('The MFA session is missing. Please try again.')
|
||||
setMessage(t('authenticating.signInFailed'))
|
||||
setSubMessage(t('authenticating.mfaSessionMissing'))
|
||||
setStatus('error')
|
||||
return
|
||||
}
|
||||
|
||||
setMfaSessionToken(data.sessionToken)
|
||||
setMfaModalOpen(true)
|
||||
setMessage('Two-factor authentication')
|
||||
setSubMessage('Verify your login to continue.')
|
||||
setMessage(t('authenticating.twoFactor'))
|
||||
setSubMessage(t('authenticating.verifyToContinue'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!data.token && !data.access_token) {
|
||||
setMessage('Sign-in failed')
|
||||
setSubMessage('No valid authentication token was returned.')
|
||||
setMessage(t('authenticating.signInFailed'))
|
||||
setSubMessage(t('authenticating.noToken'))
|
||||
setStatus('error')
|
||||
return
|
||||
}
|
||||
@@ -158,8 +160,8 @@ const AuthenticationLoading = () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Authentication request failed', error)
|
||||
setMessage('Sign-in failed')
|
||||
setSubMessage('Please try again.')
|
||||
setMessage(t('authenticating.signInFailed'))
|
||||
setSubMessage(t('authenticating.tryAgain'))
|
||||
setStatus('error')
|
||||
}
|
||||
}
|
||||
@@ -188,7 +190,7 @@ const AuthenticationLoading = () => {
|
||||
fullWidth
|
||||
sx={authButtonSx}
|
||||
>
|
||||
Back to sign in
|
||||
{t('authenticating.backToSignIn')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
@@ -199,8 +201,8 @@ const AuthenticationLoading = () => {
|
||||
sessionToken={mfaSessionToken}
|
||||
onSuccess={handleMFASuccess}
|
||||
onError={() => {
|
||||
setMessage('Sign-in failed')
|
||||
setSubMessage('Two-factor authentication failed. Please try again.')
|
||||
setMessage(t('authenticating.signInFailed'))
|
||||
setSubMessage(t('authenticating.mfaFailed'))
|
||||
}}
|
||||
/>
|
||||
</AuthShell>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import MarkEmailReadOutlined from '@mui/icons-material/MarkEmailReadOutlined'
|
||||
import { Box, Button, Link, Typography } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { ResetPassword } from '../../utils/Fetcher'
|
||||
@@ -12,6 +13,7 @@ const isInvalidEmail = email =>
|
||||
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)
|
||||
|
||||
const ForgotPasswordView = () => {
|
||||
const { t } = useTranslation('auth')
|
||||
const navigate = useNavigate()
|
||||
const [resetStatusOk, setResetStatusOk] = useState(null)
|
||||
const [email, setEmail] = useState('')
|
||||
@@ -40,21 +42,21 @@ const ForgotPasswordView = () => {
|
||||
setResetStatusOk(true)
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Reset Email Sent',
|
||||
message: 'Check your email for password reset instructions',
|
||||
title: t('resetEmailSent'),
|
||||
message: t('resetEmailSentMsg'),
|
||||
})
|
||||
} else {
|
||||
setResetStatusOk(false)
|
||||
showError({
|
||||
title: 'Reset Failed',
|
||||
message: 'Failed to send reset email, please try again later',
|
||||
title: t('resetFailed'),
|
||||
message: t('resetFailedMsg'),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
setResetStatusOk(false)
|
||||
showError({
|
||||
title: 'Reset Failed',
|
||||
message: 'Failed to send reset email, please try again later',
|
||||
title: t('resetFailed'),
|
||||
message: t('resetFailedMsg'),
|
||||
})
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
@@ -79,7 +81,7 @@ const ForgotPasswordView = () => {
|
||||
if (resetStatusOk !== null) {
|
||||
return (
|
||||
<AuthShell
|
||||
title='Check your email'
|
||||
title={t('checkYourEmail')}
|
||||
subtitle={`If an account exists for ${email}, we've sent instructions for resetting your password.`}
|
||||
footer={<LegalLinks />}
|
||||
logoSize={0}
|
||||
@@ -101,7 +103,7 @@ const ForgotPasswordView = () => {
|
||||
sx={authButtonSx}
|
||||
onClick={() => navigate('/login')}
|
||||
>
|
||||
Back to sign in
|
||||
{t('backToSignIn')}
|
||||
</Button>
|
||||
</Box>
|
||||
</AuthShell>
|
||||
@@ -110,7 +112,7 @@ const ForgotPasswordView = () => {
|
||||
|
||||
return (
|
||||
<AuthShell
|
||||
title='Reset your password'
|
||||
title={t('resetYourPassword')}
|
||||
subtitle="Enter your email and we'll send you a link to get back into your account."
|
||||
footer={<LegalLinks />}
|
||||
logoSize={0}
|
||||
@@ -126,7 +128,7 @@ const ForgotPasswordView = () => {
|
||||
name='email'
|
||||
type='email'
|
||||
autoComplete='email'
|
||||
placeholder='you@example.com'
|
||||
placeholder={t('emailPlaceholder')}
|
||||
autoFocus
|
||||
value={email}
|
||||
error={emailError}
|
||||
@@ -152,7 +154,7 @@ const ForgotPasswordView = () => {
|
||||
underline='hover'
|
||||
onClick={() => navigate('/login')}
|
||||
>
|
||||
Back to sign in
|
||||
{t('backToSignIn')}
|
||||
</Link>
|
||||
</Typography>
|
||||
</AuthShell>
|
||||
|
||||
@@ -12,10 +12,12 @@ import { offlineDB } from '../../utils/OfflineDB'
|
||||
import { AuthSubmitButton, AuthTextField } from './AuthFields'
|
||||
import AuthShell from './AuthShell'
|
||||
import { authButtonSx } from './authStyles'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const CONNECTION_TIMEOUT_MS = 8000
|
||||
|
||||
const LoginSettings = () => {
|
||||
const { t } = useTranslation('auth')
|
||||
const Navigate = useNavigate()
|
||||
const { refetch: refetchResource } = useResource()
|
||||
const [serverURL, setServerURL] = React.useState('')
|
||||
@@ -105,7 +107,7 @@ const LoginSettings = () => {
|
||||
return {
|
||||
ok: false,
|
||||
message:
|
||||
'Hostname could not be resolved. Check the URL for typos or verify DNS.',
|
||||
t('server.dnsFailed'),
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -118,7 +120,7 @@ const LoginSettings = () => {
|
||||
return {
|
||||
ok: false,
|
||||
message:
|
||||
'Unable to reach the server. Check the URL, port, and network connection.',
|
||||
t('server.unreachable'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +128,7 @@ const LoginSettings = () => {
|
||||
return {
|
||||
ok: false,
|
||||
message:
|
||||
'Unable to reach the server. Check the URL, port, and network connection.',
|
||||
t('server.unreachable'),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +146,7 @@ const LoginSettings = () => {
|
||||
if (!isValidURL(trimmedURL)) {
|
||||
setStatus('error')
|
||||
setErrorMessage(
|
||||
'Invalid URL format. Include the protocol (http:// or https://) and port if needed.',
|
||||
t('server.invalidUrl'),
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -187,7 +189,7 @@ const LoginSettings = () => {
|
||||
|
||||
return (
|
||||
<AuthShell
|
||||
title='Server settings'
|
||||
title={t('serverSettings')}
|
||||
subtitle='Point the app at your own self-hosted Donetick server.'
|
||||
>
|
||||
<Box
|
||||
@@ -196,7 +198,7 @@ const LoginSettings = () => {
|
||||
sx={{ display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
<AuthTextField
|
||||
label='Server URL'
|
||||
label={t('server.url')}
|
||||
id='serverURL'
|
||||
name='serverURL'
|
||||
inputMode='url'
|
||||
@@ -243,7 +245,7 @@ const LoginSettings = () => {
|
||||
startDecorator={<CheckCircleOutlineIcon />}
|
||||
sx={{ mt: 2, borderRadius: '12px' }}
|
||||
>
|
||||
Connected. Taking you to sign in...
|
||||
{t('serverConnected')}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -290,7 +292,7 @@ const LoginSettings = () => {
|
||||
level='body-xs'
|
||||
sx={{ mt: 2.5, textAlign: 'center', color: 'text.secondary' }}
|
||||
>
|
||||
Changing the server clears locally cached data on this device.
|
||||
{t('serverChangeWarning')}
|
||||
</Typography>
|
||||
</AuthShell>
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Avatar, Box, Button, IconButton, Link, Typography } from '@mui/joy'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import Cookies from 'js-cookie'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { LoginSocialGoogle } from 'reactjs-social-login'
|
||||
|
||||
@@ -84,6 +85,7 @@ const SegmentedControl = ({ value, onChange, options }) => (
|
||||
)
|
||||
|
||||
const LoginView = () => {
|
||||
const { t } = useTranslation('auth')
|
||||
// Use React Query client directly to invalidate the user profile query
|
||||
const queryClient = useQueryClient()
|
||||
const { data: userProfile } = useUserProfile()
|
||||
@@ -158,23 +160,23 @@ const LoginView = () => {
|
||||
if (loginType === 'sub') {
|
||||
if (!parentUsername.trim()) {
|
||||
showError({
|
||||
title: 'Validation Error',
|
||||
message: 'Primary username is required for sub account login',
|
||||
title: t('validationError'),
|
||||
message: t('primaryUsernameRequired'),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!childName.trim()) {
|
||||
showError({
|
||||
title: 'Validation Error',
|
||||
message: 'Sub account name is required for sub account login',
|
||||
title: t('validationError'),
|
||||
message: t('subNameRequired'),
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (!username.trim()) {
|
||||
showError({
|
||||
title: 'Validation Error',
|
||||
message: 'Username is required',
|
||||
title: t('validationError'),
|
||||
message: t('usernameRequired'),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -182,8 +184,8 @@ const LoginView = () => {
|
||||
|
||||
if (!password) {
|
||||
showError({
|
||||
title: 'Validation Error',
|
||||
message: 'Password is required',
|
||||
title: t('validationError'),
|
||||
message: t('passwordRequired'),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -200,7 +202,7 @@ const LoginView = () => {
|
||||
result = await authLogin({ username: actualUsername, password })
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Login Failed',
|
||||
title: t('loginFailed'),
|
||||
message: error?.message || 'An error occurred, please try again',
|
||||
})
|
||||
return
|
||||
@@ -227,8 +229,8 @@ const LoginView = () => {
|
||||
}
|
||||
} else {
|
||||
showError({
|
||||
title: 'Login Failed',
|
||||
message: result.error || 'An error occurred, please try again',
|
||||
title: t('loginFailed'),
|
||||
message: result.error || t('genericError'),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -298,15 +300,15 @@ const LoginView = () => {
|
||||
} else {
|
||||
const providerName = provider === 'apple' ? 'Apple' : 'Google'
|
||||
showError({
|
||||
title: `${providerName} Login Failed`,
|
||||
message: `Couldn't log in with ${providerName}, please try again`,
|
||||
title: t('providerLoginFailed', { provider: providerName }),
|
||||
message: t('providerLoginFailedMsg', { provider: providerName }),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
const providerName = provider === 'apple' ? 'Apple' : 'Google'
|
||||
showError({
|
||||
title: `${providerName} Login Error`,
|
||||
message: 'Network error occurred, please try again',
|
||||
title: t('providerLoginError', { provider: providerName }),
|
||||
message: t('networkError'),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -350,7 +352,7 @@ const LoginView = () => {
|
||||
|
||||
const handleMFAError = errorMessage => {
|
||||
showError({
|
||||
title: 'Two-Factor Authentication Failed',
|
||||
title: t('mfaFailed'),
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
@@ -398,8 +400,8 @@ const LoginView = () => {
|
||||
} catch (error) {
|
||||
console.error('Failed to open OAuth browser:', error)
|
||||
showError({
|
||||
title: 'OAuth Error',
|
||||
message: 'Failed to open authentication browser',
|
||||
title: t('oauthError'),
|
||||
message: t('oauthBrowserFailed'),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -439,7 +441,7 @@ const LoginView = () => {
|
||||
<IconButton
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
aria-label='Server settings'
|
||||
aria-label={t('serverSettings')}
|
||||
onClick={() => Navigate('/login/settings')}
|
||||
>
|
||||
<SettingsOutlined />
|
||||
@@ -465,7 +467,7 @@ const LoginView = () => {
|
||||
<Typography level='title-md'>{displayName}</Typography>
|
||||
{getUserDisplayInfo(userProfile).userType === 'child' && (
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
Sub Account
|
||||
{t('subAccount')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
@@ -486,7 +488,7 @@ const LoginView = () => {
|
||||
sx={authButtonSx}
|
||||
onClick={() => apiClient.handleLogout()}
|
||||
>
|
||||
Use a different account
|
||||
{t('useDifferentAccount')}
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
@@ -499,19 +501,19 @@ const LoginView = () => {
|
||||
value={loginType}
|
||||
onChange={handleLoginModeChange}
|
||||
options={[
|
||||
{ value: 'primary', label: 'Primary Account' },
|
||||
{ value: 'sub', label: 'Sub Account' },
|
||||
{ value: 'primary', label: t('primaryAccount') },
|
||||
{ value: 'sub', label: t('subAccount') },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{loginType === 'primary' ? (
|
||||
<AuthTextField
|
||||
label='Username'
|
||||
label={t('username')}
|
||||
id='username'
|
||||
name='username'
|
||||
autoComplete='username'
|
||||
placeholder='Your username'
|
||||
placeholder={t('yourUsername')}
|
||||
autoFocus
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
@@ -519,20 +521,20 @@ const LoginView = () => {
|
||||
) : (
|
||||
<>
|
||||
<AuthTextField
|
||||
label='Primary account username'
|
||||
label={t('primaryAccountUsernameLabel')}
|
||||
id='parentUsername'
|
||||
name='parentUsername'
|
||||
autoComplete='username'
|
||||
placeholder='Enter primary account username'
|
||||
placeholder={t('primaryUsernamePlaceholder')}
|
||||
autoFocus
|
||||
value={parentUsername}
|
||||
onChange={e => setParentUsername(e.target.value)}
|
||||
/>
|
||||
<AuthTextField
|
||||
label='Sub account name'
|
||||
label={t('subAccountNameLabel')}
|
||||
id='childName'
|
||||
name='childName'
|
||||
placeholder='Enter sub account name'
|
||||
placeholder={t('subNamePlaceholder')}
|
||||
value={childName}
|
||||
onChange={e => setChildName(e.target.value)}
|
||||
/>
|
||||
@@ -544,7 +546,8 @@ const LoginView = () => {
|
||||
id='password'
|
||||
name='password'
|
||||
autoComplete='current-password'
|
||||
placeholder='Enter your password'
|
||||
label={t('passwordLabel')}
|
||||
placeholder={t('loginPasswordPlaceholder')}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
@@ -556,7 +559,7 @@ const LoginView = () => {
|
||||
underline='hover'
|
||||
onClick={handleForgotPassword}
|
||||
>
|
||||
Forgot password?
|
||||
{t('forgotPassword')}
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -568,7 +571,7 @@ const LoginView = () => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{hasSocialOptions && <AuthDivider>or continue with</AuthDivider>}
|
||||
{hasSocialOptions && <AuthDivider>{t('orContinueWith')}</AuthDivider>}
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
{showSocialLogin && !Capacitor.isNativePlatform() && (
|
||||
@@ -584,7 +587,7 @@ const LoginView = () => {
|
||||
}}
|
||||
onReject={() => {
|
||||
showError({
|
||||
title: 'Google Login Failed',
|
||||
title: t('googleLoginFailed'),
|
||||
message: "Couldn't log in with Google, please try again",
|
||||
})
|
||||
}}
|
||||
@@ -608,7 +611,7 @@ const LoginView = () => {
|
||||
} catch (error) {
|
||||
console.error('Google login error:', error)
|
||||
showError({
|
||||
title: 'Google Login Failed',
|
||||
title: t('googleLoginFailed'),
|
||||
message: `Couldn't log in with Google, please try again${
|
||||
error?.message ? `: ${error.message}` : ''
|
||||
}`,
|
||||
@@ -637,7 +640,7 @@ const LoginView = () => {
|
||||
.catch(error => {
|
||||
console.error('Apple login error:', error)
|
||||
showError({
|
||||
title: 'Apple Login Failed',
|
||||
title: t('appleLoginFailed'),
|
||||
message: "Couldn't log in with Apple, please try again",
|
||||
})
|
||||
})
|
||||
@@ -670,7 +673,7 @@ const LoginView = () => {
|
||||
underline='hover'
|
||||
onClick={() => Navigate('/signup')}
|
||||
>
|
||||
Create one
|
||||
{t('createOne')}
|
||||
</Link>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import ModalActions from '../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
|
||||
import { VerifyMFA } from '../../utils/Fetcher'
|
||||
import { authInputSx } from './authStyles'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const MFAVerificationModal = ({
|
||||
open,
|
||||
@@ -13,6 +14,7 @@ const MFAVerificationModal = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
}) => {
|
||||
const { t } = useTranslation('auth')
|
||||
const [verificationCode, setVerificationCode] = useState('')
|
||||
const [isBackupCode, setIsBackupCode] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -21,7 +23,7 @@ const MFAVerificationModal = ({
|
||||
|
||||
const handleVerify = async () => {
|
||||
if (!verificationCode.trim()) {
|
||||
setError('Please enter a verification code')
|
||||
setError(t('mfaModal.codeRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -37,14 +39,14 @@ const MFAVerificationModal = ({
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
const message =
|
||||
errorData.message || 'Invalid verification code. Please try again.'
|
||||
errorData.message || t('mfaModal.invalidCode')
|
||||
setError(message)
|
||||
onError?.(message)
|
||||
}
|
||||
} catch (error) {
|
||||
// A wrong code is shown inline; a failed request is escalated to the
|
||||
// caller so it can surface a toast instead of looking like a bad code.
|
||||
const message = 'Failed to verify code. Please try again.'
|
||||
const message = t('mfaModal.verifyFailed')
|
||||
setError(message)
|
||||
onError?.(message)
|
||||
console.error('MFA verification error:', error)
|
||||
@@ -73,23 +75,23 @@ const MFAVerificationModal = ({
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
size='md'
|
||||
title='Two-factor authentication'
|
||||
title={t('mfaModal.title')}
|
||||
description={
|
||||
isBackupCode
|
||||
? 'Enter one of the backup codes you saved when setting up two-factor authentication.'
|
||||
: 'Enter the 6-digit code from your authenticator app.'
|
||||
? t('mfaModal.backupHint')
|
||||
: t('mfaModal.codeHint')
|
||||
}
|
||||
closeOnBackdrop={!loading}
|
||||
closeOnEscape={!loading}
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{
|
||||
label: 'Cancel',
|
||||
label: t('common:cancel'),
|
||||
onClick: handleClose,
|
||||
disabled: loading,
|
||||
}}
|
||||
primary={{
|
||||
label: 'Verify & Sign In',
|
||||
label: t('mfaModal.verify'),
|
||||
onClick: handleVerify,
|
||||
loading,
|
||||
disabled: !verificationCode.trim(),
|
||||
@@ -105,12 +107,12 @@ const MFAVerificationModal = ({
|
||||
level='body-sm'
|
||||
sx={{ display: 'block', fontWeight: 600, mb: 0.75 }}
|
||||
>
|
||||
{isBackupCode ? 'Backup code' : 'Verification code'}
|
||||
{isBackupCode ? t('mfaModal.backupLabel') : t('mfaModal.codeLabel')}
|
||||
</Typography>
|
||||
<Input
|
||||
id='mfa-code'
|
||||
size='lg'
|
||||
placeholder={isBackupCode ? 'Enter backup code' : '000000'}
|
||||
placeholder={isBackupCode ? t('mfaModal.backupPlaceholder') : '000000'}
|
||||
value={verificationCode}
|
||||
onChange={e => setVerificationCode(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -157,8 +159,8 @@ const MFAVerificationModal = ({
|
||||
}}
|
||||
>
|
||||
{isBackupCode
|
||||
? 'Use authenticator app instead'
|
||||
: 'Use a backup code instead'}
|
||||
? t('mfaModal.useAuthenticator')
|
||||
: t('mfaModal.useBackup')}
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
@@ -167,7 +169,7 @@ const MFAVerificationModal = ({
|
||||
level='body-xs'
|
||||
sx={{ textAlign: 'center', color: 'text.secondary' }}
|
||||
>
|
||||
Each backup code can only be used once.
|
||||
{t('mfaModal.backupOnce')}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Box, Link, Typography } from '@mui/joy'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { useAuth } from '../../hooks/useAuth.jsx'
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
import AuthShell from './AuthShell'
|
||||
|
||||
const SignupView = () => {
|
||||
const { t } = useTranslation('auth')
|
||||
const [username, setUsername] = React.useState('')
|
||||
const [password, setPassword] = React.useState('')
|
||||
const Navigate = useNavigate()
|
||||
@@ -38,7 +40,7 @@ const SignupView = () => {
|
||||
showError({
|
||||
title: 'Almost there',
|
||||
message:
|
||||
'Your account was created, but signing in failed. Please sign in.',
|
||||
t('signupSignInFailed'),
|
||||
})
|
||||
Navigate('/login')
|
||||
return
|
||||
@@ -72,36 +74,36 @@ const SignupView = () => {
|
||||
let isValid = true
|
||||
|
||||
if (!username.trim()) {
|
||||
setUsernameError('Username is required')
|
||||
setUsernameError(t('usernameRequired'))
|
||||
isValid = false
|
||||
}
|
||||
if (username.length < 4) {
|
||||
setUsernameError('Username must be at least 4 characters')
|
||||
setUsernameError(t('usernameMinLength'))
|
||||
isValid = false
|
||||
}
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
setEmailError('Invalid email address')
|
||||
setEmailError(t('invalidEmail'))
|
||||
isValid = false
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
setPasswordError('Password must be between 8 and 64 characters')
|
||||
setPasswordError(t('passwordLength'))
|
||||
isValid = false
|
||||
}
|
||||
|
||||
if (password.length > 64) {
|
||||
setPasswordError('Password must be between 8 and 64 characters')
|
||||
setPasswordError(t('passwordLength'))
|
||||
isValid = false
|
||||
}
|
||||
|
||||
if (!displayName.trim()) {
|
||||
setDisplayNameError('Display name is required')
|
||||
setDisplayNameError(t('displayNameRequired'))
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// display name should only contain letters and spaces and numbers:
|
||||
if (!/^[a-zA-Z0-9 ]+$/.test(displayName)) {
|
||||
setDisplayNameError('Display name can only contain letters and numbers')
|
||||
setDisplayNameError(t('displayNameChars'))
|
||||
isValid = false
|
||||
}
|
||||
|
||||
@@ -127,14 +129,14 @@ const SignupView = () => {
|
||||
handleLogin(username, password)
|
||||
} else if (response.status === 403) {
|
||||
showError({
|
||||
title: 'Signup Failed',
|
||||
message: 'Signup disabled, please contact admin',
|
||||
title: t('signupFailed'),
|
||||
message: t('signupDisabled'),
|
||||
})
|
||||
} else {
|
||||
console.log('Signup failed')
|
||||
response.json().then(res => {
|
||||
showError({
|
||||
title: 'Signup Failed',
|
||||
title: t('signupFailed'),
|
||||
message: res.error || 'An error occurred during signup',
|
||||
})
|
||||
})
|
||||
@@ -145,7 +147,7 @@ const SignupView = () => {
|
||||
|
||||
return (
|
||||
<AuthShell
|
||||
title='Create your account'
|
||||
title={t('createYourAccount')}
|
||||
subtitle={
|
||||
getPendingInvite()
|
||||
? 'Create an account and we’ll send your circle join request right after.'
|
||||
@@ -160,11 +162,11 @@ const SignupView = () => {
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
|
||||
>
|
||||
<AuthTextField
|
||||
label='Display name'
|
||||
label={t('displayNameLabel')}
|
||||
id='displayName'
|
||||
name='displayName'
|
||||
autoComplete='name'
|
||||
placeholder='How others see your name'
|
||||
placeholder={t('displayNamePlaceholder')}
|
||||
autoFocus
|
||||
value={displayName}
|
||||
error={displayNameError}
|
||||
@@ -175,11 +177,11 @@ const SignupView = () => {
|
||||
/>
|
||||
|
||||
<AuthTextField
|
||||
label='Username'
|
||||
label={t('username')}
|
||||
id='username'
|
||||
name='username'
|
||||
autoComplete='username'
|
||||
placeholder='lowercase letters, numbers, dot and dash'
|
||||
placeholder={t('usernameHint')}
|
||||
value={username}
|
||||
error={usernameError}
|
||||
onChange={e => {
|
||||
@@ -189,12 +191,12 @@ const SignupView = () => {
|
||||
/>
|
||||
|
||||
<AuthTextField
|
||||
label='Email'
|
||||
label={t('email')}
|
||||
id='email'
|
||||
name='email'
|
||||
type='email'
|
||||
autoComplete='email'
|
||||
placeholder='you@example.com'
|
||||
placeholder={t('emailPlaceholder')}
|
||||
value={email}
|
||||
error={emailError}
|
||||
onChange={e => {
|
||||
@@ -207,10 +209,11 @@ const SignupView = () => {
|
||||
id='password'
|
||||
name='password'
|
||||
autoComplete='new-password'
|
||||
placeholder='At least 8 characters'
|
||||
label={t('passwordLabel')}
|
||||
placeholder={t('signupPasswordPlaceholder')}
|
||||
value={password}
|
||||
error={passwordError}
|
||||
helper='Use 8 to 64 characters.'
|
||||
helper={t('signupPasswordHelper')}
|
||||
onChange={e => {
|
||||
setPasswordError(null)
|
||||
setPassword(e.target.value)
|
||||
@@ -218,14 +221,14 @@ const SignupView = () => {
|
||||
/>
|
||||
|
||||
<AuthSubmitButton loading={isSubmitting} sx={{ mt: 1 }}>
|
||||
Create account
|
||||
{t('createAccountButton')}
|
||||
</AuthSubmitButton>
|
||||
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ textAlign: 'center', color: 'text.secondary' }}
|
||||
>
|
||||
By creating an account you agree to our Terms of Service and Privacy
|
||||
{t('termsShort')}
|
||||
Policy.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Button } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
@@ -9,6 +10,7 @@ import AuthShell from './AuthShell'
|
||||
import { authButtonSx } from './authStyles'
|
||||
|
||||
const UpdatePasswordView = () => {
|
||||
const { t } = useTranslation('auth')
|
||||
const navigate = useNavigate()
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordConfirm, setPasswordConfirm] = useState('')
|
||||
@@ -67,23 +69,22 @@ const UpdatePasswordView = () => {
|
||||
if (response.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Password Updated',
|
||||
message:
|
||||
'Your password has been updated successfully. Redirecting to login...',
|
||||
title: t('passwordUpdated'),
|
||||
message: t('passwordUpdatedMsg'),
|
||||
})
|
||||
setTimeout(() => {
|
||||
navigate('/login')
|
||||
}, 3000)
|
||||
} else {
|
||||
showError({
|
||||
title: 'Password Update Failed',
|
||||
message: 'Failed to update password, please try again later',
|
||||
title: t('passwordUpdateFailed'),
|
||||
message: t('passwordUpdateFailedMsg'),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Password Update Failed',
|
||||
message: 'Failed to update password, please try again later',
|
||||
title: t('passwordUpdateFailed'),
|
||||
message: t('passwordUpdateFailedMsg'),
|
||||
})
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
@@ -93,7 +94,7 @@ const UpdatePasswordView = () => {
|
||||
if (!verificationCode) {
|
||||
return (
|
||||
<AuthShell
|
||||
title='This link is not valid'
|
||||
title={t('linkInvalid')}
|
||||
subtitle='The password reset link is incomplete or has already been used. Request a new one to continue.'
|
||||
footer={<LegalLinks />}
|
||||
showLogo
|
||||
@@ -106,7 +107,7 @@ const UpdatePasswordView = () => {
|
||||
sx={authButtonSx}
|
||||
onClick={() => navigate('/forgot-password')}
|
||||
>
|
||||
Request a new link
|
||||
{t('requestNewLink')}
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
@@ -116,7 +117,7 @@ const UpdatePasswordView = () => {
|
||||
sx={authButtonSx}
|
||||
onClick={() => navigate('/login')}
|
||||
>
|
||||
Back to sign in
|
||||
{t('backToSignIn')}
|
||||
</Button>
|
||||
</Box>
|
||||
</AuthShell>
|
||||
@@ -125,7 +126,7 @@ const UpdatePasswordView = () => {
|
||||
|
||||
return (
|
||||
<AuthShell
|
||||
title='Set a new password'
|
||||
title={t('setNewPassword')}
|
||||
subtitle='Choose a password you have not used on this account before.'
|
||||
footer={<LegalLinks />}
|
||||
// Reached from an emailed link, usually in a browser: an unbranded page
|
||||
@@ -138,7 +139,7 @@ const UpdatePasswordView = () => {
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
|
||||
>
|
||||
<AuthPasswordField
|
||||
label='New password'
|
||||
label={t('newPassword')}
|
||||
id='password'
|
||||
name='password'
|
||||
autoComplete='new-password'
|
||||
@@ -151,18 +152,18 @@ const UpdatePasswordView = () => {
|
||||
/>
|
||||
|
||||
<AuthPasswordField
|
||||
label='Confirm new password'
|
||||
label={t('confirmNewPassword')}
|
||||
id='passwordConfirm'
|
||||
name='passwordConfirm'
|
||||
autoComplete='new-password'
|
||||
placeholder='Re-enter your password'
|
||||
placeholder={t('reenterPassword')}
|
||||
value={passwordConfirm}
|
||||
error={passwordConfirmationError}
|
||||
onChange={handlePasswordConfirmChange}
|
||||
/>
|
||||
|
||||
<AuthSubmitButton loading={isSubmitting} sx={{ mt: 1 }}>
|
||||
Save password
|
||||
{t('savePassword')}
|
||||
</AuthSubmitButton>
|
||||
|
||||
<Button
|
||||
@@ -174,7 +175,7 @@ const UpdatePasswordView = () => {
|
||||
sx={authButtonSx}
|
||||
onClick={() => navigate('/login')}
|
||||
>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
</AuthShell>
|
||||
|
||||
Reference in New Issue
Block a user