Skip to content

Commit

Permalink
feature: Enable OTP auto-completion from email (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
skambalin authored Jul 12, 2024
1 parent c7f094c commit 64991a2
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 114 deletions.
66 changes: 10 additions & 56 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"@cere-wallet/communication": "0.0.0",
"@cere-wallet/ui": "0.0.0",
"@cere-wallet/wallet-engine": "0.0.0",
"@cere/react-code-input": "3.11.1",
"@fontsource/lexend": "^4.5.13",
"@hookform/resolvers": "^2.9.9",
"@polkadot/api": "^10.2.1",
Expand All @@ -26,6 +25,7 @@
"axios": "^1.1.2",
"ethers": "^5.6.9",
"firebase": "^9.12.1",
"input-otp": "^1.2.4",
"jwt-decode": "^3.1.2",
"mobx": "^6.6.2",
"mobx-react-lite": "^3.4.0",
Expand Down
112 changes: 60 additions & 52 deletions packages/ui/src/components/OtpInput/OtpInput.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,78 @@
import ReactCodeInput from '@cere/react-code-input';
import { styled } from '@cere-wallet/ui';
import { Typography, Stack } from '@mui/material';
import { OTPInput, SlotProps } from 'input-otp';
import { styled, Typography, Stack, Box } from '@cere-wallet/ui';
import { forwardRef } from 'react';

const DIGITS_NUMBER = 6;

interface OtpProps {
errorMessage?: string;
onChange?: (code: string) => void;
}

const CodeInput = styled(ReactCodeInput)(({ theme }) => ({
type SlotInputProps = {
active?: boolean;
error?: boolean;
};

const SlotInput = styled(Typography, {
shouldForwardProp: (prop) => !['active', 'error'].includes(prop as string),
})<SlotInputProps>(({ theme, active, error }) => ({
width: 44,
height: 54,
lineHeight: '52px',
borderRadius: 16,
borderWidth: 1,
borderStyle: 'solid',
textAlign: 'center',
input: {
height: '56px',
width: '44px',
borderRadius: '16px',
border: `1px solid #E7E8EB`,
fontSize: '16px',
// @ts-ignore
textAlign: '-webkit-center',
outline: 'none',
margin: '0 2px',
padding: '0',
backgroundColor: theme.isGame ? 'transparent' : '#fff',
color: theme.isGame ? theme.palette.primary.light : theme.palette.text.primary,
'& :first-of-type': {
marginLeft: '0 auto !important',
},
'& :last-of-type': {
marginRight: '0 auto !important',
},
'&:focus': {
border: `2px solid ${theme.isGame ? theme.palette.primary.light : theme.palette.primary.main} !important`,
},
padding: '1px',
borderColor: error ? theme.palette.error.main : theme.palette.divider,
backgroundColor: theme.isGame ? 'transparent' : theme.palette.background.default,
color: theme.isGame ? theme.palette.primary.light : theme.palette.text.primary,

'@media (min-width: 376px)': {
'&:nth-of-type(3)': {
marginRight: '26px !important',
},
},
},
...(active && {
padding: 0,
borderWidth: 2,
borderColor: theme.isGame ? theme.palette.primary.light : theme.palette.primary.main,
}),
}));

const Slot = ({ char, isActive, error }: SlotProps & SlotInputProps) => (
<SlotInput as="div" active={isActive} error={error}>
{char}
</SlotInput>
);

export const OtpInput = forwardRef<null, OtpProps>(({ onChange, errorMessage }, ref) => {
const handleCodeChange = (value: string) => {
if (typeof onChange === 'function') {
onChange(value.toLowerCase());
}
};
const hasError = !!errorMessage;

return (
<Stack direction="column" textAlign="center" spacing={1}>
<CodeInput
<Box display="flex" flexDirection="column" alignItems="center">
<OTPInput
ref={ref}
name="OTP"
type={'text'}
inputMode="url"
fields={DIGITS_NUMBER}
onChange={handleCodeChange}
inputStyleInvalid={{ border: '1px solid red' }}
isValid={!errorMessage}
autoFocus
maxLength={6}
onChange={onChange}
aria-label="OTP input"
render={({ slots }) => (
<Stack direction="row" spacing={3}>
<Stack direction="row" spacing={1}>
{slots.slice(0, 3).map((slot, index) => (
<Slot key={index} error={hasError} {...slot} />
))}
</Stack>

<Stack direction="row" spacing={1}>
{slots.slice(3).map((slot, index) => (
<Slot key={index} error={hasError} {...slot} />
))}
</Stack>
</Stack>
)}
/>
<Typography variant="body2" color="error.main">
{errorMessage}
</Typography>
</Stack>

{hasError && (
<Typography marginTop={1} variant="body2" color="error.main">
{errorMessage}
</Typography>
)}
</Box>
);
});
10 changes: 5 additions & 5 deletions tests/wdio/objects/WalletAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export class WalletAuth {
return browser.findByRole$('button', { name: 'Verify' });
}

get otpInput() {
return browser.findByRole$('textbox', { name: 'OTP input' });
}

async enterRandomEmail() {
const emailSuffix = Math.random().toString(32).slice(2);
const email = `auto+${emailSuffix}@test.io`;
Expand All @@ -25,10 +29,6 @@ export class WalletAuth {
}

async enterOTP(otp: string) {
await $('.react-code-input').waitForDisplayed();

const inputs = await browser.$$('.react-code-input input');

return Promise.all(inputs.map((input, index) => input.setValue(otp[index])));
await this.otpInput.setValue(otp);
}
}

0 comments on commit 64991a2

Please sign in to comment.