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
25 changes: 25 additions & 0 deletions docs/docs/guides/12-otp-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# OTPInput

A component for entering one-time passwords (OTP) with multiple input fields.

## Usage

```js
import * as React from 'react';
import { OTPInput } from 'react-native-paper';

const MyComponent = () => {
const [otp, setOtp] = React.useState('');

return (
<OTPInput
length={6}
value={otp}
onChangeText={setOtp}
onComplete={(value) => console.log(value)}
/>
);
};

export default MyComponent;
```
2 changes: 2 additions & 0 deletions example/src/ExampleList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import ListAccordionExampleGroup from './Examples/ListAccordionGroupExample';
import ListItemExample from './Examples/ListItemExample';
import ListSectionExample from './Examples/ListSectionExample';
import MenuExample from './Examples/MenuExample';
import OTPInputExample from './Examples/OTPInputExample';
import ProgressBarExample from './Examples/ProgressBarExample';
import RadioButtonExample from './Examples/RadioButtonExample';
import RadioButtonGroupExample from './Examples/RadioButtonGroupExample';
Expand Down Expand Up @@ -79,6 +80,7 @@ export const mainExamples: Record<
listSection: ListSectionExample,
listItem: ListItemExample,
menu: MenuExample,
otpInput: OTPInputExample,
progressbar: ProgressBarExample,
radio: RadioButtonExample,
radioGroup: RadioButtonGroupExample,
Expand Down
87 changes: 87 additions & 0 deletions example/src/Examples/OTPInputExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as React from 'react';
import { View, StyleSheet } from 'react-native';

import { Text, Button } from 'react-native-paper';

import OTPInput from '../../../src/components/OTPInput/OTPInput';

const OTPInputExample = () => {
const [otp, setOtp] = React.useState('');
const [otp4, setOtp4] = React.useState('');
const [error, setError] = React.useState(false);

const handleComplete = (value: string) => {
if (value !== '123456') {
setError(true);
} else {
setError(false);
}
};

return (
<View style={styles.container}>
<Text variant="titleMedium" style={styles.title}>
6 Digit OTP
</Text>

<OTPInput
length={6}
value={otp}
onChangeText={(val) => {
setOtp(val);
if (error) setError(false);
}}
onComplete={handleComplete}
error={error}
autoFocus
/>

<Text style={styles.helper}>Try entering 123456</Text>

<Text variant="titleMedium" style={styles.title}>
4 Digit OTP
</Text>

<OTPInput
length={4}
value={otp4}
onChangeText={setOtp4}
style={styles.input}
/>

<View style={styles.actions}>
<Button
mode="contained"
onPress={() => {
setOtp('');
setOtp4('');
setError(false);
}}
>
Reset
</Button>
</View>
</View>
);
};
OTPInputExample.title = 'OTP Input';
export default OTPInputExample;

const styles = StyleSheet.create({
container: {
padding: 16,
gap: 16,
},
title: {
marginTop: 16,
},
helper: {
opacity: 0.6,
},
actions: {
marginTop: 24,
},
input: {
justifyContent: 'space-evenly',
},
});
177 changes: 177 additions & 0 deletions src/components/OTPInput/OTPInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import * as React from 'react';
import {
View,
StyleSheet,
NativeSyntheticEvent,
TextInputKeyPressEventData,
ViewStyle,
StyleProp,
} from 'react-native';

import { useInternalTheme } from '../../core/theming';
import type { ThemeProp } from '../../types';
import { forwardRef } from '../../utils/forwardRef';
import TextInput from '../TextInput/TextInput';
type Props = {
length?: number;
value: string;
onChangeText: (text: string) => void;
onComplete?: (otp: string) => void;
disabled?: boolean;
error?: boolean;
autoFocus?: boolean;
style?: StyleProp<ViewStyle>;
theme?: ThemeProp;
testID?: string;
};

const OTPInput = forwardRef<View, Props>(
(
{
length = 6,
value,
onChangeText,
onComplete,
disabled = false,
error = false,
autoFocus = false,
style,
theme: themeOverrides,
testID,
},
ref
) => {
const theme = useInternalTheme(themeOverrides);

type PaperTextInputRef = React.ElementRef<typeof TextInput>;
const inputsRef = React.useRef<Array<PaperTextInputRef | null>>([]);

const values = React.useMemo(() => {
const arr = value.split('').slice(0, length);
while (arr.length < length) arr.push('');
return arr;
}, [value, length]);

const prevValuesRef = React.useRef<string[]>(values);

React.useEffect(() => {
prevValuesRef.current = values;
}, [values]);

const focusInput = (index: number) => {
inputsRef.current[index]?.focus();
};

const updateValue = (arr: string[]) => {
const otp = arr.join('');
onChangeText(otp);

if (otp.length === length && !arr.includes('')) {
onComplete?.(otp);
}
};

const handleChange = (text: string, index: number) => {
const prev = prevValuesRef.current;
let newValues = [...values];

if (text === '') {
if (prev[index] !== '') {
newValues[index] = '';
updateValue(newValues);
return;
}

if (index > 0) {
newValues[index - 1] = '';
updateValue(newValues);
focusInput(index - 1);
return;
}

return;
}

if (text.length > 1) {
const pasted = text.replace(/\s/g, '').slice(0, length).split('');
const filled = Array(length).fill('');

pasted.forEach((char, i) => {
filled[i] = char;
});

updateValue(filled);

const lastIndex = Math.min(pasted.length - 1, length - 1);
focusInput(lastIndex);
return;
}

newValues[index] = text;
updateValue(newValues);

if (index < length - 1) {
focusInput(index + 1);
}
};

const handleKeyPress = (
e: NativeSyntheticEvent<TextInputKeyPressEventData>,
index: number
) => {
if (e.nativeEvent.key === 'Backspace') {
if (values[index] === '' && index > 0) {
focusInput(index - 1);
}
}
};

return (
<View ref={ref} testID={testID} style={[styles.container, style]}>
{Array.from({ length }).map((_, index) => (
<TextInput
key={index}
ref={(input: PaperTextInputRef | null) => {
inputsRef.current[index] = input;
}}
mode="outlined"
dense
keyboardType="number-pad"
maxLength={1}
value={values[index]}
onChangeText={(text) => handleChange(text, index)}
onKeyPress={(e) => handleKeyPress(e, index)}
autoFocus={autoFocus && index === 0}
disabled={disabled}
error={error}
textContentType={index === 0 ? 'oneTimeCode' : 'none'}
autoComplete={index === 0 ? 'sms-otp' : 'off'}
importantForAutofill={index === 0 ? 'yes' : 'no'}
accessibilityLabel={`OTP digit ${index + 1}`}
accessibilityRole="text"
style={styles.input}
contentStyle={[styles.content, { color: theme.colors.onSurface }]}
textAlign="center"
/>
))}
</View>
);
}
);

const styles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'space-between',
},
input: {
width: 48,
paddingHorizontal: 18,
},
content: {
fontSize: 18,
paddingHorizontal: 0,
},
});

export default OTPInput;
Loading