From ec8d2ddea652396aec8544b8244dff03bb55b1dc Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:28:49 -0600 Subject: [PATCH 1/5] feat: add RuxPhoneNumber component for phone number input in ReturnUserExperience --- src/ReturnUserExperience/ReturnUserExperience.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index ae23b12137..a93d7e3082 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -71,6 +71,13 @@ export const ReturnUserExperience = React.forwardRef(() => { userEnteredPhone={userEnteredPhone} /> )} + + {view === RUXViews.PHONE_NUMBER && ( + + )} ) }) From a4b1e31b4c121238250a5d2ef824dc24d1358dc3 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:28:49 -0600 Subject: [PATCH 2/5] feat: add RuxPhoneNumber component for phone number input in ReturnUserExperience --- src/ReturnUserExperience/ReturnUserExperience.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index a93d7e3082..dee4b9648b 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -78,6 +78,13 @@ export const ReturnUserExperience = React.forwardRef(() => { userEnteredPhone={userEnteredPhone} /> )} + + {view === RUXViews.PHONE_NUMBER && ( + + )} ) }) From 3f2d3f60e36fc628a00827c3f1627e13aafc2260 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:33:17 -0600 Subject: [PATCH 3/5] feat: add RuxPhoneNumber component for phone number input in ReturnUserExperience --- .../ReturnUserExperience.tsx | 5 +++++ src/ReturnUserExperience/RuxOtp.tsx | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/ReturnUserExperience/RuxOtp.tsx diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index dee4b9648b..86d4721e57 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -4,6 +4,7 @@ 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 RuxOtp from 'src/ReturnUserExperience/RuxOtp' import { Stack } from '@mui/material' import { Icon } from '@mxenabled/mxui' @@ -85,6 +86,10 @@ export const ReturnUserExperience = React.forwardRef(() => { userEnteredPhone={userEnteredPhone} /> )} + + {view === RUXViews.OTP && } + + {view === RUXViews.LIST &&
{__('List of connections goes here.')}
} ) }) diff --git a/src/ReturnUserExperience/RuxOtp.tsx b/src/ReturnUserExperience/RuxOtp.tsx new file mode 100644 index 0000000000..391858583c --- /dev/null +++ b/src/ReturnUserExperience/RuxOtp.tsx @@ -0,0 +1,22 @@ +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' + +export const RuxPhoneNumber = () => { + return ( + <> + {/* OTP Style Input */} + OTP Style Input + + + Resend code in (10 seconds) + + + ) +} + +export default RuxPhoneNumber From a78d8004c7dbafc24a0cb027430d008dcfd6b1a1 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:39:37 -0600 Subject: [PATCH 4/5] feat: add RuxList component to display institution selection in ReturnUserExperience --- .../ReturnUserExperience.tsx | 5 +-- src/ReturnUserExperience/RuxList.tsx | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 src/ReturnUserExperience/RuxList.tsx diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index 86d4721e57..56b8ebfcff 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -3,8 +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' @@ -89,7 +90,7 @@ export const ReturnUserExperience = React.forwardRef(() => { {view === RUXViews.OTP && } - {view === RUXViews.LIST &&
{__('List of connections goes here.')}
} + {view === RUXViews.LIST && } ) }) diff --git a/src/ReturnUserExperience/RuxList.tsx b/src/ReturnUserExperience/RuxList.tsx new file mode 100644 index 0000000000..fc20ccb25e --- /dev/null +++ b/src/ReturnUserExperience/RuxList.tsx @@ -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 ( + <> + {__('Select your institution')} + + {__('Choose a previously connected institution or add a new one.')} + + + {/* Connections list goes here */} +

Connections list goes here

+ + + + + {__('Disconnect accounts from MX anytime by contacting support.')} + + + + ) +} + +export default RuxList From 2360b1bd0edff6daa1cdcb6959ffe2d210b4439c Mon Sep 17 00:00:00 2001 From: Jameson Date: Wed, 3 Jun 2026 08:55:01 -0600 Subject: [PATCH 5/5] feat: implement OTP input component and integrate into RuxOtp --- .../OTPInput/OTPInput.tsx | 202 ++++++++++++++++++ .../OTPInput/otpInput.module.css | 3 + .../ReturnUserExperience.tsx | 14 -- src/ReturnUserExperience/RuxOtp.tsx | 27 ++- 4 files changed, 225 insertions(+), 21 deletions(-) create mode 100644 src/ReturnUserExperience/OTPInput/OTPInput.tsx create mode 100644 src/ReturnUserExperience/OTPInput/otpInput.module.css diff --git a/src/ReturnUserExperience/OTPInput/OTPInput.tsx b/src/ReturnUserExperience/OTPInput/OTPInput.tsx new file mode 100644 index 0000000000..dccdc5f053 --- /dev/null +++ b/src/ReturnUserExperience/OTPInput/OTPInput.tsx @@ -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> + value: string +}) => { + const inputRefs = React.useRef>( + Array.from({ length: OTP_LENGTH }, () => null), + ) + const [seconds, setSeconds] = React.useState(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, 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, 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, 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 ( + <> + + {Array.from({ length: OTP_LENGTH }, (_, index) => ( + { + inputRefs.current[index] = ele + }} + key={index} + onChange={(event: React.ChangeEvent) => handleChange(event, index)} + onFocus={() => selectInput(index)} + onKeyDown={(event: React.KeyboardEvent) => + handleKeyDown(event, index) + } + onPaste={(event: React.ClipboardEvent) => handlePaste(event, index)} + size="small" + value={otpDigits[index]} + variant="outlined" + /> + ))} + + + {seconds > 0 ? ( + + {__('Resend code in (%1 seconds)', seconds)} + + ) : ( + + )} + + ) +} + +const OTPTextField = styled(TextField)(() => ({ + height: '60px', + '& .MuiInputBase-input': { + textAlign: 'center', + fontSize: '23px', + }, + '& .MuiOutlinedInput-root': { + height: '100%', + }, +})) diff --git a/src/ReturnUserExperience/OTPInput/otpInput.module.css b/src/ReturnUserExperience/OTPInput/otpInput.module.css new file mode 100644 index 0000000000..045bbfa0f6 --- /dev/null +++ b/src/ReturnUserExperience/OTPInput/otpInput.module.css @@ -0,0 +1,3 @@ +.container { + margin: 40px 0 0; +} diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index 56b8ebfcff..4714568c6f 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -74,20 +74,6 @@ export const ReturnUserExperience = React.forwardRef(() => { /> )} - {view === RUXViews.PHONE_NUMBER && ( - - )} - - {view === RUXViews.PHONE_NUMBER && ( - - )} - {view === RUXViews.OTP && } {view === RUXViews.LIST && } diff --git a/src/ReturnUserExperience/RuxOtp.tsx b/src/ReturnUserExperience/RuxOtp.tsx index 391858583c..19b32cf6a7 100644 --- a/src/ReturnUserExperience/RuxOtp.tsx +++ b/src/ReturnUserExperience/RuxOtp.tsx @@ -5,18 +5,31 @@ 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('') -export const RuxPhoneNumber = () => { return ( <> - {/* OTP Style Input */} - OTP Style Input - - - Resend code in (10 seconds) + + + {__('Verify your phone number')} + + + {__('Enter the code sent to ••• ••• 1234.')} + + + + + + + ) } -export default RuxPhoneNumber +export default RuxOtp