Skip to content

Commit

Permalink
feat: send mail when login and device reach limit
Browse files Browse the repository at this point in the history
  • Loading branch information
Ponchimeow authored and Logos50607 committed Jan 1, 2025
1 parent 8fff2ff commit ef62af0
Show file tree
Hide file tree
Showing 12 changed files with 709 additions and 55 deletions.
33 changes: 19 additions & 14 deletions src/components/auth/login/LoginSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AxiosError } from 'axios'
import { CommonTitleMixin } from 'lodestar-app-element/src/components/common'
import { useApp } from 'lodestar-app-element/src/contexts/AppContext'
import { useAuth } from 'lodestar-app-element/src/contexts/AuthContext'
import { BackendServerError, BindDeviceError, LoginDeviceError } from 'lodestar-app-element/src/helpers/error'
import { BackendServerError, LoginDeviceError } from 'lodestar-app-element/src/helpers/error'
import { useTracking } from 'lodestar-app-element/src/hooks/tracking'
import React, { useContext, useState } from 'react'
import { useForm } from 'react-hook-form'
Expand Down Expand Up @@ -56,9 +56,10 @@ const LoginSection: React.VFC<{
const history = useHistory()
const [returnTo] = useQueryParam('returnTo', StringParam)
const { login, forceLogin } = useAuth()
const { setVisible, setIsBusinessMember } = useContext(AuthModalContext)
const { setVisible: setAuthModalVisible, setIsBusinessMember } = useContext(AuthModalContext)
const [loading, setLoading] = useState(false)
const [forceLoginLoading, setForceLoginLoading] = useState(false)
const [currentMember, setCurrentMember] = useState<{ id: string; email: string }>({ id: '', email: '' })
const { register, handleSubmit, reset } = useForm({
defaultValues: {
account: '',
Expand All @@ -70,11 +71,10 @@ const LoginSection: React.VFC<{
const [passwordShow, setPasswordShow] = useState(false)

const handleLogin = handleSubmit(
({ account, password }) => {
async ({ account, password }) => {
if (login === undefined) {
return
}

setLoading(true)
login({
account: account.trim().toLowerCase(),
Expand All @@ -83,14 +83,16 @@ const LoginSection: React.VFC<{
})
.then(() => {
tracking.login()
setVisible?.(false)
setAuthModalVisible?.(false)
reset()
returnTo && history.push(returnTo)
})
.catch(error => {
if (error instanceof LoginDeviceError) {
setIsOverLoginDeviceModalVisible(true)
} else if (error instanceof BindDeviceError) {
}
if (error?.code === 'E_BIND_DEVICE') {
setCurrentMember({ id: error?.result?.member?.id || '', email: error?.result?.member?.email || '' })
setIsOverBindDeviceModalVisible(true)
}

Expand Down Expand Up @@ -123,7 +125,7 @@ const LoginSection: React.VFC<{
})
.then(() => {
tracking.login()
setVisible?.(false)
setAuthModalVisible?.(false)
reset()
returnTo && history.push(returnTo)
message.success(formatMessage(localAuthMessages.default.LoginSection.loginSuccess))
Expand Down Expand Up @@ -233,13 +235,16 @@ const LoginSection: React.VFC<{
loading={forceLoginLoading}
/>

<OverBindDeviceModal
visible={isOverBindDeviceModalVisible}
onClose={() => {
setIsOverBindDeviceModalVisible(false)
setLoading(false)
}}
/>
{currentMember.email !== '' && currentMember.id !== '' ? (
<OverBindDeviceModal
member={currentMember}
visible={isOverBindDeviceModalVisible}
onClose={() => {
setIsOverBindDeviceModalVisible(false)
setLoading(false)
}}
/>
) : null}
</>
)}
</>
Expand Down
126 changes: 120 additions & 6 deletions src/components/auth/login/OverBindDeviceModal.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,103 @@
import { useIntl } from 'react-intl'
import * as localAuthMessages from '../translation'
import authMessages, * as localAuthMessages from '../translation'
import { StyledModal, StyledModalTitle } from './LoginSection'
import { Input, Text, Flex, Box, Button, useToast } from '@chakra-ui/react'
import { useContext, useState } from 'react'
import { useApp } from 'lodestar-app-element/src/contexts/AppContext'
import { handleError } from '../../../helpers'
import { AuthModalContext } from '../AuthModal'
import axios from 'axios'
import { fetchCurrentGeolocation } from '../../../hooks/util'

const OverBindDeviceModal: React.VFC<{
const OverBindDeviceModal: React.FC<{
member: { id: string; email: string }
visible: boolean
onClose: () => void
}> = ({ visible, onClose }) => {
}> = ({ member, visible, onClose }) => {
const eventType = 'login-device-limit'
const toast = useToast()
const { id: appId } = useApp()
const { formatMessage } = useIntl()
const { setVisible: setAuthModalVisible } = useContext(AuthModalContext)
const [currentCode, setCurrentCode] = useState('')
const [error, setError] = useState(false)

const handleConfirm = async () => {
try {
await axios
.post(`${process.env.REACT_APP_LODESTAR_SERVER_ENDPOINT}/mail-verification-code/verify`, {
appId,
email: member.email,
memberId: member.id,
type: eventType,
code: currentCode,
})
.then(({ data: { code, result } }) => {
if (code !== 'SUCCESS') {
toast({
title: formatMessage(authMessages.OverBindDeviceModal.validationFailed),
status: 'error',
duration: 3000,
isClosable: false,
position: 'top',
})
if (result?.intervalTime) {
localStorage.setItem('mail-last-sent-time', result?.intervalTime || '0')
}
setError(true)
} else {
toast({
title: formatMessage(authMessages.OverBindDeviceModal.validationSuccessfulText),
status: 'success',
duration: 3000,
isClosable: false,
position: 'top',
})
localStorage.removeItem('mail-last-sent-time')
setError(false)
onClose()
}
setAuthModalVisible?.(true)
})
} catch (error) {
handleError(error)
}
}

const handleReSend = async () => {
try {
const { ip } = await fetchCurrentGeolocation()
await axios
.post(`${process.env.REACT_APP_LODESTAR_SERVER_ENDPOINT}/mail-verification-code/send`, {
appId,
email: member.email,
type: eventType,
ip,
})
.then(({ data: { code, result } }) => {
if (code === 'SUCCESS') {
toast({
title: formatMessage(authMessages.OverBindDeviceModal.sentSuccessfully),
status: 'success',
duration: 3000,
isClosable: false,
position: 'top',
})
} else {
toast({
title: formatMessage(authMessages.OverBindDeviceModal.failedToSend),
status: 'error',
duration: 3000,
isClosable: false,
position: 'top',
})
localStorage.setItem('mail-last-sent-time', result?.lastSentTime)
}
})
} catch (error) {
handleError(error)
}
}

return (
<StyledModal
Expand All @@ -15,14 +106,37 @@ const OverBindDeviceModal: React.VFC<{
visible={visible}
okText={formatMessage(localAuthMessages.default.LoginSection.deviceReachLimitConfirm)}
okButtonProps={{ type: 'primary' }}
onOk={onClose}
onOk={handleConfirm}
cancelText={null}
onCancel={onClose}
onCancel={() => {
onClose()
setError(false)
}}
>
<StyledModalTitle className="mb-4">
{formatMessage(localAuthMessages.default.LoginSection.deviceReachLimitTitle)}
</StyledModalTitle>
<div className="mb-4">{formatMessage(localAuthMessages.default.LoginSection.deviceReachLimitDescription)}</div>
<div>{formatMessage(localAuthMessages.default.LoginSection.deviceReachLimitDescription)}</div>
<div className="mb-4">
{formatMessage(localAuthMessages.default.LoginSection.yourEmail, { email: member.email })}
</div>
<div>
<Input
value={currentCode}
onChange={v => setCurrentCode(v.target.value.trim())}
placeholder={formatMessage(authMessages.OverBindDeviceModal.deviceVerificationCode)}
style={{ border: error ? 'red 1px solid' : undefined }}
/>
<Box mt="0.5rem" display={error ? 'block' : 'none'}>
<Text textColor="#FF0000">{formatMessage(authMessages.OverBindDeviceModal.validationCodeError)}</Text>
</Box>
</div>
<Flex justifyContent="center" alignItems="center" mt="1rem">
<Box>{formatMessage(authMessages.OverBindDeviceModal.didNotReceiveVerificationCode)}</Box>
<Button id="send" onClick={() => handleReSend()} variant="ghost" color="primary.500" fontWeight="500">
{formatMessage(authMessages.OverBindDeviceModal.reSend)}
</Button>
</Flex>
</StyledModal>
)
}
Expand Down
33 changes: 30 additions & 3 deletions src/components/auth/translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,43 @@ const authMessages = {
},
deviceReachLimitTitle: {
id: 'auth.LoginSection.deviceReachLimitTitle',
defaultMessage: '裝置綁定已達上限',
defaultMessage: '裝置已達上限',
},
deviceReachLimitDescription: {
id: 'auth.LoginSection.deviceReachLimitDescription',
defaultMessage: '裝置綁定已達上限,請至裝置管理頁移除裝置,方可於當前裝置登入。',
defaultMessage:
'Your account has reached the device usage limit. Please receive a verification email and enter the verification code to log in.',
},
deviceReachLimitConfirm: {
id: 'auth.LoginSection.deviceReachLimitConfirm',
defaultMessage: '確定',
defaultMessage: 'Send',
},
yourEmail: {
id: 'auth.LoginSection.yourEmail',
defaultMessage: 'your email: {email}',
},
}),
OverBindDeviceModal: defineMessages({
deviceVerificationCode: {
id: 'auth.OverBindDeiceModal.deviceVerificationCode',
defaultMessage: 'Device verification code',
},
validationSuccessfulText: {
id: 'auth.OverBindDeiceModal.validationSuccessful',
defaultMessage: 'Validation successful. Please log in again',
},
validationCodeError: {
id: 'auth.OverBindDeiceModal.validationCodeError',
defaultMessage: 'Verification code error',
},
didNotReceiveVerificationCode: {
id: 'auth.OverBindDeiceModal.didNotReceiveVerificationCode',
defaultMessage: 'Did not receive the verification code?',
},
reSend: { id: 'auth.OverBindDeiceModal.reSend', defaultMessage: 'Resend' },
validationFailed: { id: 'auth.OverBindDeiceModal.validationFailed', defaultMessage: 'Validation failed' },
sentSuccessfully: { id: 'auth.OverBindDeiceModal.sentSuccessfully', defaultMessage: 'Sent successfully' },
failedToSend: { id: 'auth.OverBindDeiceModal.failedToSend', defaultMessage: 'Failed to send' },
}),
}

Expand Down
Loading

0 comments on commit ef62af0

Please sign in to comment.