Skip to content

feat: add onEnterValidation prop for EnterLayout component, to enable customization of validation check on enter #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
223 changes: 145 additions & 78 deletions src/EnterLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,163 @@
import React, { useState } from 'react';
import React, { useState } from "react";
import { Platform, Pressable, Text, Vibration, View } from "react-native";
import { DEFAULT } from "./common";
import NumbersPanel from "./components/NumbersPanel";
import Pin from "./components/Pin";
import { PinCodeT } from "./types";
import { DEFAULT } from './common';
import NumbersPanel from './components/NumbersPanel';
import Pin from './components/Pin';

const EnterLayout = ({ pin, styles, mode, textOptions, options, onSwitchMode, onEnter, onReset, onMaxAttempt }: {
pin: string | undefined;
styles?: PinCodeT.EnterStyles;
mode: PinCodeT.Modes;
textOptions: PinCodeT.TextOptions;
options?: PinCodeT.Options;
onSwitchMode?: () => void;
onEnter: (newPin: string) => void;
onMaxAttempt: () => void;
onReset: () => void;

const EnterLayout = ({
pin,
styles,
mode,
textOptions,
options,
onSwitchMode,
onEnter,
onReset,
onMaxAttempt,
onEnterValidation,
}: {
pin: string | undefined;
styles?: PinCodeT.EnterStyles;
mode: PinCodeT.Modes;
textOptions: PinCodeT.TextOptions;
options?: PinCodeT.Options;
onSwitchMode?: () => void;
onEnter: (newPin: string) => void;
onMaxAttempt: () => void;
onReset: () => void;
onEnterValidation?: (enteredPin: string) => Promise<boolean>;
}) => {
const [curPin, setCurPin] = useState('');
const [disabled, disableButtons] = useState(false);
const [failureCount, setFailureCount] = useState(0);
const [showError, setShowError] = useState(false);
const [curPin, setCurPin] = useState("");
const [disabled, setDisabled] = useState(false);
const [failureCount, setFailureCount] = useState(0);
const [showError, setShowError] = useState(false);

async function onNumberPress(value: string) {
const newPin = (value == 'delete') ?
(curPin.substring(0, curPin.length - 1)) :
(curPin + value);
async function onNumberPress(value: string) {
const newPin =
value == "delete"
? curPin.substring(0, curPin.length - 1)
: curPin + value;

setCurPin(newPin);
setCurPin(newPin);

if (newPin.length == options?.pinLength) {
await processEnterPin(newPin)
}
if (newPin.length == options?.pinLength) {
await processEnterPin(newPin);
}
}

async function processEnterPin(enteredPin: string) {
disableButtons(true);

if (pin === enteredPin) {
setFailureCount(0);
disableButtons(false);
onEnter(enteredPin);
return;
}
async function processEnterPin(enteredPin: string) {
setDisabled(true);

if (!options?.disableLock && failureCount >= (options?.maxAttempt || DEFAULT.Options.maxAttempt || 5) - 1) {
disableButtons(false);
onMaxAttempt();
return;
}
let isPinValid: boolean = false;

setCurPin('');
setFailureCount(failureCount + 1);
if (onEnterValidation) {
isPinValid = await onEnterValidation(enteredPin);
if (isPinValid) {
resetPinSetupToDefault(enteredPin);
return;
}
}

if (Platform.OS === 'ios') {
Vibration.vibrate(); // android requires VIBRATE permission
}
if (pin === enteredPin && !onEnterValidation) {
resetPinSetupToDefault(enteredPin);
return;
}

setShowError(true);
setTimeout(() => setShowError(false), options?.retryLockDuration || DEFAULT.Options.retryLockDuration);
setTimeout(() => disableButtons(false), options?.retryLockDuration || DEFAULT.Options.retryLockDuration);
if (
!options?.disableLock &&
failureCount >=
(options?.maxAttempt || DEFAULT.Options.maxAttempt || 5) - 1
) {
setDisabled(false);
onMaxAttempt();
return;
}

return <>
<View style={[DEFAULT.Styles.enter?.header, styles?.header]}>
<Text style={[DEFAULT.Styles.enter?.title, styles?.title]}>{textOptions.enter?.title || DEFAULT.TextOptions.enter?.title}</Text>
setCurPin("");
setFailureCount(failureCount + 1);

<Text style={[DEFAULT.Styles.enter?.subTitle, styles?.subTitle]}>
{textOptions.enter?.subTitle?.replace('{{pinLength}}', (options?.pinLength || DEFAULT.Options.pinLength || 4).toString())}
if (Platform.OS === "ios") {
Vibration.vibrate(); // android requires VIBRATE permission
}

setShowError(true);
setTimeout(
() => setShowError(false),
options?.retryLockDuration ?? DEFAULT.Options.retryLockDuration
);
setTimeout(
() => setDisabled(false),
options?.retryLockDuration ?? DEFAULT.Options.retryLockDuration
);
}

const resetPinSetupToDefault = (enteredPin: string) => {
setFailureCount(0);
setDisabled(false);
onEnter(enteredPin);
};

return (
<>
<View style={[DEFAULT.Styles.enter?.header, styles?.header]}>
<Text style={[DEFAULT.Styles.enter?.title, styles?.title]}>
{textOptions.enter?.title ?? DEFAULT.TextOptions.enter?.title}
</Text>

<Text style={[DEFAULT.Styles.enter?.subTitle, styles?.subTitle]}>
{textOptions.enter?.subTitle?.replace(
"{{pinLength}}",
(options?.pinLength ?? DEFAULT.Options.pinLength ?? 4).toString()
)}
</Text>
{showError && (
<Text style={[DEFAULT.Styles.enter?.errorText, styles?.errorText]}>
{textOptions.enter?.error ?? DEFAULT.TextOptions.enter?.error}
</Text>
)}
</View>
<View style={[DEFAULT.Styles.enter?.content, styles?.content]}>
<Pin
pin={curPin}
pinLength={options?.pinLength ?? DEFAULT.Options.pinLength ?? 4}
style={styles?.pinContainer}
pinStyle={[DEFAULT.Styles.enter?.pin, styles?.pin]}
enteredPinStyle={[
DEFAULT.Styles.enter?.enteredPin,
styles?.enteredPin,
]}
/>

<NumbersPanel
disabled={disabled}
onButtonPress={onNumberPress}
backSpace={options?.backSpace}
backSpaceText={textOptions.enter?.backSpace}
buttonStyle={styles?.button}
rowStyle={styles?.buttonRow}
style={styles?.buttonContainer}
textStyle={styles?.buttonText}
disabledStyle={styles?.buttonTextDisabled}
/>
</View>
<View style={[DEFAULT.Styles.enter?.footer, styles?.footer]}>
{options?.allowReset && (
<Pressable
onPress={onReset}
style={(state) => ({ opacity: state.pressed ? 0.6 : 1 })}
>
<Text
style={[DEFAULT.Styles.enter?.footerText, styles?.footerText]}
>
{textOptions.enter?.footerText ??
DEFAULT.TextOptions.enter?.footerText}
</Text>
{showError && <Text style={[DEFAULT.Styles.enter?.errorText, styles?.errorText]}>{textOptions.enter?.error || DEFAULT.TextOptions.enter?.error}</Text>}
</View>
<View style={[DEFAULT.Styles.enter?.content, styles?.content]}>
<Pin pin={curPin} pinLength={options?.pinLength || DEFAULT.Options.pinLength || 4}
style={styles?.pinContainer}
pinStyle={[DEFAULT.Styles.enter?.pin, styles?.pin]}
enteredPinStyle={[DEFAULT.Styles.enter?.enteredPin, styles?.enteredPin]} />

<NumbersPanel disabled={disabled} onButtonPress={onNumberPress}
backSpace={options?.backSpace} backSpaceText={textOptions.enter?.backSpace}
buttonStyle={styles?.button} rowStyle={styles?.buttonRow} style={styles?.buttonContainer}
textStyle={styles?.buttonText} disabledStyle={styles?.buttonTextDisabled}
/>
</View>
<View style={[DEFAULT.Styles.enter?.footer, styles?.footer]}>
{options?.allowReset && <Pressable onPress={onReset} style={state => ({ opacity: state.pressed ? 0.6 : 1 })}>
<Text style={[DEFAULT.Styles.enter?.footerText, styles?.footerText]}>{textOptions.enter?.footerText || DEFAULT.TextOptions.enter?.footerText}</Text>
</Pressable>}
</View>
</Pressable>
)}
</View>
</>
}



);
};

export default EnterLayout;
export default EnterLayout;
78 changes: 48 additions & 30 deletions src/PinCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const PinCode = ({
onSet,
onSetCancel,
onReset,
onModeChanged
onModeChanged,
onEnterValidation
}: PinCodeT.PinCodeProps) => {
const [curMode, setCurMode] = useState<PinCodeT.Modes>(mode);
const [curOptions, setCurOptions] = useState<PinCodeT.Options>(DEFAULT.Options);
Expand Down Expand Up @@ -64,35 +65,52 @@ const PinCode = ({

if (!visible) return null;

return <View style={[DEFAULT.Styles.main, styles?.main]}>
{(curMode == PinCodeT.Modes.Enter) &&
<EnterLayout pin={pin} mode={curMode}
options={curOptions} textOptions={curTextOptions}
onEnter={onEnter}
onMaxAttempt={() => switchMode(PinCodeT.Modes.Locked)}
onReset={() => switchMode(PinCodeT.Modes.Reset)}
styles={styles?.enter}
/>
}
{(curMode == PinCodeT.Modes.Set) &&
<SetLayout pin={pin} mode={curMode}
options={curOptions} textOptions={curTextOptions}
onSet={onSet}
onReset={() => switchMode(PinCodeT.Modes.Reset)}
onSetCancel={onSetCancel}
styles={styles?.enter}
/>
}
{(curMode == PinCodeT.Modes.Locked) &&
<LockedLayout options={curOptions} textOptions={curTextOptions.locked} styles={styles?.locked}
onClockFinish={() => switchMode(PinCodeT.Modes.Enter)} />}
{(curMode == PinCodeT.Modes.Reset) &&
<ResetLayout styles={styles?.reset} textOptions={curTextOptions.reset}
options={curOptions}
onReset={onReset}
onCancel={() => switchMode(PinCodeT.Modes.Enter)}
/>}
</View>
return (
<View style={[DEFAULT.Styles.main, styles?.main]}>
{curMode == PinCodeT.Modes.Enter && (
<EnterLayout
pin={pin}
mode={curMode}
options={curOptions}
textOptions={curTextOptions}
onEnter={onEnter}
onMaxAttempt={() => switchMode(PinCodeT.Modes.Locked)}
onReset={() => switchMode(PinCodeT.Modes.Reset)}
styles={styles?.enter}
onEnterValidation={onEnterValidation}
/>
)}
{curMode == PinCodeT.Modes.Set && (
<SetLayout
pin={pin}
mode={curMode}
options={curOptions}
textOptions={curTextOptions}
onSet={onSet}
onReset={() => switchMode(PinCodeT.Modes.Reset)}
onSetCancel={onSetCancel}
styles={styles?.enter}
/>
)}
{curMode == PinCodeT.Modes.Locked && (
<LockedLayout
options={curOptions}
textOptions={curTextOptions.locked}
styles={styles?.locked}
onClockFinish={() => switchMode(PinCodeT.Modes.Enter)}
/>
)}
{curMode == PinCodeT.Modes.Reset && (
<ResetLayout
styles={styles?.reset}
textOptions={curTextOptions.reset}
options={curOptions}
onReset={onReset}
onCancel={() => switchMode(PinCodeT.Modes.Enter)}
/>
)}
</View>
);
}

export default PinCode;
Loading