Skip to content

Commit

Permalink
Merge pull request #11 from pakenfit/10-features-clear-programmatically
Browse files Browse the repository at this point in the history
feat: clear all pins programmatically
  • Loading branch information
pakenfit authored Jun 15, 2024
2 parents 282d9ef + 69603ec commit aa84bf4
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 118 deletions.
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Phone Pin Input for React Native.

<p align='center'>
<img src='./assets/demo.gif' width="300">
<img src='./assets/demo2.gif' width="300">
</p>


Expand Down Expand Up @@ -45,11 +46,19 @@ For `expo-clipboard` to work you need to follow [these additional steps to insta
## Usage

```js
import { PinInput } from '@pakenfit/react-native-pin-input';

// ...

<PinInput length={6} onFillEnded={otp => console.log(otp)}/>
import { Button, StyleSheet, View } from 'react-native';
import { PinInput, PinInputRef } from '@pakenfit/react-native-pin-input';

export default function App() {
const ref = React.useRef<PinInputRef>(null);

return (
<View style={styles.container}>
<PinInput onFillEnded={(otp) => console.log(otp)} autoFocus ref={ref} />
<Button title="Clear all" onPress={() => ref.current?.clear()} />
</View>
);
}
```
---
## Props
Expand Down Expand Up @@ -90,6 +99,16 @@ The style applied to the `View` container.
### `autoFocus`
Should autoFocus the first `input` element.
## API
The PinInput component provides the following methods through the PinInputRef:
- `clear()`: clear all the pin inputs
## Types
### PinInputRef
The `PinInputRef` type represents the reference to the PinInput component, allowing access to its methods. It has the only method: `clear()` see above.
## Contributing
Expand Down
Binary file added assets/demo2.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 6 additions & 3 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import * as React from 'react';

import { StyleSheet, View } from 'react-native';
import { PinInput } from '@pakenfit/react-native-pin-input';
import { Button, StyleSheet, View } from 'react-native';
import { PinInput, PinInputRef } from '@pakenfit/react-native-pin-input';

export default function App() {
const ref = React.useRef<PinInputRef>(null);

return (
<View style={styles.container}>
<PinInput onFillEnded={(otp) => console.log(otp)} autoFocus />
<PinInput onFillEnded={(otp) => console.log(otp)} autoFocus ref={ref} />
<Button title="Clear all" onPress={() => ref.current?.clear()} />
</View>
);
}
Expand Down
245 changes: 136 additions & 109 deletions src/components/PinInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { useCallback, useRef, useState } from 'react';
import React, {
forwardRef,
useCallback,
useImperativeHandle,
useRef,
useState,
} from 'react';
import { StyleSheet } from 'react-native';
import {
View,
Expand All @@ -13,6 +19,10 @@ import { Input } from './Input';
import * as Clipboard from 'expo-clipboard';
import { IS_IOS } from '../constants';

export type PinInputRef = {
clear: () => void;
};

type PinInputProps = {
inputProps?: Omit<
TextInputProps,
Expand All @@ -30,125 +40,142 @@ type PinInputProps = {
onFillEnded?: (otp: string) => void;
autoFocus?: boolean;
};
export const PinInput = ({
length = 4,
inputProps,
inputStyle,
containerProps,
containerStyle,
onFillEnded,
autoFocus = true,
}: PinInputProps) => {
const pins = Array.from({ length }).map((_, i) => i);
const inputRefs = useRef<TextInput[]>([]);
const pinsValues = useRef<string[]>([]);
const iosOTP = useRef<{
key: string;
index: number | null;
}>({ key: '', index: null });

const [keyPressed, setKeyPressed] = useState<boolean>(false);

const handleOTP = useCallback(
(otp: string): boolean => {
const regexp = new RegExp(`[0-9]{${length}}`);
const otps = otp.match(regexp);
if (otps?.length) {
const otpSplits = otp.split('');
otpSplits.forEach(
(otpSplit, i) =>
inputRefs?.current[i]?.setNativeProps({ text: otpSplit })
);
onFillEnded?.(otp);
iosOTP.current = { key: '', index: null };
Keyboard.dismiss();
return true;
}
return false;
export const PinInput = forwardRef<PinInputRef, PinInputProps>(
(
{
length = 4,
inputProps,
inputStyle,
containerProps,
containerStyle,
onFillEnded,
autoFocus = true,
},
[length, onFillEnded]
);
ref
) => {
const pins = Array.from({ length }).map((_, i) => i);
const inputRefs = useRef<TextInput[]>([]);
const pinsValues = useRef<string[]>([]);
const iosOTP = useRef<{
key: string;
index: number | null;
}>({ key: '', index: null });

const [keyPressed, setKeyPressed] = useState<boolean>(false);

const handleChangeText = useCallback(
async (text: string, index: number) => {
const copiedText = await Clipboard.getStringAsync();
if (copiedText.includes(text) && !keyPressed) {
const otpHandled = handleOTP(copiedText);
if (otpHandled) {
return;
const handleOTP = useCallback(
(otp: string): boolean => {
const regexp = new RegExp(`[0-9]{${length}}`);
const otps = otp.match(regexp);
if (otps?.length) {
const otpSplits = otp.split('');
otpSplits.forEach(
(otpSplit, i) =>
inputRefs?.current[i]?.setNativeProps({ text: otpSplit })
);
onFillEnded?.(otp);
iosOTP.current = { key: '', index: null };
Keyboard.dismiss();
return true;
}
}
pinsValues.current[index] = text;
if (index + 1 <= pins.length - 1) {
inputRefs?.current[index + 1]?.focus();
} else {
onFillEnded?.(pinsValues.current.join(''));
setKeyPressed(false);
Keyboard.dismiss();
}
},
[handleOTP, keyPressed, onFillEnded, pins.length]
);
return false;
},
[length, onFillEnded]
);

const onKeyPress = useCallback(
(
event: NativeSyntheticEvent<TextInputKeyPressEventData>,
index: number
) => {
event.persist();
setKeyPressed(true);
if (IS_IOS && Number.isInteger(Number(event.nativeEvent.key))) {
if (iosOTP.current.index === null) {
iosOTP.current = { key: event.nativeEvent.key, index };
const handleChangeText = useCallback(
async (text: string, index: number) => {
const copiedText = await Clipboard.getStringAsync();
if (copiedText.includes(text) && !keyPressed) {
const otpHandled = handleOTP(copiedText);
if (otpHandled) {
return;
}
}
pinsValues.current[index] = text;
if (index + 1 <= pins.length - 1) {
inputRefs?.current[index + 1]?.focus();
} else {
if (iosOTP.current.index === index) {
iosOTP.current = {
key: `${iosOTP.current.key}${event.nativeEvent.key}`,
index,
};
onFillEnded?.(pinsValues.current.join(''));
setKeyPressed(false);
Keyboard.dismiss();
}
},
[handleOTP, keyPressed, onFillEnded, pins.length]
);

const onKeyPress = useCallback(
(
event: NativeSyntheticEvent<TextInputKeyPressEventData>,
index: number
) => {
event.persist();
setKeyPressed(true);
if (IS_IOS && Number.isInteger(Number(event.nativeEvent.key))) {
if (iosOTP.current.index === null) {
iosOTP.current = { key: event.nativeEvent.key, index };
} else {
iosOTP.current = { key: '', index: null };
if (iosOTP.current.index === index) {
iosOTP.current = {
key: `${iosOTP.current.key}${event.nativeEvent.key}`,
index,
};
} else {
iosOTP.current = { key: '', index: null };
}
}
if (iosOTP.current.key.length === length) {
handleOTP(iosOTP.current.key);
return;
}
}
if (iosOTP.current.key.length === length) {
handleOTP(iosOTP.current.key);
return;
}
}

if (event.nativeEvent.key === 'Backspace') {
onFillEnded?.('');
setKeyPressed(false);
iosOTP.current = { key: '', index: null };
if (index - 1 >= 0) {
inputRefs?.current[index - 1]?.focus();
if (event.nativeEvent.key === 'Backspace') {
onFillEnded?.('');
setKeyPressed(false);
iosOTP.current = { key: '', index: null };
if (index - 1 >= 0) {
inputRefs?.current[index - 1]?.focus();
}
}
}
},
[handleOTP, length, onFillEnded]
);
},
[handleOTP, length, onFillEnded]
);

return (
<View style={[styles.container, containerStyle]} {...containerProps}>
{pins.map((pin) => {
return (
<Input
{...inputProps}
autoFocus={autoFocus && pin === 0}
ref={(input) => inputRefs?.current.push(input as TextInput)}
key={pin}
style={inputStyle}
onChangeText={(text) => handleChangeText(text, pin)}
onKeyPress={(event) => onKeyPress(event, pin)}
autoComplete="sms-otp"
textContentType="oneTimeCode"
keyboardType="numeric"
/>
);
})}
</View>
);
};
const clear = useCallback(() => {
pinsValues.current = [];
inputRefs.current.forEach((input) => {
input?.setNativeProps({ text: '', placeholder: '0' });
});
inputRefs.current[0]?.focus();
}, []);

useImperativeHandle(ref, () => ({ clear }), [clear]);

return (
<View style={[styles.container, containerStyle]} {...containerProps}>
{pins.map((pin) => {
return (
<Input
{...inputProps}
autoFocus={autoFocus && pin === 0}
ref={(input) => inputRefs?.current.push(input as TextInput)}
key={pin}
style={inputStyle}
onChangeText={(text) => handleChangeText(text, pin)}
onKeyPress={(event) => onKeyPress(event, pin)}
autoComplete="sms-otp"
textContentType="oneTimeCode"
keyboardType="numeric"
/>
);
})}
</View>
);
}
);

PinInput.displayName = 'PinInput';

const styles = StyleSheet.create({
container: {
Expand Down
3 changes: 2 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PinInput } from './components/PinInput';
import { PinInputRef } from './components/PinInput';

export { PinInput };
export { PinInput, PinInputRef };

0 comments on commit aa84bf4

Please sign in to comment.