1.2.15 • Published 8 months ago

fedapay-reactnative-pin v1.2.15

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

React Native Pincode

This component is inspired by https://github.com/jarden-digital/react-native-pincode. The layout looks similar but I rewrite in typescript, simpler, more organized and just enough options. I also add the Reset PIN code feature. I rewrite for personal usage, so the business logic is very limited. If you find this useful, you can suggest improvements. PRs are welcome.

The options look intimidating but don't worry. Almost all of them are optional.

NOTE: The component doesn't block the app for you. It just renders the Enter PIN screen, and you should style the component to cover the whole screen with absolute position, for instance. The best way is to place the PinCode component in your App component and use the state management tool to switch the visibility. Check the example below.

Basic usage

import { PinCode, PinCodeT } from '@anhnch/react-native-pincode';
const Screen = () => {
  return <View>
    <PinCode mode={PinCodeT.Modes.Enter} visible={true}
      styles={{
        main: { position: 'absolute', left: 0, right; 0, top: 0, bottom: 0, zIndex: 99 }
      }}
    />
  </View>
}

Full options usage

//...
import { PinCode, PinCodeT, hasSetPIN, clearPIN } from '@anhnch/react-native-pincode';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';

const customTexts = {
    enter: {
        title: 'Custom enter PIN title',
        subTitle: 'custom enter PIN sub title',
        error: 'custom enter PIN error',
        backSpace: 'Del',
        footerText: 'Forgot PIN?'
    },
    set: {
        title: 'Custom set PIN title',
        subTitle: 'Custom set PIN sub title',
        repeat: 'Custom enter PIN again',
        error: 'Custom repeat PIN error',
        cancelText: 'Cancel',
    },
    locked: {
        title: 'Custom locked title',
        subTitle: `You have entered wrong PIN {{maxAttempt}} times. The app is locked in {{lockDuration}}.`,
        lockedText: 'Locked',
    },
    reset: {
        title: 'Custom reset PIN title',
        subTitle: `Custom reset PIN sub title`,
        confirm: 'Custom confirm message'
    }
};

const customStyles = {
  main: { position: 'absolute', left: 0, right; 0, top: 0, bottom: 0, zIndex: 99 },
  enter: {
    titleContainer: { borderWidth: 1 },
    title: { color: 'yellow' },
    subTitle: { color: 'red' },
    buttonContainer: { borderWidth: 1 },
    buttonText: { color: 'blue' },
    buttons: { backgroundColor: 'green' },
    footer: { borderWidth: 1 },
    footerText: { color: 'purple' },
    pinContainer: { borderWidth: 1 }
  },
  locked: {
    titleContainer: { borderWidth: 1 },
    title: { color: 'yellow' },
    subTitle: { color: 'red' },
    clockContainer: { borderWidth: 1 },
    clockText: { color: 'red' },
    locked: { color: 'yellow' }
  },
  reset: {
    titleContainer: { borderWidth: 1 },
    title: { color: 'yellow' },
    subTitle: { color: 'red' },
    buttons: { backgroundColor: 'green' }
  }
}

const App = () => {
  const [visible, setVisible] = useState(true);
  const [mode, setMode] = useState<PinCodeT.Modes>(PinCodeT.Modes.Enter);

  useEffect(() => {
    hasSetPIN().then(hasPin => setVisible(hasPin));
  }, [])

  return <View>
    <Button onPress={() => setMode(PinCodeT.Modes.Set)} title="Set new PIN" />
    <Button onPress={() => setMode(PinCodeT.Modes.Enter)} title="Enter PIN" />
    <Button onPress={() => clearPIN().then(() =>
        console.log('PIN is cleared')
      )} title="Remove PIN" />

    <PinCode mode={mode} visible={visible}
      onSetCancel={() => setVisible(false)}
      onSetSuccess={(newPin: string) => console.log('A new PIN has been set: ' + newPin)}
      onEnterSuccess={(pin: string) => console.log('User has entered PIN: ' + pin)}
      onResetSuccess={() => console.log('Do clean up app data when PIN is reset')}
      onModeChanged={(lastMode: PinCodeT.Modes, newMode: PinCodeT.Modes) => {
        console.log(`Mode has been changed, from ${lastMode} to ${newMode}`)
      }}
      options={{
        pinLength: 6,
        maxAttempt: 4,
        lockDuration: 10000,
        allowedReset: true,
        disableLock: false,
        backSpace: <Icon name='backspace' size={40} color='white' />,
        lockIcon: <Icon name='lock' size={24} color='white' />
      }}
      textOptions={customTexts}
      styles={customStyles} />
  </View>
}

Properties

NameDescriptionRequiredDefault
visibleShow or not show the componenttruefalse
modeThe component has 4 modes:enter: user has to enter the PIN to accessset: set up new PINlocked: lock the user from accessing and count down.reset: allow user to remove PIN.trueenter
onEnterSuccesscallback when the mode is 'enter' and the user has entered PIN successfully.Parameters:pin (string, optional): the entered PINfalse
onSetSuccesscallback when the mode is 'set' and the user has set a new PIN successfully.Parameters:pin (string, optional): the set PINfalse
onSetCancelcallback when the mode is 'set' and the user has canceled the setting.false
onResetSuccesscallback when the mode is 'reset' and the PIN is cleared successfully.false
onModeChangedcallback when the mode is changed by the component. <PinCode onModeChange={(lastMode: PinCodeT.Modes, newMode: PinCodeT.Modes) => console.log(lastMode, newMode)}/>false
checkPinA custom function to check PIN, in case you want to use a different way to store and check the pin. Check the example belowfalse
optionsSpecify how the component works. Check the options belowfalse
textOptionsAllow customizing the texts in the component. Check the options belowfalse
stylesAllow customizing the layout of the screens. Check the style options belowfalse

Options

NameDescriptionRequiredDefault
pinLengthNumber of digitsfalse4
disableLockBy default, the locked screen is shown when maxAttempt has reached. Set this to true to disable the locked modefalsefalse
maxAttemptThe number of attempts when entering PIN. When user enters wrong PIN for a number of times, the Locked screen is shownfalse10
lockDurationThe time that the Locked screen is shown in milisecondsfalse60000
allowResetIf allowReset is set to true, the "Forgot PIN?" button is displayed at the bottom of the Enter screenfalsetrue
backSpaceOn Enter/Set screen the "Delete" button is used to delete the entered digit. But you can pass an <Icon name='backspace' size={24} /> to display an icon instead. This is to remove the react-native-vector-icon dependency from the package.false
lockedIconOn Locked screen the "Locked" text is shown above the clock. But you can pass an <Icon name='lock' size={24} /> to display an icon instead. This is to remove the react-native-vector-icon dependency from the package.false

Text Options

The text options are grouped by screen for the ease to find. You can pass the textOptions in this syntax

<Pincode mode='enter'
  textOptions={{
    enter: {
      title: 'custom enter title',
      subTitle: 'custom sub title',
      error: 'wrong PIN',
      backSpace: 'del',
      footerText: 'Forgot PIN?'
    },
    set: {
      title: 'custom set title',
      subTitle: 'custom sub title',
      repeat: 'Enter PIN again',
      error: `repeated PIN doesn't match`,
      cancelText: 'Cancel'
    },
    locked: {
      title: 'custom locked title',
      subTitle: 'custom locked sub title',
      lockedText: 'locked'
    },
    reset: {
      title: 'custom reset title',
      subTitle: 'custom reset sub title',
      resetButton: 'Remove PIN',
      confirm: 'custom confirm text',
      confirmButton: 'Remove',
      backButton: 'Go Back'
    }
  }}
/>

Enter screen text options

NameDescriptionRequiredDefaultType
titlethe Enter screen titlefalseEnter PINstring
subTitlethe Enter screen sub titlefalseEnter 4-digit PIN to access.string
errorerror message when user enter wrong PINfalseWrong PIN! Try again.string
backSpacethe text of the backspace buttonfalseDeletestring
footerTextthe text of the footer buttonfalseForgot PIN?string

Set screen text options

NameDescriptionRequiredDefaultType
titlethe Set screen titlefalseSet up a new PINstring
subTitlethe Set screen sub titlefalseEnter 4 digits.string
repeatthe text to ask to enter new PIN againfalseEnter new PIN again.string
errorthe error message when repeated PIN doesn't matchfalsePIN don't match. Start the process again.string
cancelthe cancel buttonfalseCancelstring

Locked screen text options

NameDescriptionRequiredDefaultType
titlethe Locked screen titlefalseLockedstring
subTitlethe Locked screen sub titlefalseYour have entered wrong PIN many times.The app is temporarily locked.string
lockedTextthe locked text (this can be replaced with icon) by using the lockedIcon optionsfalseLockedstring

Reset screen text options

NameDescriptionRequiredDefaultType
titleThe Reset screen titlefalseForgot PIN?string
subTitleThe Reset screen sub title. You can use the {{maxAttempt}} and {{lockDuration}} placeholders to display the maxAttempt and lockDuration (in minutes) in the sub title.falseRemove the PIN may wipe out the app data and settings.string
resetButtonThe reset button textfalseResetstring
confirmAsk user to confirm removeing PINfalseAre you sure you want remove the PIN?string
confirmButtonThe confirm button textfalseConfirmstring
backButtonThe back button textfalseBackstring

Styles options

The style is organized like textOptions for the ease of finding. Note that

Enter screen styles

NameDescriptionRequiredDefault
titleContainerthe Enter screen title container ViewStylefalse
titleTextStyle of the titlefalse
subTitleTextStyle of the sub titlefalse
pinContainerViewStyle which wraps the PIN dotsfalse
buttonContainerViewStyle which wraps digit buttonsfalse
buttonsViewStyle which wraps digit buttonsfalse
buttonTextTextStyle of each buttonfalse
footerViewStyle the footerfalse
footerTextTextStyle the footerfalse

Locked screen styles

NameDescriptionRequiredType
titleContainerthe Enter screen title container ViewStylefalseViewStyle
titleStyle of the titlefalseTextStyle
subTitleStyle of the sub titlefalseTextStyle
clockContainerStyle of the clock componentfalseViewStyle
clockTextStyle of the count down textfalseTextStyle
lockedStyle of the locked Text. If you use custom icon for this, you can set the icon style.falseTextStyle

Reset screen styles

NameDescriptionRequiredType
titleContainerStyle of title containerfalseViewStyle
titleStyle of the titlefalseTextStyle
subTitleStyle of the sub titlefalseTextStyle
buttonsStyle of the buttons in Reset screen, including Reset and Confirm butonsfalseTextStyle

Utilities

NameDescriptionReturn
hasSetPINcheck if user has set a PINPromise
clearPINclear the PINPromise

Storage

To make it simple and least dependencies, I use the SecureStore to save pin. But you can use the callbacks to implement your own way.

...
  <PinCode mode={mode} visible={visible}
    onSetSuccess={(newPin: string) => {
        Keychain.setGenericPassword('pin', newPin);
    })}
    onResetSuccess={() => {
        Keychain.resetGenericPassword();
    }}
    checkPin={async (pin: string) => {
        const credential = await getGenericPassword();
        return (credential && credential.password===pin);
    }}
  />
...

Example

Here is an example how to use Recoil to manage pinState to toggle PinCode visibility and mode. I also use react-native-keychain to store pin.

import React, { useEffect } from 'react';
import * as Keychain from 'react-native-keychain';
import { View, StyleSheet, AppState, AppStateStatus, Text, Button } from 'react-native';
import { RecoilRoot, useRecoilState, atom } from 'recoil';
import { NavigationContainer, } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { PinCode, PinCodeT, clearPIN, hasSetPIN } from '@anhnch/react-native-pincode';

const MainStack = createStackNavigator();

const PinState = atom({
    key: 'common.pinState',
    default: {
        mode: PinCodeT.Modes.Enter,
        show: false,
        hasPin: false
    }
})

const HomeScreen = () => {
    const [pinState, setPinState] = useRecoilState(PinState);

    return <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <Text style={{ fontSize: 30, marginBottom: 30, fontWeight: 'bold' }}>React Native Pin Code</Text>
        {pinState.hasPin && <Button title='Show PinCode Enter'
            onPress={() => setPinState({ ...pinState, mode: PinCodeT.Modes.Enter, show: true })} />}
        <Button title='Set a new PIN'
            onPress={() => setPinState({ ...pinState, mode: PinCodeT.Modes.Set, show: true })} />
        {pinState.hasPin && <Button title='Remove PIN' onPress={() => {
            clearPIN();
            setPinState({ ...pinState, hasPin: false });
        }} />}
    </View>
}

const App = () => {
    const [pinState, setPinState] = useRecoilState(PinState);

    /** Show the Pin Enter screen on app load if user has set a PIN */
    useEffect(() => {
        Keychain.getGenericPassword().then(credential => {
            setPinState({ ...pinState, hasPin: credential ? true : false, show: credential ? true : false });
        });
    }, [])

    useEffect(() => {
        AppState.addEventListener("change", appStateChanged);
        return () => AppState.removeEventListener("change", appStateChanged);
    })

    /** You may want to protect the content when the app goes to inactivity or background */
    function appStateChanged(nextAppState: AppStateStatus) {
        if ((nextAppState == 'inactive' || nextAppState == 'background')
            && pinState.hasPin
            && pinState.mode != PinCodeT.Modes.Locked) {
            setPinState({ ...pinState, show: true, mode: PinCodeT.Modes.Enter });
        }
    }

    return <>
        <NavigationContainer fallback={<Text>Loading</Text>}>
            <MainStack.Navigator initialRouteName="Home">
                <MainStack.Screen name="Home" component={HomeScreen} />
            </MainStack.Navigator>
        </NavigationContainer>

        <PinCode mode={pinState.mode} visible={pinState.show}
            onSetCancel={() => setPinState({ ...pinState, show: false })}
            onSetSuccess={async (pin: string) => {
                await Keychain.setGenericPassword('pin', pin);
                setPinState({ show: false, mode: PinCodeT.Modes.Enter, hasPin: true });
            }}
            onEnterSuccess={() => setPinState({ ...pinState, mode: PinCodeT.Modes.Enter, show: false })}
            onResetSuccess={async () => {
                /** @todo implement your business logic before removing pin */
                // ...
                await Keychain.resetGenericPassword();
                setPinState({ mode: PinCodeT.Modes.Enter, show: false, hasPin: false });
            }}
            checkPin={async (pin: string) => {
                const credential = await Keychain.getGenericPassword();
                return (credential && credential.password == pin);
            }}
            styles={{ main: styles.pincode }} />
    </>;
}

const styles = StyleSheet.create({
    pincode: { position: 'absolute', top: 0, right: 0, left: 0, bottom: 0, zIndex: 99, backgroundColor: '#006FB3' }
})

export default () => <RecoilRoot><App /></RecoilRoot>;
1.2.15

8 months ago

1.2.13

2 years ago

1.2.14

2 years ago

1.2.12

2 years ago

1.2.11

2 years ago

1.2.10

2 years ago

1.2.9

2 years ago

1.2.8

2 years ago

1.2.7

2 years ago

1.2.6

2 years ago

1.2.5

2 years ago