Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions src/ReturnUserExperience/OTPInput/OTPInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import React, { useEffect } from 'react'

import { Text } from '@mxenabled/mxui'
import Button from '@mui/material/Button'
import { Stack, styled } from '@mui/system'
import { TextField } from 'src/privacy/input'

import { __ } from 'src/utilities/Intl'
import styles from './otpInput.module.css'

const RESEND_OTP_INTERVAL = 10
const OTP_LENGTH = 6

const getOtpDigits = (value: string) => {
const digits = value.replace(/\D/g, '').slice(0, OTP_LENGTH).split('')
return [...digits, ...new Array(OTP_LENGTH - digits.length).fill('')]
}

export const OTPInput = ({
onChange,
value,
}: {
onChange: React.Dispatch<React.SetStateAction<string>>
value: string
}) => {
const inputRefs = React.useRef<Array<HTMLInputElement | null>>(
Array.from({ length: OTP_LENGTH }, () => null),
)
const [seconds, setSeconds] = React.useState<number>(RESEND_OTP_INTERVAL)
const otpDigits = React.useMemo(() => getOtpDigits(value), [value])

useEffect(() => {
// This timer is a delay before the user can request a new OTP.
setSeconds(RESEND_OTP_INTERVAL)
const timer = window.setInterval(() => {
setSeconds((s) => {
if (s <= 1) {
clearInterval(timer)
return 0
}
return s - 1
})
}, 1000)

return () => clearInterval(timer)
}, [])

const focusInput = (targetIndex: number) => {
inputRefs.current[targetIndex]?.focus()
}

const selectInput = (targetIndex: number) => {
inputRefs.current[targetIndex]?.select()
}

const updateOtp = (updater: (digits: string[]) => void) => {
onChange((prev) => {
const digits = getOtpDigits(prev)
updater(digits)
return digits.join('')
})
}

const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, currentIndex: number) => {
switch (event.key) {
case 'ArrowUp':
case 'ArrowDown':
case ' ':
event.preventDefault()
break
case 'ArrowLeft':
event.preventDefault()
if (currentIndex > 0) {
focusInput(currentIndex - 1)
selectInput(currentIndex - 1)
}
break
case 'ArrowRight':
event.preventDefault()
if (currentIndex < OTP_LENGTH - 1) {
focusInput(currentIndex + 1)
selectInput(currentIndex + 1)
}
break
case 'Delete':
event.preventDefault()
updateOtp((digits) => {
digits[currentIndex] = ''
})
break
case 'Backspace': {
event.preventDefault()
const isEmpty = event.currentTarget.value === ''

updateOtp((digits) => {
if (isEmpty && currentIndex > 0) {
digits[currentIndex - 1] = ''
} else {
digits[currentIndex] = ''
}
})

if (isEmpty && currentIndex > 0) {
focusInput(currentIndex - 1)
selectInput(currentIndex - 1)
}
break
}
default:
break
}
}

const handleChange = (event: React.ChangeEvent<HTMLInputElement>, currentIndex: number) => {
const nextValue = event.target.value.replace(/\D/g, '').slice(-1)

updateOtp((digits) => {
digits[currentIndex] = nextValue
})

if (nextValue && currentIndex < OTP_LENGTH - 1) {
focusInput(currentIndex + 1)
}
}

const handlePaste = (event: React.ClipboardEvent<HTMLInputElement>, currentIndex: number) => {
event.preventDefault()

const pastedText = event.clipboardData
.getData('text/plain')
.replace(/\D/g, '')
.slice(0, OTP_LENGTH - currentIndex)

if (!pastedText) {
return
}

updateOtp((digits) => {
for (let index = 0; index < pastedText.length; index += 1) {
digits[currentIndex + index] = pastedText[index]
}
})

setTimeout(() => {
const focusIndex = Math.min(currentIndex + pastedText.length, OTP_LENGTH - 1)
focusInput(focusIndex)
selectInput(focusIndex)
}, 0)
}

return (
<>
<Stack alignItems="center" className={styles.container} direction="row" spacing="8px">
{Array.from({ length: OTP_LENGTH }, (_, index) => (
<OTPTextField
aria-label={`Digit ${index + 1} of ${OTP_LENGTH}`}
inputProps={{
maxLength: 1,
inputMode: 'numeric',
pattern: '[0-9]*',
autoComplete: 'one-time-code',
}}
inputRef={(ele: HTMLInputElement | null) => {
inputRefs.current[index] = ele
}}
key={index}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => handleChange(event, index)}
onFocus={() => selectInput(index)}
onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) =>
handleKeyDown(event, index)
}
onPaste={(event: React.ClipboardEvent<HTMLInputElement>) => handlePaste(event, index)}
size="small"
value={otpDigits[index]}
variant="outlined"
/>
))}
</Stack>

{seconds > 0 ? (
<Text truncate={false} variant="caption">
{__('Resend code in (%1 seconds)', seconds)}
</Text>
) : (
<Button fullWidth={true} onClick={() => {}} size="small" variant="text">
{__('Resend code')}
</Button>
)}
</>
)
}

const OTPTextField = styled(TextField)(() => ({
height: '60px',
'& .MuiInputBase-input': {
textAlign: 'center',
fontSize: '23px',
},
'& .MuiOutlinedInput-root': {
height: '100%',
},
}))
3 changes: 3 additions & 0 deletions src/ReturnUserExperience/OTPInput/otpInput.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.container {
margin: 40px 0 0;
}
8 changes: 7 additions & 1 deletion src/ReturnUserExperience/ReturnUserExperience.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { useDispatch, useSelector } from 'react-redux'

import styles from './returnUserExperience.module.css'
import RuxInfo from 'src/ReturnUserExperience/RuxInfo'
import { RuxPhoneNumber } from 'src/ReturnUserExperience/RuxPhoneNumber'
import RuxPhoneNumber from 'src/ReturnUserExperience/RuxPhoneNumber'
import RuxOtp from 'src/ReturnUserExperience/RuxOtp'
import RuxList from 'src/ReturnUserExperience/RuxList'

import { Stack } from '@mui/material'
import { Icon } from '@mxenabled/mxui'
Expand Down Expand Up @@ -71,6 +73,10 @@ export const ReturnUserExperience = React.forwardRef(() => {
userEnteredPhone={userEnteredPhone}
/>
)}

{view === RUXViews.OTP && <RuxOtp />}

{view === RUXViews.LIST && <RuxList />}
</div>
)
})
Expand Down
33 changes: 33 additions & 0 deletions src/ReturnUserExperience/RuxList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import Stack from '@mui/material/Stack'
import { Text } from '@mxenabled/mxui'
import { Icon } from '@mxenabled/mxui'
import Button from '@mui/material/Button'

import { __ } from 'src/utilities/Intl'
import styles from './returnUserExperience.module.css'

export const RuxList = () => {
return (
<>
<Text variant="h2">{__('Select your institution')}</Text>
<Text variant="subtitle1">
{__('Choose a previously connected institution or add a new one.')}
</Text>

{/* Connections list goes here */}
<p>Connections list goes here</p>

<Stack className={styles.buttonContainer} spacing="8px">
<Button variant="outlined">
<Icon name="plus" /> {__('Connect new institution')}
</Button>
<Text variant="subtitle2">
{__('Disconnect accounts from MX anytime by contacting support.')}
</Text>
</Stack>
</>
)
}

export default RuxList
35 changes: 35 additions & 0 deletions src/ReturnUserExperience/RuxOtp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react'
import Stack from '@mui/material/Stack'
import { Text } from '@mxenabled/mxui'
import Button from '@mui/material/Button'

import { __ } from 'src/utilities/Intl'
import styles from './returnUserExperience.module.css'
import { OTPInput } from 'src/ReturnUserExperience/OTPInput/OTPInput'

export const RuxOtp = () => {
const [otp, setOtp] = React.useState('')

return (
<>
<Stack className={styles.titleContainer} spacing="6px">
<Text bold={true} className={styles.centerText} truncate={false} variant="h2">
{__('Verify your phone number')}
</Text>
<Text className={styles.centerText} truncate={false} variant="subtitle1">
{__('Enter the code sent to ••• ••• 1234.')}
</Text>
</Stack>

<OTPInput onChange={setOtp} value={otp} />

<Stack className={styles.buttonContainer}>
<Button onClick={() => {}} variant="contained">
{__('Continue')}
</Button>
</Stack>
</>
)
}

export default RuxOtp
Loading