Using Custom Passcode Vaults
Overview
Identity Vault allows you to create a Custom Passcode vault. With a custom passcode vault, your application needs to provide the mechanism used to collect the passcode from the user. The passcode is then used to generate a key that is used to lock and unlock the vault. The passcode is only used to generate a key, it is never stored anywhere by Identity Vault. Since the passcode is not stored, it cannot be stolen.
You can think of it working like this:
- When the vault is created, the user supplies a passcode.
- The passcode is used to generate a key and the passcode is discarded.
- This key becomes part of the current vault configuration so the vault can later be locked.
- When the vault is locked the key is destroyed, resulting in a new key being required to unlock the vault.
- When the user wants to unlock the vault, they supply a passcode.
- The passcode is used to generate a key and the passcode is discarded.
- If the new passcode was the same as the original passcode, the new key matches the old key and unlocks the vault, otherwise it does not.
The important items are: the passcode does not live beyond generating a key and the key does not live beyond locking the vault.
Let's Code
This tutorial builds upon the application created when doing the startup strategies tutorial. If you have the code from when you performed that tutorial, then you are ready to go. If you need the code you can make a copy from our GitHub repository.
Update the Vault Type
_93import {_93 BrowserVault,_93 Vault,_93 VaultType,_93 DeviceSecurityType,_93 IdentityVaultConfig,_93} from '@ionic-enterprise/identity-vault';_93import { createVault } from './vault-factory';_93import { Session } from '../models/Session';_93import { setState } from './session-store';_93_93export type UnlockMode =_93 'BiometricsWithPasscode' |_93 'InMemory' |_93 'SecureStorage' |_93 'CustomPasscode';_93_93const vault: Vault | BrowserVault = createVault();_93_93export const initializeVault = async (): Promise<void> => {_93 try {_93 await vault.initialize({_93 key: 'io.ionic.gettingstartedivreact',_93 type: VaultType.SecureStorage,_93 deviceSecurityType: DeviceSecurityType.None,_93 lockAfterBackgrounded: 2000,_93 });_93 } catch (e: unknown) {_93 await vault.clear();_93 await updateUnlockMode('SecureStorage');_93 }_93_93 vault.onLock(() => {_93 setState({ session: null });_93 });_93};_93_93export const storeSession = async (session: Session): Promise<void> => {_93 vault.setValue('session', session);_93 setState({ session });_93};_93_93export const restoreSession = async (): Promise<Session | null> => {_93 let session: Session | null = null;_93 if (!(await vault.isEmpty())) {_93 session = await vault.getValue<Session>('session');_93 }_93 setState({ session });_93 return session;_93};_93_93export const clearSession = async (): Promise<void> => {_93 await vault.clear();_93 setState({ session: null });_93};_93_93export const sessionIsLocked = async (): Promise<boolean> => {_93 return (_93 vault.config?.type !== VaultType.SecureStorage &&_93 vault.config?.type !== VaultType.InMemory &&_93 !(await vault.isEmpty()) &&_93 (await vault.isLocked())_93 );_93};_93_93export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {_93 const newConfig = { ...(vault.config as IdentityVaultConfig) };_93_93 switch (mode) {_93 case 'BiometricsWithPasscode': {_93 newConfig.type = VaultType.DeviceSecurity;_93 newConfig.deviceSecurityType = DeviceSecurityType.Both;_93 break;_93 }_93 case 'InMemory': {_93 newConfig.type = VaultType.InMemory;_93 newConfig.deviceSecurityType = DeviceSecurityType.None;_93 break;_93 }_93 default: {_93 newConfig.type = VaultType.SecureStorage;_93 newConfig.deviceSecurityType = DeviceSecurityType.None;_93 break;_93 }_93 }_93_93 await vault.updateConfig(newConfig);_93};_93_93export const lockSession = async (): Promise<void> => {_93 await vault.lock();_93 setState({ session: null });_93};
Add CustomPasscode
to the UnlockMode
.
_98import {_98 BrowserVault,_98 Vault,_98 VaultType,_98 DeviceSecurityType,_98 IdentityVaultConfig,_98} from '@ionic-enterprise/identity-vault';_98import { createVault } from './vault-factory';_98import { Session } from '../models/Session';_98import { setState } from './session-store';_98_98export type UnlockMode =_98 'BiometricsWithPasscode' |_98 'InMemory' |_98 'SecureStorage' |_98 'CustomPasscode';_98_98const vault: Vault | BrowserVault = createVault();_98_98export const initializeVault = async (): Promise<void> => {_98 try {_98 await vault.initialize({_98 key: 'io.ionic.gettingstartedivreact',_98 type: VaultType.SecureStorage,_98 deviceSecurityType: DeviceSecurityType.None,_98 lockAfterBackgrounded: 2000,_98 });_98 } catch (e: unknown) {_98 await vault.clear();_98 await updateUnlockMode('SecureStorage');_98 }_98_98 vault.onLock(() => {_98 setState({ session: null });_98 });_98};_98_98export const storeSession = async (session: Session): Promise<void> => {_98 vault.setValue('session', session);_98 setState({ session });_98};_98_98export const restoreSession = async (): Promise<Session | null> => {_98 let session: Session | null = null;_98 if (!(await vault.isEmpty())) {_98 session = await vault.getValue<Session>('session');_98 }_98 setState({ session });_98 return session;_98};_98_98export const clearSession = async (): Promise<void> => {_98 await vault.clear();_98 setState({ session: null });_98};_98_98export const sessionIsLocked = async (): Promise<boolean> => {_98 return (_98 vault.config?.type !== VaultType.SecureStorage &&_98 vault.config?.type !== VaultType.InMemory &&_98 !(await vault.isEmpty()) &&_98 (await vault.isLocked())_98 );_98};_98_98export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {_98 const newConfig = { ...(vault.config as IdentityVaultConfig) };_98_98 switch (mode) {_98 case 'BiometricsWithPasscode': {_98 newConfig.type = VaultType.DeviceSecurity;_98 newConfig.deviceSecurityType = DeviceSecurityType.Both;_98 break;_98 }_98 case 'InMemory': {_98 newConfig.type = VaultType.InMemory;_98 newConfig.deviceSecurityType = DeviceSecurityType.None;_98 break;_98 }_98 case 'CustomPasscode': {_98 newConfig.type = VaultType.CustomPasscode;_98 newConfig.deviceSecurityType = DeviceSecurityType.None;_98 break;_98 }_98 default: {_98 newConfig.type = VaultType.SecureStorage;_98 newConfig.deviceSecurityType = DeviceSecurityType.None;_98 break;_98 }_98 }_98_98 await vault.updateConfig(newConfig);_98};_98_98export const lockSession = async (): Promise<void> => {_98 await vault.lock();_98 setState({ session: null });_98};
Modify the updateUnlockMode
function to include a case for CustomPasscode
.
_108import {_108 BrowserVault,_108 Vault,_108 VaultType,_108 DeviceSecurityType,_108 IdentityVaultConfig,_108} from '@ionic-enterprise/identity-vault';_108import { createVault } from './vault-factory';_108import { Session } from '../models/Session';_108import { setState } from './session-store';_108_108export type UnlockMode =_108 'BiometricsWithPasscode' |_108 'InMemory' |_108 'SecureStorage' |_108 'CustomPasscode';_108_108const vault: Vault | BrowserVault = createVault();_108_108export const initializeVault = async (): Promise<void> => {_108 try {_108 await vault.initialize({_108 key: 'io.ionic.gettingstartedivreact',_108 type: VaultType.SecureStorage,_108 deviceSecurityType: DeviceSecurityType.None,_108 lockAfterBackgrounded: 2000,_108 });_108 } catch (e: unknown) {_108 await vault.clear();_108 await updateUnlockMode('SecureStorage');_108 }_108_108 vault.onLock(() => {_108 setState({ session: null });_108 });_108};_108_108export const addOnPasscodeRequested = (_108 callback: (isPasscodeSetRequest: boolean, onComplete: (code: string) => void) => void,_108) => {_108 vault.onPasscodeRequested(callback);_108};_108_108export const removeOnPasscodeRequested = () => {_108 vault.onPasscodeRequested(() => {});_108};_108_108export const storeSession = async (session: Session): Promise<void> => {_108 vault.setValue('session', session);_108 setState({ session });_108};_108_108export const restoreSession = async (): Promise<Session | null> => {_108 let session: Session | null = null;_108 if (!(await vault.isEmpty())) {_108 session = await vault.getValue<Session>('session');_108 }_108 setState({ session });_108 return session;_108};_108_108export const clearSession = async (): Promise<void> => {_108 await vault.clear();_108 setState({ session: null });_108};_108_108export const sessionIsLocked = async (): Promise<boolean> => {_108 return (_108 vault.config?.type !== VaultType.SecureStorage &&_108 vault.config?.type !== VaultType.InMemory &&_108 !(await vault.isEmpty()) &&_108 (await vault.isLocked())_108 );_108};_108_108export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {_108 const newConfig = { ...(vault.config as IdentityVaultConfig) };_108_108 switch (mode) {_108 case 'BiometricsWithPasscode': {_108 newConfig.type = VaultType.DeviceSecurity;_108 newConfig.deviceSecurityType = DeviceSecurityType.Both;_108 break;_108 }_108 case 'InMemory': {_108 newConfig.type = VaultType.InMemory;_108 newConfig.deviceSecurityType = DeviceSecurityType.None;_108 break;_108 }_108 case 'CustomPasscode': {_108 newConfig.type = VaultType.CustomPasscode;_108 newConfig.deviceSecurityType = DeviceSecurityType.None;_108 break;_108 }_108 default: {_108 newConfig.type = VaultType.SecureStorage;_108 newConfig.deviceSecurityType = DeviceSecurityType.None;_108 break;_108 }_108 }_108_108 await vault.updateConfig(newConfig);_108};_108_108export const lockSession = async (): Promise<void> => {_108 await vault.lock();_108 setState({ session: null });_108};
Create a couple of functions that allow us to hook in to the onPasscodeRequested
event from other parts of the code.
_98import {_98 IonButton,_98 IonContent,_98 IonHeader,_98 IonItem,_98 IonLabel,_98 IonList,_98 IonPage,_98 IonTitle,_98 IonToolbar,_98} from '@ionic/react';_98import { useHistory } from 'react-router';_98import { logout } from '../util/authentication';_98import { useSession } from '../util/session-store';_98import { updateUnlockMode } from '../util/session-vault';_98import './Tab1.css';_98_98const Tab1: React.FC = () => {_98 const history = useHistory();_98 const { session } = useSession();_98_98 const logoutClicked = async () => {_98 await logout();_98 history.replace('/login');_98 };_98_98 return (_98 <IonPage>_98 <IonHeader>_98 <IonToolbar>_98 <IonTitle>Tab 1</IonTitle>_98 </IonToolbar>_98 </IonHeader>_98 <IonContent fullscreen>_98 <IonHeader collapse="condense">_98 <IonToolbar>_98 <IonTitle size="large">Tab 1</IonTitle>_98 </IonToolbar>_98 </IonHeader>_98_98 <IonList>_98 <IonItem>_98 <IonLabel>_98 <IonButton expand="block" color="secondary" onClick={() => updateUnlockMode('BiometricsWithPasscode')}>_98 Use Biometrics_98 </IonButton>_98 </IonLabel>_98 </IonItem>_98_98 <IonItem>_98 <IonLabel>_98 <IonButton expand="block" color="secondary" onClick={() => updateUnlockMode('InMemory')}>_98 Use In Memory_98 </IonButton>_98 </IonLabel>_98 </IonItem>_98_98 <IonItem>_98 <IonLabel>_98 <IonButton expand="block" color="secondary" onClick={() => updateUnlockMode('SecureStorage')}>_98 Use Secure Storage_98 </IonButton>_98 </IonLabel>_98 </IonItem>_98_98 <IonItem>_98 <IonLabel>_98 <IonButton expand="block" color="secondary" onClick={() => updateUnlockMode('CustomPasscode')}>_98 Use Custom Passcode _98 </IonButton>_98 </IonLabel>_98 </IonItem>_98_98 <IonItem>_98 <IonLabel>_98 <IonButton expand="block" color="danger" onClick={logoutClicked}>_98 Logout_98 </IonButton>_98 </IonLabel>_98 </IonItem>_98_98 <IonItem>_98 <div>_98 <div>{session?.email}</div>_98 <div>_98 {session?.firstName} {session?.lastName}_98 </div>_98 <div>{session?.accessToken}</div>_98 <div>{session?.refreshToken}</div>_98 </div>_98 </IonItem>_98 </IonList>_98 </IonContent>_98 </IonPage>_98 );_98};_98_98export default Tab1;
Create a button on Tab1
page to use the custom passcode.
Add CustomPasscode
to the UnlockMode
.
Modify the updateUnlockMode
function to include a case for CustomPasscode
.
Create a couple of functions that allow us to hook in to the onPasscodeRequested
event from other parts of the code.
Create a button on Tab1
page to use the custom passcode.
_93import {_93 BrowserVault,_93 Vault,_93 VaultType,_93 DeviceSecurityType,_93 IdentityVaultConfig,_93} from '@ionic-enterprise/identity-vault';_93import { createVault } from './vault-factory';_93import { Session } from '../models/Session';_93import { setState } from './session-store';_93_93export type UnlockMode =_93 'BiometricsWithPasscode' |_93 'InMemory' |_93 'SecureStorage' |_93 'CustomPasscode';_93_93const vault: Vault | BrowserVault = createVault();_93_93export const initializeVault = async (): Promise<void> => {_93 try {_93 await vault.initialize({_93 key: 'io.ionic.gettingstartedivreact',_93 type: VaultType.SecureStorage,_93 deviceSecurityType: DeviceSecurityType.None,_93 lockAfterBackgrounded: 2000,_93 });_93 } catch (e: unknown) {_93 await vault.clear();_93 await updateUnlockMode('SecureStorage');_93 }_93_93 vault.onLock(() => {_93 setState({ session: null });_93 });_93};_93_93export const storeSession = async (session: Session): Promise<void> => {_93 vault.setValue('session', session);_93 setState({ session });_93};_93_93export const restoreSession = async (): Promise<Session | null> => {_93 let session: Session | null = null;_93 if (!(await vault.isEmpty())) {_93 session = await vault.getValue<Session>('session');_93 }_93 setState({ session });_93 return session;_93};_93_93export const clearSession = async (): Promise<void> => {_93 await vault.clear();_93 setState({ session: null });_93};_93_93export const sessionIsLocked = async (): Promise<boolean> => {_93 return (_93 vault.config?.type !== VaultType.SecureStorage &&_93 vault.config?.type !== VaultType.InMemory &&_93 !(await vault.isEmpty()) &&_93 (await vault.isLocked())_93 );_93};_93_93export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {_93 const newConfig = { ...(vault.config as IdentityVaultConfig) };_93_93 switch (mode) {_93 case 'BiometricsWithPasscode': {_93 newConfig.type = VaultType.DeviceSecurity;_93 newConfig.deviceSecurityType = DeviceSecurityType.Both;_93 break;_93 }_93 case 'InMemory': {_93 newConfig.type = VaultType.InMemory;_93 newConfig.deviceSecurityType = DeviceSecurityType.None;_93 break;_93 }_93 default: {_93 newConfig.type = VaultType.SecureStorage;_93 newConfig.deviceSecurityType = DeviceSecurityType.None;_93 break;_93 }_93 }_93_93 await vault.updateConfig(newConfig);_93};_93_93export const lockSession = async (): Promise<void> => {_93 await vault.lock();_93 setState({ session: null });_93};
Create Passcode workflow
When you are using a Custom Passcode, the type of passcode along with the strategies used to gather it are up to you.
Workflows are used to accomplish two distinct tasks:
- Create the passcode that is used to lock the vault.
- Obtain the passcode that is used to attempt to unlock the vault.
The basic elements for all workflows is:
- Respond to the onPasscodeRequested event.
- Gather the passcode.
- Either use the
onComplete
callback from the onPasscodeRequested event or call setCustomPasscode.
In the next section we will look at one example of a workflow we can use to gather a PIN style passcode from the user.
A PIN Entry Workflow
One type of workflow we can use is a PIN entry workflow. With this workflow, the user will use a Personal Identification Number consisting of four to nine digits to lock and unlock the vault.
When creating a new PIN:
- The user enters a PIN and taps the "Enter" button.
- The user is asked to enter the PIN again to verify.
- The user enters a PIN and taps the "Enter" button.
- If the PINs do not match an error is displayed.
- If the PINs match the modal is closed, passing back the PIN that was entered.
- The calling code passes the PIN to the vault so it can be used to generate the key that is used when the vault locks.
The user cannot cancel the creation of a PIN, they must complete the operation.
When unlocking the vault:
- The user enters a PIN and taps the "Enter" button.
- The modal is closed, passing back the PIN that was entered.
- The calling code passes the PIN to the vault so it can be used to generate a key to unlock the vault.
The user can cancel the PIN entry. If the user cancels, the modal is closed without any data being passed back.
To accomplish this, we will create a PIN Entry Component that encapsulates the user interaction required to enter a PIN. This dialog is displayed in a modal when the vault requests a passcode.
Create a PIN Entry Component
The PIN entry component is a numeric keypad that handles the PIN entry flows for both PIN creation and for unlocking the vault. The full code can be found in our GitHub demo repository. We walk through the key points below.
_155import {_155 IonButton,_155 IonButtons,_155 IonCol,_155 IonContent,_155 IonFooter,_155 IonGrid,_155 IonHeader,_155 IonIcon,_155 IonLabel,_155 IonRow,_155 IonTitle,_155 IonToolbar,_155} from '@ionic/react';_155import { useRef, useState } from 'react';_155import { backspace } from 'ionicons/icons';_155import './PinDialog.css';_155_155type PinDialogProperties = {_155 setPasscodeMode: boolean;_155 onDismiss: (data: string | null) => void;_155};_155_155const PinDialog = ({ setPasscodeMode, onDismiss }: PinDialogProperties) => {_155 const [errorMessage, setErrorMessage] = useState('');_155 const [pin, setPin] = useState('');_155_155 const disableDelete = !pin.length;_155 const disableEnter = !(pin.length > 2);_155 const disableInput = pin.length > 8;_155_155 const verifyPin = useRef<string>('');_155 const displayPin = '*********'.slice(0, pin.length);_155 const title = setPasscodeMode ? 'Create PIN' : 'Unlock';_155 const prompt = verifyPin.current ? 'Verify PIN' : setPasscodeMode ? 'Create Session PIN' : 'Enter PIN to Unlock';_155_155 const handleGetPasscodeFlow = () => {_155 onDismiss(pin);_155 };_155_155 const handleSetPasscodeFlow = () => {_155 if (!verifyPin.current) {_155 verifyPin.current = pin;_155 setPin('');_155 } else if (verifyPin.current === pin) {_155 onDismiss(pin);_155 } else {_155 setErrorMessage('PINs do not match');_155 verifyPin.current = '';_155 setPin('');_155 }_155 };_155_155 const append = (n: number) => {_155 setErrorMessage('');_155 setPin(pin.concat(n.toString()));_155 };_155_155 const remove = () => {_155 if (pin) {_155 setPin(pin.slice(0, pin.length - 1));_155 }_155 };_155_155 const cancel = () => {_155 onDismiss(null);_155 };_155_155 const submit = () => {_155 if (setPasscodeMode) {_155 handleSetPasscodeFlow();_155 } else {_155 handleGetPasscodeFlow();_155 }_155 };_155_155 return (_155 <>_155 <IonHeader>_155 <IonToolbar>_155 <IonTitle>{title}</IonTitle>_155 {setPasscodeMode ? undefined : (_155 <IonButtons slot="start">_155 <IonButton onClick={cancel}> Cancel </IonButton>_155 </IonButtons>_155 )}_155 <IonButtons slot="end">_155 <IonButton strong={true} onClick={submit} disabled={disableEnter}>_155 Enter_155 </IonButton>_155 </IonButtons>_155 </IonToolbar>_155 </IonHeader>_155_155 <IonContent className="pin-dialog-content ion-padding ion-text-center">_155 <IonLabel>_155 <div className="prompt">{prompt}</div>_155 </IonLabel>_155 <IonLabel>_155 <div className="pin">{displayPin}</div>_155 </IonLabel>_155 <IonLabel color="danger">_155 <div className="error">{errorMessage}</div>_155 </IonLabel>_155 </IonContent>_155_155 <IonFooter className="pin-dialog-footer">_155 <IonGrid>_155 <IonRow>_155 {[1, 2, 3].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[4, 5, 6].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[7, 8, 9].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 <IonCol> </IonCol>_155 <IonCol>_155 <IonButton expand="block" fill="outline" onClick={() => append(0)} disabled={disableInput}>_155 0_155 </IonButton>_155 </IonCol>_155 <IonCol>_155 <IonButton icon-only color="tertiary" expand="block" onClick={remove} disabled={disableDelete}>_155 <IonIcon icon={backspace}></IonIcon>_155 </IonButton>_155 </IonCol>_155 </IonRow>_155 </IonGrid>_155 </IonFooter>_155 </>_155 );_155};_155_155export default PinDialog;
Cancel and Submit buttons.
Do not allow the user to cancel if they are creating a passcode.
_155import {_155 IonButton,_155 IonButtons,_155 IonCol,_155 IonContent,_155 IonFooter,_155 IonGrid,_155 IonHeader,_155 IonIcon,_155 IonLabel,_155 IonRow,_155 IonTitle,_155 IonToolbar,_155} from '@ionic/react';_155import { useRef, useState } from 'react';_155import { backspace } from 'ionicons/icons';_155import './PinDialog.css';_155_155type PinDialogProperties = {_155 setPasscodeMode: boolean;_155 onDismiss: (data: string | null) => void;_155};_155_155const PinDialog = ({ setPasscodeMode, onDismiss }: PinDialogProperties) => {_155 const [errorMessage, setErrorMessage] = useState('');_155 const [pin, setPin] = useState('');_155_155 const disableDelete = !pin.length;_155 const disableEnter = !(pin.length > 2);_155 const disableInput = pin.length > 8;_155_155 const verifyPin = useRef<string>('');_155 const displayPin = '*********'.slice(0, pin.length);_155 const title = setPasscodeMode ? 'Create PIN' : 'Unlock';_155 const prompt = verifyPin.current ? 'Verify PIN' : setPasscodeMode ? 'Create Session PIN' : 'Enter PIN to Unlock';_155_155 const handleGetPasscodeFlow = () => {_155 onDismiss(pin);_155 };_155_155 const handleSetPasscodeFlow = () => {_155 if (!verifyPin.current) {_155 verifyPin.current = pin;_155 setPin('');_155 } else if (verifyPin.current === pin) {_155 onDismiss(pin);_155 } else {_155 setErrorMessage('PINs do not match');_155 verifyPin.current = '';_155 setPin('');_155 }_155 };_155_155 const append = (n: number) => {_155 setErrorMessage('');_155 setPin(pin.concat(n.toString()));_155 };_155_155 const remove = () => {_155 if (pin) {_155 setPin(pin.slice(0, pin.length - 1));_155 }_155 };_155_155 const cancel = () => {_155 onDismiss(null);_155 };_155_155 const submit = () => {_155 if (setPasscodeMode) {_155 handleSetPasscodeFlow();_155 } else {_155 handleGetPasscodeFlow();_155 }_155 };_155_155 return (_155 <>_155 <IonHeader>_155 <IonToolbar>_155 <IonTitle>{title}</IonTitle>_155 {setPasscodeMode ? undefined : (_155 <IonButtons slot="start">_155 <IonButton onClick={cancel}> Cancel </IonButton>_155 </IonButtons>_155 )}_155 <IonButtons slot="end">_155 <IonButton strong={true} onClick={submit} disabled={disableEnter}>_155 Enter_155 </IonButton>_155 </IonButtons>_155 </IonToolbar>_155 </IonHeader>_155_155 <IonContent className="pin-dialog-content ion-padding ion-text-center">_155 <IonLabel>_155 <div className="prompt">{prompt}</div>_155 </IonLabel>_155 <IonLabel>_155 <div className="pin">{displayPin}</div>_155 </IonLabel>_155 <IonLabel color="danger">_155 <div className="error">{errorMessage}</div>_155 </IonLabel>_155 </IonContent>_155_155 <IonFooter className="pin-dialog-footer">_155 <IonGrid>_155 <IonRow>_155 {[1, 2, 3].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[4, 5, 6].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[7, 8, 9].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 <IonCol> </IonCol>_155 <IonCol>_155 <IonButton expand="block" fill="outline" onClick={() => append(0)} disabled={disableInput}>_155 0_155 </IonButton>_155 </IonCol>_155 <IonCol>_155 <IonButton icon-only color="tertiary" expand="block" onClick={remove} disabled={disableDelete}>_155 <IonIcon icon={backspace}></IonIcon>_155 </IonButton>_155 </IonCol>_155 </IonRow>_155 </IonGrid>_155 </IonFooter>_155 </>_155 );_155};_155_155export default PinDialog;
A user feedback area so they know what is going on.
_155import {_155 IonButton,_155 IonButtons,_155 IonCol,_155 IonContent,_155 IonFooter,_155 IonGrid,_155 IonHeader,_155 IonIcon,_155 IonLabel,_155 IonRow,_155 IonTitle,_155 IonToolbar,_155} from '@ionic/react';_155import { useRef, useState } from 'react';_155import { backspace } from 'ionicons/icons';_155import './PinDialog.css';_155_155type PinDialogProperties = {_155 setPasscodeMode: boolean;_155 onDismiss: (data: string | null) => void;_155};_155_155const PinDialog = ({ setPasscodeMode, onDismiss }: PinDialogProperties) => {_155 const [errorMessage, setErrorMessage] = useState('');_155 const [pin, setPin] = useState('');_155_155 const disableDelete = !pin.length;_155 const disableEnter = !(pin.length > 2);_155 const disableInput = pin.length > 8;_155_155 const verifyPin = useRef<string>('');_155 const displayPin = '*********'.slice(0, pin.length);_155 const title = setPasscodeMode ? 'Create PIN' : 'Unlock';_155 const prompt = verifyPin.current ? 'Verify PIN' : setPasscodeMode ? 'Create Session PIN' : 'Enter PIN to Unlock';_155_155 const handleGetPasscodeFlow = () => {_155 onDismiss(pin);_155 };_155_155 const handleSetPasscodeFlow = () => {_155 if (!verifyPin.current) {_155 verifyPin.current = pin;_155 setPin('');_155 } else if (verifyPin.current === pin) {_155 onDismiss(pin);_155 } else {_155 setErrorMessage('PINs do not match');_155 verifyPin.current = '';_155 setPin('');_155 }_155 };_155_155 const append = (n: number) => {_155 setErrorMessage('');_155 setPin(pin.concat(n.toString()));_155 };_155_155 const remove = () => {_155 if (pin) {_155 setPin(pin.slice(0, pin.length - 1));_155 }_155 };_155_155 const cancel = () => {_155 onDismiss(null);_155 };_155_155 const submit = () => {_155 if (setPasscodeMode) {_155 handleSetPasscodeFlow();_155 } else {_155 handleGetPasscodeFlow();_155 }_155 };_155_155 return (_155 <>_155 <IonHeader>_155 <IonToolbar>_155 <IonTitle>{title}</IonTitle>_155 {setPasscodeMode ? undefined : (_155 <IonButtons slot="start">_155 <IonButton onClick={cancel}> Cancel </IonButton>_155 </IonButtons>_155 )}_155 <IonButtons slot="end">_155 <IonButton strong={true} onClick={submit} disabled={disableEnter}>_155 Enter_155 </IonButton>_155 </IonButtons>_155 </IonToolbar>_155 </IonHeader>_155_155 <IonContent className="pin-dialog-content ion-padding ion-text-center">_155 <IonLabel>_155 <div className="prompt">{prompt}</div>_155 </IonLabel>_155 <IonLabel>_155 <div className="pin">{displayPin}</div>_155 </IonLabel>_155 <IonLabel color="danger">_155 <div className="error">{errorMessage}</div>_155 </IonLabel>_155 </IonContent>_155_155 <IonFooter className="pin-dialog-footer">_155 <IonGrid>_155 <IonRow>_155 {[1, 2, 3].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[4, 5, 6].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[7, 8, 9].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 <IonCol> </IonCol>_155 <IonCol>_155 <IonButton expand="block" fill="outline" onClick={() => append(0)} disabled={disableInput}>_155 0_155 </IonButton>_155 </IonCol>_155 <IonCol>_155 <IonButton icon-only color="tertiary" expand="block" onClick={remove} disabled={disableDelete}>_155 <IonIcon icon={backspace}></IonIcon>_155 </IonButton>_155 </IonCol>_155 </IonRow>_155 </IonGrid>_155 </IonFooter>_155 </>_155 );_155};_155_155export default PinDialog;
The keypad.
_155import {_155 IonButton,_155 IonButtons,_155 IonCol,_155 IonContent,_155 IonFooter,_155 IonGrid,_155 IonHeader,_155 IonIcon,_155 IonLabel,_155 IonRow,_155 IonTitle,_155 IonToolbar,_155} from '@ionic/react';_155import { useRef, useState } from 'react';_155import { backspace } from 'ionicons/icons';_155import './PinDialog.css';_155_155type PinDialogProperties = {_155 setPasscodeMode: boolean;_155 onDismiss: (data: string | null) => void;_155};_155_155const PinDialog = ({ setPasscodeMode, onDismiss }: PinDialogProperties) => {_155 const [errorMessage, setErrorMessage] = useState('');_155 const [pin, setPin] = useState('');_155_155 const disableDelete = !pin.length;_155 const disableEnter = !(pin.length > 2);_155 const disableInput = pin.length > 8;_155_155 const verifyPin = useRef<string>('');_155 const displayPin = '*********'.slice(0, pin.length);_155 const title = setPasscodeMode ? 'Create PIN' : 'Unlock';_155 const prompt = verifyPin.current ? 'Verify PIN' : setPasscodeMode ? 'Create Session PIN' : 'Enter PIN to Unlock';_155_155 const handleGetPasscodeFlow = () => {_155 onDismiss(pin);_155 };_155_155 const handleSetPasscodeFlow = () => {_155 if (!verifyPin.current) {_155 verifyPin.current = pin;_155 setPin('');_155 } else if (verifyPin.current === pin) {_155 onDismiss(pin);_155 } else {_155 setErrorMessage('PINs do not match');_155 verifyPin.current = '';_155 setPin('');_155 }_155 };_155_155 const append = (n: number) => {_155 setErrorMessage('');_155 setPin(pin.concat(n.toString()));_155 };_155_155 const remove = () => {_155 if (pin) {_155 setPin(pin.slice(0, pin.length - 1));_155 }_155 };_155_155 const cancel = () => {_155 onDismiss(null);_155 };_155_155 const submit = () => {_155 if (setPasscodeMode) {_155 handleSetPasscodeFlow();_155 } else {_155 handleGetPasscodeFlow();_155 }_155 };_155_155 return (_155 <>_155 <IonHeader>_155 <IonToolbar>_155 <IonTitle>{title}</IonTitle>_155 {setPasscodeMode ? undefined : (_155 <IonButtons slot="start">_155 <IonButton onClick={cancel}> Cancel </IonButton>_155 </IonButtons>_155 )}_155 <IonButtons slot="end">_155 <IonButton strong={true} onClick={submit} disabled={disableEnter}>_155 Enter_155 </IonButton>_155 </IonButtons>_155 </IonToolbar>_155 </IonHeader>_155_155 <IonContent className="pin-dialog-content ion-padding ion-text-center">_155 <IonLabel>_155 <div className="prompt">{prompt}</div>_155 </IonLabel>_155 <IonLabel>_155 <div className="pin">{displayPin}</div>_155 </IonLabel>_155 <IonLabel color="danger">_155 <div className="error">{errorMessage}</div>_155 </IonLabel>_155 </IonContent>_155_155 <IonFooter className="pin-dialog-footer">_155 <IonGrid>_155 <IonRow>_155 {[1, 2, 3].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[4, 5, 6].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[7, 8, 9].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 <IonCol> </IonCol>_155 <IonCol>_155 <IonButton expand="block" fill="outline" onClick={() => append(0)} disabled={disableInput}>_155 0_155 </IonButton>_155 </IonCol>_155 <IonCol>_155 <IonButton icon-only color="tertiary" expand="block" onClick={remove} disabled={disableDelete}>_155 <IonIcon icon={backspace}></IonIcon>_155 </IonButton>_155 </IonCol>_155 </IonRow>_155 </IonGrid>_155 </IonFooter>_155 </>_155 );_155};_155_155export default PinDialog;
We do not want to display the PIN, so we calculate a string of asterisks instead.
_155import {_155 IonButton,_155 IonButtons,_155 IonCol,_155 IonContent,_155 IonFooter,_155 IonGrid,_155 IonHeader,_155 IonIcon,_155 IonLabel,_155 IonRow,_155 IonTitle,_155 IonToolbar,_155} from '@ionic/react';_155import { useRef, useState } from 'react';_155import { backspace } from 'ionicons/icons';_155import './PinDialog.css';_155_155type PinDialogProperties = {_155 setPasscodeMode: boolean;_155 onDismiss: (data: string | null) => void;_155};_155_155const PinDialog = ({ setPasscodeMode, onDismiss }: PinDialogProperties) => {_155 const [errorMessage, setErrorMessage] = useState('');_155 const [pin, setPin] = useState('');_155_155 const disableDelete = !pin.length;_155 const disableEnter = !(pin.length > 2);_155 const disableInput = pin.length > 8;_155_155 const verifyPin = useRef<string>('');_155 const displayPin = '*********'.slice(0, pin.length);_155 const title = setPasscodeMode ? 'Create PIN' : 'Unlock';_155 const prompt = verifyPin.current ? 'Verify PIN' : setPasscodeMode ? 'Create Session PIN' : 'Enter PIN to Unlock';_155_155 const handleGetPasscodeFlow = () => {_155 onDismiss(pin);_155 };_155_155 const handleSetPasscodeFlow = () => {_155 if (!verifyPin.current) {_155 verifyPin.current = pin;_155 setPin('');_155 } else if (verifyPin.current === pin) {_155 onDismiss(pin);_155 } else {_155 setErrorMessage('PINs do not match');_155 verifyPin.current = '';_155 setPin('');_155 }_155 };_155_155 const append = (n: number) => {_155 setErrorMessage('');_155 setPin(pin.concat(n.toString()));_155 };_155_155 const remove = () => {_155 if (pin) {_155 setPin(pin.slice(0, pin.length - 1));_155 }_155 };_155_155 const cancel = () => {_155 onDismiss(null);_155 };_155_155 const submit = () => {_155 if (setPasscodeMode) {_155 handleSetPasscodeFlow();_155 } else {_155 handleGetPasscodeFlow();_155 }_155 };_155_155 return (_155 <>_155 <IonHeader>_155 <IonToolbar>_155 <IonTitle>{title}</IonTitle>_155 {setPasscodeMode ? undefined : (_155 <IonButtons slot="start">_155 <IonButton onClick={cancel}> Cancel </IonButton>_155 </IonButtons>_155 )}_155 <IonButtons slot="end">_155 <IonButton strong={true} onClick={submit} disabled={disableEnter}>_155 Enter_155 </IonButton>_155 </IonButtons>_155 </IonToolbar>_155 </IonHeader>_155_155 <IonContent className="pin-dialog-content ion-padding ion-text-center">_155 <IonLabel>_155 <div className="prompt">{prompt}</div>_155 </IonLabel>_155 <IonLabel>_155 <div className="pin">{displayPin}</div>_155 </IonLabel>_155 <IonLabel color="danger">_155 <div className="error">{errorMessage}</div>_155 </IonLabel>_155 </IonContent>_155_155 <IonFooter className="pin-dialog-footer">_155 <IonGrid>_155 <IonRow>_155 {[1, 2, 3].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[4, 5, 6].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[7, 8, 9].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 <IonCol> </IonCol>_155 <IonCol>_155 <IonButton expand="block" fill="outline" onClick={() => append(0)} disabled={disableInput}>_155 0_155 </IonButton>_155 </IonCol>_155 <IonCol>_155 <IonButton icon-only color="tertiary" expand="block" onClick={remove} disabled={disableDelete}>_155 <IonIcon icon={backspace}></IonIcon>_155 </IonButton>_155 </IonCol>_155 </IonRow>_155 </IonGrid>_155 </IonFooter>_155 </>_155 );_155};_155_155export default PinDialog;
When the modal opens, the user is either creating a PIN or entering a PIN to unlock the vault.
_155import {_155 IonButton,_155 IonButtons,_155 IonCol,_155 IonContent,_155 IonFooter,_155 IonGrid,_155 IonHeader,_155 IonIcon,_155 IonLabel,_155 IonRow,_155 IonTitle,_155 IonToolbar,_155} from '@ionic/react';_155import { useRef, useState } from 'react';_155import { backspace } from 'ionicons/icons';_155import './PinDialog.css';_155_155type PinDialogProperties = {_155 setPasscodeMode: boolean;_155 onDismiss: (data: string | null) => void;_155};_155_155const PinDialog = ({ setPasscodeMode, onDismiss }: PinDialogProperties) => {_155 const [errorMessage, setErrorMessage] = useState('');_155 const [pin, setPin] = useState('');_155_155 const disableDelete = !pin.length;_155 const disableEnter = !(pin.length > 2);_155 const disableInput = pin.length > 8;_155_155 const verifyPin = useRef<string>('');_155 const displayPin = '*********'.slice(0, pin.length);_155 const title = setPasscodeMode ? 'Create PIN' : 'Unlock';_155 const prompt = verifyPin.current ? 'Verify PIN' : setPasscodeMode ? 'Create Session PIN' : 'Enter PIN to Unlock';_155_155 const handleGetPasscodeFlow = () => {_155 onDismiss(pin);_155 };_155_155 const handleSetPasscodeFlow = () => {_155 if (!verifyPin.current) {_155 verifyPin.current = pin;_155 setPin('');_155 } else if (verifyPin.current === pin) {_155 onDismiss(pin);_155 } else {_155 setErrorMessage('PINs do not match');_155 verifyPin.current = '';_155 setPin('');_155 }_155 };_155_155 const append = (n: number) => {_155 setErrorMessage('');_155 setPin(pin.concat(n.toString()));_155 };_155_155 const remove = () => {_155 if (pin) {_155 setPin(pin.slice(0, pin.length - 1));_155 }_155 };_155_155 const cancel = () => {_155 onDismiss(null);_155 };_155_155 const submit = () => {_155 if (setPasscodeMode) {_155 handleSetPasscodeFlow();_155 } else {_155 handleGetPasscodeFlow();_155 }_155 };_155_155 return (_155 <>_155 <IonHeader>_155 <IonToolbar>_155 <IonTitle>{title}</IonTitle>_155 {setPasscodeMode ? undefined : (_155 <IonButtons slot="start">_155 <IonButton onClick={cancel}> Cancel </IonButton>_155 </IonButtons>_155 )}_155 <IonButtons slot="end">_155 <IonButton strong={true} onClick={submit} disabled={disableEnter}>_155 Enter_155 </IonButton>_155 </IonButtons>_155 </IonToolbar>_155 </IonHeader>_155_155 <IonContent className="pin-dialog-content ion-padding ion-text-center">_155 <IonLabel>_155 <div className="prompt">{prompt}</div>_155 </IonLabel>_155 <IonLabel>_155 <div className="pin">{displayPin}</div>_155 </IonLabel>_155 <IonLabel color="danger">_155 <div className="error">{errorMessage}</div>_155 </IonLabel>_155 </IonContent>_155_155 <IonFooter className="pin-dialog-footer">_155 <IonGrid>_155 <IonRow>_155 {[1, 2, 3].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[4, 5, 6].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[7, 8, 9].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 <IonCol> </IonCol>_155 <IonCol>_155 <IonButton expand="block" fill="outline" onClick={() => append(0)} disabled={disableInput}>_155 0_155 </IonButton>_155 </IonCol>_155 <IonCol>_155 <IonButton icon-only color="tertiary" expand="block" onClick={remove} disabled={disableDelete}>_155 <IonIcon icon={backspace}></IonIcon>_155 </IonButton>_155 </IonCol>_155 </IonRow>_155 </IonGrid>_155 </IonFooter>_155 </>_155 );_155};_155_155export default PinDialog;
There are three modes. The user is either creating a PIN, verifying the newly created PIN, or entering a PIN to unlock the vault.
_155import {_155 IonButton,_155 IonButtons,_155 IonCol,_155 IonContent,_155 IonFooter,_155 IonGrid,_155 IonHeader,_155 IonIcon,_155 IonLabel,_155 IonRow,_155 IonTitle,_155 IonToolbar,_155} from '@ionic/react';_155import { useRef, useState } from 'react';_155import { backspace } from 'ionicons/icons';_155import './PinDialog.css';_155_155type PinDialogProperties = {_155 setPasscodeMode: boolean;_155 onDismiss: (data: string | null) => void;_155};_155_155const PinDialog = ({ setPasscodeMode, onDismiss }: PinDialogProperties) => {_155 const [errorMessage, setErrorMessage] = useState('');_155 const [pin, setPin] = useState('');_155_155 const disableDelete = !pin.length;_155 const disableEnter = !(pin.length > 2);_155 const disableInput = pin.length > 8;_155_155 const verifyPin = useRef<string>('');_155 const displayPin = '*********'.slice(0, pin.length);_155 const title = setPasscodeMode ? 'Create PIN' : 'Unlock';_155 const prompt = verifyPin.current ? 'Verify PIN' : setPasscodeMode ? 'Create Session PIN' : 'Enter PIN to Unlock';_155_155 const handleGetPasscodeFlow = () => {_155 onDismiss(pin);_155 };_155_155 const handleSetPasscodeFlow = () => {_155 if (!verifyPin.current) {_155 verifyPin.current = pin;_155 setPin('');_155 } else if (verifyPin.current === pin) {_155 onDismiss(pin);_155 } else {_155 setErrorMessage('PINs do not match');_155 verifyPin.current = '';_155 setPin('');_155 }_155 };_155_155 const append = (n: number) => {_155 setErrorMessage('');_155 setPin(pin.concat(n.toString()));_155 };_155_155 const remove = () => {_155 if (pin) {_155 setPin(pin.slice(0, pin.length - 1));_155 }_155 };_155_155 const cancel = () => {_155 onDismiss(null);_155 };_155_155 const submit = () => {_155 if (setPasscodeMode) {_155 handleSetPasscodeFlow();_155 } else {_155 handleGetPasscodeFlow();_155 }_155 };_155_155 return (_155 <>_155 <IonHeader>_155 <IonToolbar>_155 <IonTitle>{title}</IonTitle>_155 {setPasscodeMode ? undefined : (_155 <IonButtons slot="start">_155 <IonButton onClick={cancel}> Cancel </IonButton>_155 </IonButtons>_155 )}_155 <IonButtons slot="end">_155 <IonButton strong={true} onClick={submit} disabled={disableEnter}>_155 Enter_155 </IonButton>_155 </IonButtons>_155 </IonToolbar>_155 </IonHeader>_155_155 <IonContent className="pin-dialog-content ion-padding ion-text-center">_155 <IonLabel>_155 <div className="prompt">{prompt}</div>_155 </IonLabel>_155 <IonLabel>_155 <div className="pin">{displayPin}</div>_155 </IonLabel>_155 <IonLabel color="danger">_155 <div className="error">{errorMessage}</div>_155 </IonLabel>_155 </IonContent>_155_155 <IonFooter className="pin-dialog-footer">_155 <IonGrid>_155 <IonRow>_155 {[1, 2, 3].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[4, 5, 6].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[7, 8, 9].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 <IonCol> </IonCol>_155 <IonCol>_155 <IonButton expand="block" fill="outline" onClick={() => append(0)} disabled={disableInput}>_155 0_155 </IonButton>_155 </IonCol>_155 <IonCol>_155 <IonButton icon-only color="tertiary" expand="block" onClick={remove} disabled={disableDelete}>_155 <IonIcon icon={backspace}></IonIcon>_155 </IonButton>_155 </IonCol>_155 </IonRow>_155 </IonGrid>_155 </IonFooter>_155 </>_155 );_155};_155_155export default PinDialog;
We need to update the PIN value when the user presses a number button or the delete button on the keypad.
_155import {_155 IonButton,_155 IonButtons,_155 IonCol,_155 IonContent,_155 IonFooter,_155 IonGrid,_155 IonHeader,_155 IonIcon,_155 IonLabel,_155 IonRow,_155 IonTitle,_155 IonToolbar,_155} from '@ionic/react';_155import { useRef, useState } from 'react';_155import { backspace } from 'ionicons/icons';_155import './PinDialog.css';_155_155type PinDialogProperties = {_155 setPasscodeMode: boolean;_155 onDismiss: (data: string | null) => void;_155};_155_155const PinDialog = ({ setPasscodeMode, onDismiss }: PinDialogProperties) => {_155 const [errorMessage, setErrorMessage] = useState('');_155 const [pin, setPin] = useState('');_155_155 const disableDelete = !pin.length;_155 const disableEnter = !(pin.length > 2);_155 const disableInput = pin.length > 8;_155_155 const verifyPin = useRef<string>('');_155 const displayPin = '*********'.slice(0, pin.length);_155 const title = setPasscodeMode ? 'Create PIN' : 'Unlock';_155 const prompt = verifyPin.current ? 'Verify PIN' : setPasscodeMode ? 'Create Session PIN' : 'Enter PIN to Unlock';_155_155 const handleGetPasscodeFlow = () => {_155 onDismiss(pin);_155 };_155_155 const handleSetPasscodeFlow = () => {_155 if (!verifyPin.current) {_155 verifyPin.current = pin;_155 setPin('');_155 } else if (verifyPin.current === pin) {_155 onDismiss(pin);_155 } else {_155 setErrorMessage('PINs do not match');_155 verifyPin.current = '';_155 setPin('');_155 }_155 };_155_155 const append = (n: number) => {_155 setErrorMessage('');_155 setPin(pin.concat(n.toString()));_155 };_155_155 const remove = () => {_155 if (pin) {_155 setPin(pin.slice(0, pin.length - 1));_155 }_155 };_155_155 const cancel = () => {_155 onDismiss(null);_155 };_155_155 const submit = () => {_155 if (setPasscodeMode) {_155 handleSetPasscodeFlow();_155 } else {_155 handleGetPasscodeFlow();_155 }_155 };_155_155 return (_155 <>_155 <IonHeader>_155 <IonToolbar>_155 <IonTitle>{title}</IonTitle>_155 {setPasscodeMode ? undefined : (_155 <IonButtons slot="start">_155 <IonButton onClick={cancel}> Cancel </IonButton>_155 </IonButtons>_155 )}_155 <IonButtons slot="end">_155 <IonButton strong={true} onClick={submit} disabled={disableEnter}>_155 Enter_155 </IonButton>_155 </IonButtons>_155 </IonToolbar>_155 </IonHeader>_155_155 <IonContent className="pin-dialog-content ion-padding ion-text-center">_155 <IonLabel>_155 <div className="prompt">{prompt}</div>_155 </IonLabel>_155 <IonLabel>_155 <div className="pin">{displayPin}</div>_155 </IonLabel>_155 <IonLabel color="danger">_155 <div className="error">{errorMessage}</div>_155 </IonLabel>_155 </IonContent>_155_155 <IonFooter className="pin-dialog-footer">_155 <IonGrid>_155 <IonRow>_155 {[1, 2, 3].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[4, 5, 6].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[7, 8, 9].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 <IonCol> </IonCol>_155 <IonCol>_155 <IonButton expand="block" fill="outline" onClick={() => append(0)} disabled={disableInput}>_155 0_155 </IonButton>_155 </IonCol>_155 <IonCol>_155 <IonButton icon-only color="tertiary" expand="block" onClick={remove} disabled={disableDelete}>_155 <IonIcon icon={backspace}></IonIcon>_155 </IonButton>_155 </IonCol>_155 </IonRow>_155 </IonGrid>_155 </IonFooter>_155 </>_155 );_155};_155_155export default PinDialog;
When entering a PIN to unlock the vault, close the modal returning the entered PIN.
_155import {_155 IonButton,_155 IonButtons,_155 IonCol,_155 IonContent,_155 IonFooter,_155 IonGrid,_155 IonHeader,_155 IonIcon,_155 IonLabel,_155 IonRow,_155 IonTitle,_155 IonToolbar,_155} from '@ionic/react';_155import { useRef, useState } from 'react';_155import { backspace } from 'ionicons/icons';_155import './PinDialog.css';_155_155type PinDialogProperties = {_155 setPasscodeMode: boolean;_155 onDismiss: (data: string | null) => void;_155};_155_155const PinDialog = ({ setPasscodeMode, onDismiss }: PinDialogProperties) => {_155 const [errorMessage, setErrorMessage] = useState('');_155 const [pin, setPin] = useState('');_155_155 const disableDelete = !pin.length;_155 const disableEnter = !(pin.length > 2);_155 const disableInput = pin.length > 8;_155_155 const verifyPin = useRef<string>('');_155 const displayPin = '*********'.slice(0, pin.length);_155 const title = setPasscodeMode ? 'Create PIN' : 'Unlock';_155 const prompt = verifyPin.current ? 'Verify PIN' : setPasscodeMode ? 'Create Session PIN' : 'Enter PIN to Unlock';_155_155 const handleGetPasscodeFlow = () => {_155 onDismiss(pin);_155 };_155_155 const handleSetPasscodeFlow = () => {_155 if (!verifyPin.current) {_155 verifyPin.current = pin;_155 setPin('');_155 } else if (verifyPin.current === pin) {_155 onDismiss(pin);_155 } else {_155 setErrorMessage('PINs do not match');_155 verifyPin.current = '';_155 setPin('');_155 }_155 };_155_155 const append = (n: number) => {_155 setErrorMessage('');_155 setPin(pin.concat(n.toString()));_155 };_155_155 const remove = () => {_155 if (pin) {_155 setPin(pin.slice(0, pin.length - 1));_155 }_155 };_155_155 const cancel = () => {_155 onDismiss(null);_155 };_155_155 const submit = () => {_155 if (setPasscodeMode) {_155 handleSetPasscodeFlow();_155 } else {_155 handleGetPasscodeFlow();_155 }_155 };_155_155 return (_155 <>_155 <IonHeader>_155 <IonToolbar>_155 <IonTitle>{title}</IonTitle>_155 {setPasscodeMode ? undefined : (_155 <IonButtons slot="start">_155 <IonButton onClick={cancel}> Cancel </IonButton>_155 </IonButtons>_155 )}_155 <IonButtons slot="end">_155 <IonButton strong={true} onClick={submit} disabled={disableEnter}>_155 Enter_155 </IonButton>_155 </IonButtons>_155 </IonToolbar>_155 </IonHeader>_155_155 <IonContent className="pin-dialog-content ion-padding ion-text-center">_155 <IonLabel>_155 <div className="prompt">{prompt}</div>_155 </IonLabel>_155 <IonLabel>_155 <div className="pin">{displayPin}</div>_155 </IonLabel>_155 <IonLabel color="danger">_155 <div className="error">{errorMessage}</div>_155 </IonLabel>_155 </IonContent>_155_155 <IonFooter className="pin-dialog-footer">_155 <IonGrid>_155 <IonRow>_155 {[1, 2, 3].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[4, 5, 6].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[7, 8, 9].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 <IonCol> </IonCol>_155 <IonCol>_155 <IonButton expand="block" fill="outline" onClick={() => append(0)} disabled={disableInput}>_155 0_155 </IonButton>_155 </IonCol>_155 <IonCol>_155 <IonButton icon-only color="tertiary" expand="block" onClick={remove} disabled={disableDelete}>_155 <IonIcon icon={backspace}></IonIcon>_155 </IonButton>_155 </IonCol>_155 </IonRow>_155 </IonGrid>_155 </IonFooter>_155 </>_155 );_155};_155_155export default PinDialog;
When creating a PIN, either move on to "verify" mode, display an error if the PIN failed in "verify" mode, or close the modal returning the PIN.
_155import {_155 IonButton,_155 IonButtons,_155 IonCol,_155 IonContent,_155 IonFooter,_155 IonGrid,_155 IonHeader,_155 IonIcon,_155 IonLabel,_155 IonRow,_155 IonTitle,_155 IonToolbar,_155} from '@ionic/react';_155import { useRef, useState } from 'react';_155import { backspace } from 'ionicons/icons';_155import './PinDialog.css';_155_155type PinDialogProperties = {_155 setPasscodeMode: boolean;_155 onDismiss: (data: string | null) => void;_155};_155_155const PinDialog = ({ setPasscodeMode, onDismiss }: PinDialogProperties) => {_155 const [errorMessage, setErrorMessage] = useState('');_155 const [pin, setPin] = useState('');_155_155 const disableDelete = !pin.length;_155 const disableEnter = !(pin.length > 2);_155 const disableInput = pin.length > 8;_155_155 const verifyPin = useRef<string>('');_155 const displayPin = '*********'.slice(0, pin.length);_155 const title = setPasscodeMode ? 'Create PIN' : 'Unlock';_155 const prompt = verifyPin.current ? 'Verify PIN' : setPasscodeMode ? 'Create Session PIN' : 'Enter PIN to Unlock';_155_155 const handleGetPasscodeFlow = () => {_155 onDismiss(pin);_155 };_155_155 const handleSetPasscodeFlow = () => {_155 if (!verifyPin.current) {_155 verifyPin.current = pin;_155 setPin('');_155 } else if (verifyPin.current === pin) {_155 onDismiss(pin);_155 } else {_155 setErrorMessage('PINs do not match');_155 verifyPin.current = '';_155 setPin('');_155 }_155 };_155_155 const append = (n: number) => {_155 setErrorMessage('');_155 setPin(pin.concat(n.toString()));_155 };_155_155 const remove = () => {_155 if (pin) {_155 setPin(pin.slice(0, pin.length - 1));_155 }_155 };_155_155 const cancel = () => {_155 onDismiss(null);_155 };_155_155 const submit = () => {_155 if (setPasscodeMode) {_155 handleSetPasscodeFlow();_155 } else {_155 handleGetPasscodeFlow();_155 }_155 };_155_155 return (_155 <>_155 <IonHeader>_155 <IonToolbar>_155 <IonTitle>{title}</IonTitle>_155 {setPasscodeMode ? undefined : (_155 <IonButtons slot="start">_155 <IonButton onClick={cancel}> Cancel </IonButton>_155 </IonButtons>_155 )}_155 <IonButtons slot="end">_155 <IonButton strong={true} onClick={submit} disabled={disableEnter}>_155 Enter_155 </IonButton>_155 </IonButtons>_155 </IonToolbar>_155 </IonHeader>_155_155 <IonContent className="pin-dialog-content ion-padding ion-text-center">_155 <IonLabel>_155 <div className="prompt">{prompt}</div>_155 </IonLabel>_155 <IonLabel>_155 <div className="pin">{displayPin}</div>_155 </IonLabel>_155 <IonLabel color="danger">_155 <div className="error">{errorMessage}</div>_155 </IonLabel>_155 </IonContent>_155_155 <IonFooter className="pin-dialog-footer">_155 <IonGrid>_155 <IonRow>_155 {[1, 2, 3].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[4, 5, 6].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[7, 8, 9].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 <IonCol> </IonCol>_155 <IonCol>_155 <IonButton expand="block" fill="outline" onClick={() => append(0)} disabled={disableInput}>_155 0_155 </IonButton>_155 </IonCol>_155 <IonCol>_155 <IonButton icon-only color="tertiary" expand="block" onClick={remove} disabled={disableDelete}>_155 <IonIcon icon={backspace}></IonIcon>_155 </IonButton>_155 </IonCol>_155 </IonRow>_155 </IonGrid>_155 </IonFooter>_155 </>_155 );_155};_155_155export default PinDialog;
When the user taps the Enter button, we need to handle the proper flow.
_18.pin-dialog-content .prompt {_18 font-size: 2rem;_18 font-weight: bold;_18}_18_18.pin-dialog-content .pin {_18 font-size: 3rem;_18 font-weight: bold;_18}_18_18.pin-dialog-content .error {_18 font-size: 1.5rem;_18 font-weight: bold;_18}_18_18.pin-dialog-footer ion-grid {_18 padding-bottom: 32px;_18}
Add some basic styling.
Cancel and Submit buttons.
Do not allow the user to cancel if they are creating a passcode.
A user feedback area so they know what is going on.
The keypad.
We do not want to display the PIN, so we calculate a string of asterisks instead.
When the modal opens, the user is either creating a PIN or entering a PIN to unlock the vault.
There are three modes. The user is either creating a PIN, verifying the newly created PIN, or entering a PIN to unlock the vault.
We need to update the PIN value when the user presses a number button or the delete button on the keypad.
When entering a PIN to unlock the vault, close the modal returning the entered PIN.
When creating a PIN, either move on to "verify" mode, display an error if the PIN failed in "verify" mode, or close the modal returning the PIN.
When the user taps the Enter button, we need to handle the proper flow.
Add some basic styling.
_155import {_155 IonButton,_155 IonButtons,_155 IonCol,_155 IonContent,_155 IonFooter,_155 IonGrid,_155 IonHeader,_155 IonIcon,_155 IonLabel,_155 IonRow,_155 IonTitle,_155 IonToolbar,_155} from '@ionic/react';_155import { useRef, useState } from 'react';_155import { backspace } from 'ionicons/icons';_155import './PinDialog.css';_155_155type PinDialogProperties = {_155 setPasscodeMode: boolean;_155 onDismiss: (data: string | null) => void;_155};_155_155const PinDialog = ({ setPasscodeMode, onDismiss }: PinDialogProperties) => {_155 const [errorMessage, setErrorMessage] = useState('');_155 const [pin, setPin] = useState('');_155_155 const disableDelete = !pin.length;_155 const disableEnter = !(pin.length > 2);_155 const disableInput = pin.length > 8;_155_155 const verifyPin = useRef<string>('');_155 const displayPin = '*********'.slice(0, pin.length);_155 const title = setPasscodeMode ? 'Create PIN' : 'Unlock';_155 const prompt = verifyPin.current ? 'Verify PIN' : setPasscodeMode ? 'Create Session PIN' : 'Enter PIN to Unlock';_155_155 const handleGetPasscodeFlow = () => {_155 onDismiss(pin);_155 };_155_155 const handleSetPasscodeFlow = () => {_155 if (!verifyPin.current) {_155 verifyPin.current = pin;_155 setPin('');_155 } else if (verifyPin.current === pin) {_155 onDismiss(pin);_155 } else {_155 setErrorMessage('PINs do not match');_155 verifyPin.current = '';_155 setPin('');_155 }_155 };_155_155 const append = (n: number) => {_155 setErrorMessage('');_155 setPin(pin.concat(n.toString()));_155 };_155_155 const remove = () => {_155 if (pin) {_155 setPin(pin.slice(0, pin.length - 1));_155 }_155 };_155_155 const cancel = () => {_155 onDismiss(null);_155 };_155_155 const submit = () => {_155 if (setPasscodeMode) {_155 handleSetPasscodeFlow();_155 } else {_155 handleGetPasscodeFlow();_155 }_155 };_155_155 return (_155 <>_155 <IonHeader>_155 <IonToolbar>_155 <IonTitle>{title}</IonTitle>_155 {setPasscodeMode ? undefined : (_155 <IonButtons slot="start">_155 <IonButton onClick={cancel}> Cancel </IonButton>_155 </IonButtons>_155 )}_155 <IonButtons slot="end">_155 <IonButton strong={true} onClick={submit} disabled={disableEnter}>_155 Enter_155 </IonButton>_155 </IonButtons>_155 </IonToolbar>_155 </IonHeader>_155_155 <IonContent className="pin-dialog-content ion-padding ion-text-center">_155 <IonLabel>_155 <div className="prompt">{prompt}</div>_155 </IonLabel>_155 <IonLabel>_155 <div className="pin">{displayPin}</div>_155 </IonLabel>_155 <IonLabel color="danger">_155 <div className="error">{errorMessage}</div>_155 </IonLabel>_155 </IonContent>_155_155 <IonFooter className="pin-dialog-footer">_155 <IonGrid>_155 <IonRow>_155 {[1, 2, 3].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[4, 5, 6].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 {[7, 8, 9].map((n) => (_155 <IonCol key={n}>_155 <IonButton expand="block" fill="outline" onClick={() => append(n)} disabled={disableInput}>_155 {n}_155 </IonButton>_155 </IonCol>_155 ))}_155 </IonRow>_155 <IonRow>_155 <IonCol> </IonCol>_155 <IonCol>_155 <IonButton expand="block" fill="outline" onClick={() => append(0)} disabled={disableInput}>_155 0_155 </IonButton>_155 </IonCol>_155 <IonCol>_155 <IonButton icon-only color="tertiary" expand="block" onClick={remove} disabled={disableDelete}>_155 <IonIcon icon={backspace}></IonIcon>_155 </IonButton>_155 </IonCol>_155 </IonRow>_155 </IonGrid>_155 </IonFooter>_155 </>_155 );_155};_155_155export default PinDialog;
Hook Up the Modal
When the vault needs a PIN it will call onPasscodeRequested()
with a callback function that passes a Boolean flag
indicating if the passcode is being used to set a new passcode (true
) or unlock the vault (false
). The signature
used in this app also takes an onComplete
callback that we will use to pass the entered PIN back to the vault.
_104import {_104 BrowserVault,_104 Vault,_104 VaultType,_104 DeviceSecurityType,_104 IdentityVaultConfig,_104} from '@ionic-enterprise/identity-vault';_104import { createVault } from './vault-factory';_104import { Session } from '../models/Session';_104import { setState } from './session-store';_104_104export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage' | 'CustomPasscode';_104_104const vault: Vault | BrowserVault = createVault();_104_104export const initializeVault = async (): Promise<void> => {_104 try {_104 await vault.initialize({_104 key: 'io.ionic.gettingstartedivreact',_104 type: VaultType.SecureStorage,_104 deviceSecurityType: DeviceSecurityType.None,_104 lockAfterBackgrounded: 2000,_104 });_104 } catch (e: unknown) {_104 await vault.clear();_104 await updateUnlockMode('SecureStorage');_104 }_104_104 vault.onLock(() => {_104 setState({ session: null });_104 });_104};_104_104export const addOnPasscodeRequested = (_104 callback: (isPasscodeSetRequest: boolean, onComplete: (code: string) => void) => void,_104) => {_104 vault.onPasscodeRequested(callback);_104};_104_104export const removeOnPasscodeRequested = () => {_104 vault.onPasscodeRequested(() => {});_104};_104_104export const storeSession = async (session: Session): Promise<void> => {_104 vault.setValue('session', session);_104 setState({ session });_104};_104_104export const restoreSession = async (): Promise<Session | null> => {_104 let session: Session | null = null;_104 if (!(await vault.isEmpty())) {_104 session = await vault.getValue<Session>('session');_104 }_104 setState({ session });_104 return session;_104};_104_104export const clearSession = async (): Promise<void> => {_104 await vault.clear();_104 setState({ session: null });_104};_104_104export const sessionIsLocked = async (): Promise<boolean> => {_104 return (_104 vault.config?.type !== VaultType.SecureStorage &&_104 vault.config?.type !== VaultType.InMemory &&_104 !(await vault.isEmpty()) &&_104 (await vault.isLocked())_104 );_104};_104_104export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {_104 const newConfig = { ...(vault.config as IdentityVaultConfig) };_104_104 switch (mode) {_104 case 'BiometricsWithPasscode': {_104 newConfig.type = VaultType.DeviceSecurity;_104 newConfig.deviceSecurityType = DeviceSecurityType.Both;_104 break;_104 }_104 case 'InMemory': {_104 newConfig.type = VaultType.InMemory;_104 newConfig.deviceSecurityType = DeviceSecurityType.None;_104 break;_104 }_104 case 'CustomPasscode': {_104 newConfig.type = VaultType.CustomPasscode;_104 newConfig.deviceSecurityType = DeviceSecurityType.None;_104 break;_104 }_104 default: {_104 newConfig.type = VaultType.SecureStorage;_104 newConfig.deviceSecurityType = DeviceSecurityType.None;_104 break;_104 }_104 }_104_104 await vault.updateConfig(newConfig);_104};_104_104export const lockSession = async (): Promise<void> => {_104 await vault.lock();_104 setState({ session: null });_104};
We previously created the hooks into onPasscodeRequested()
that we need.
_60import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';_60import { IonReactRouter } from '@ionic/react-router';_60import { useRef, useState } from 'react';_60import { Route } from 'react-router-dom';_60import Tabs from './components/Tabs';_60import Login from './pages/Login';_60import Start from './pages/Start';_60import Unlock from './pages/Unlock';_60_60/* Core CSS required for Ionic components to work properly */_60import '@ionic/react/css/core.css';_60_60/* Basic CSS for apps built with Ionic */_60import '@ionic/react/css/normalize.css';_60import '@ionic/react/css/structure.css';_60import '@ionic/react/css/typography.css';_60_60/* Optional CSS utils that can be commented out */_60import '@ionic/react/css/display.css';_60import '@ionic/react/css/flex-utils.css';_60import '@ionic/react/css/float-elements.css';_60import '@ionic/react/css/padding.css';_60import '@ionic/react/css/text-alignment.css';_60import '@ionic/react/css/text-transformation.css';_60_60import '@ionic/react/css/palettes/dark.system.css';_60_60/* Theme variables */_60import './theme/variables.css';_60_60setupIonicReact();_60_60const App: React.FC = () => {_60 const [showPinDialog, setShowPinDialog] = useState<boolean>(false);_60 const passcodeSetRequest = useRef<boolean>(true);_60 const passcodeCallback = useRef<((code: string) => void) | null>(null);_60_60 return (_60 <IonApp>_60 <IonReactRouter>_60 <IonRouterOutlet>_60 <Route exact path="/login">_60 <Login />_60 </Route>_60 <Route exact path="/">_60 <Start />_60 </Route>_60 <Route exact path="/unlock">_60 <Unlock />_60 </Route>_60 <Route path="/tabs">_60 <Tabs />_60 </Route>_60 </IonRouterOutlet>_60 </IonReactRouter>_60 </IonApp>_60 );_60};_60_60export default App;
Set up the state required to open and close the PIN dialog as well as handle the data passing.
_73import { IonApp, IonModal, IonRouterOutlet, setupIonicReact } from '@ionic/react';_73import { IonReactRouter } from '@ionic/react-router';_73import { useRef, useState } from 'react';_73import { Route } from 'react-router-dom';_73import PinDialog from './components/PinDialog';_73import Tabs from './components/Tabs';_73import Login from './pages/Login';_73import Start from './pages/Start';_73import Unlock from './pages/Unlock';_73_73/* Core CSS required for Ionic components to work properly */_73import '@ionic/react/css/core.css';_73_73/* Basic CSS for apps built with Ionic */_73import '@ionic/react/css/normalize.css';_73import '@ionic/react/css/structure.css';_73import '@ionic/react/css/typography.css';_73_73/* Optional CSS utils that can be commented out */_73import '@ionic/react/css/display.css';_73import '@ionic/react/css/flex-utils.css';_73import '@ionic/react/css/float-elements.css';_73import '@ionic/react/css/padding.css';_73import '@ionic/react/css/text-alignment.css';_73import '@ionic/react/css/text-transformation.css';_73_73import '@ionic/react/css/palettes/dark.system.css';_73_73/* Theme variables */_73import './theme/variables.css';_73_73setupIonicReact();_73_73const App: React.FC = () => {_73 const [showPinDialog, setShowPinDialog] = useState<boolean>(false);_73 const passcodeSetRequest = useRef<boolean>(true);_73 const passcodeCallback = useRef<((code: string) => void) | null>(null);_73_73 return (_73 <IonApp>_73 <IonReactRouter>_73 <IonModal isOpen={showPinDialog} backdropDismiss={false}>_73 <PinDialog_73 setPasscodeMode={passcodeSetRequest.current}_73 onDismiss={(code: string | null) => {_73 if (passcodeCallback.current) {_73 passcodeCallback.current(code || '');_73 }_73 setShowPinDialog(false);_73 }}_73 />_73 </IonModal>_73_73 <IonRouterOutlet>_73 <Route exact path="/login">_73 <Login />_73 </Route>_73 <Route exact path="/">_73 <Start />_73 </Route>_73 <Route exact path="/unlock">_73 <Unlock />_73 </Route>_73 <Route path="/tabs">_73 <Tabs />_73 </Route>_73 </IonRouterOutlet>_73 </IonReactRouter>_73 </IonApp>_73 );_73};_73_73export default App;
Include the PIN dialog in the application, displayed in a modal.
_84import { IonApp, IonModal, IonRouterOutlet, setupIonicReact } from '@ionic/react';_84import { IonReactRouter } from '@ionic/react-router';_84import { useEffect, useRef, useState } from 'react';_84import { Route } from 'react-router-dom';_84import PinDialog from './components/PinDialog';_84import Tabs from './components/Tabs';_84import Login from './pages/Login';_84import Start from './pages/Start';_84import Unlock from './pages/Unlock';_84import { addOnPasscodeRequested, removeOnPasscodeRequested } from './util/session-vault';_84_84/* Core CSS required for Ionic components to work properly */_84import '@ionic/react/css/core.css';_84_84/* Basic CSS for apps built with Ionic */_84import '@ionic/react/css/normalize.css';_84import '@ionic/react/css/structure.css';_84import '@ionic/react/css/typography.css';_84_84/* Optional CSS utils that can be commented out */_84import '@ionic/react/css/display.css';_84import '@ionic/react/css/flex-utils.css';_84import '@ionic/react/css/float-elements.css';_84import '@ionic/react/css/padding.css';_84import '@ionic/react/css/text-alignment.css';_84import '@ionic/react/css/text-transformation.css';_84_84import '@ionic/react/css/palettes/dark.system.css';_84_84/* Theme variables */_84import './theme/variables.css';_84_84setupIonicReact();_84_84const App: React.FC = () => {_84 const [showPinDialog, setShowPinDialog] = useState<boolean>(false);_84 const passcodeSetRequest = useRef<boolean>(true);_84 const passcodeCallback = useRef<((code: string) => void) | null>(null);_84_84 useEffect(() => {_84 const onPasscodeRequested = (isPasscodeSetRequest: boolean, onComplete: (code: string) => void) => {_84 passcodeSetRequest.current = isPasscodeSetRequest;_84 passcodeCallback.current = onComplete;_84 setShowPinDialog(true);_84 };_84 addOnPasscodeRequested(onPasscodeRequested);_84 return removeOnPasscodeRequested;_84 }, []);_84_84 return (_84 <IonApp>_84 <IonReactRouter>_84 <IonModal isOpen={showPinDialog} backdropDismiss={false}>_84 <PinDialog_84 setPasscodeMode={passcodeSetRequest.current}_84 onDismiss={(code: string | null) => {_84 if (passcodeCallback.current) {_84 passcodeCallback.current(code || '');_84 }_84 setShowPinDialog(false);_84 }}_84 />_84 </IonModal>_84_84 <IonRouterOutlet>_84 <Route exact path="/login">_84 <Login />_84 </Route>_84 <Route exact path="/">_84 <Start />_84 </Route>_84 <Route exact path="/unlock">_84 <Unlock />_84 </Route>_84 <Route path="/tabs">_84 <Tabs />_84 </Route>_84 </IonRouterOutlet>_84 </IonReactRouter>_84 </IonApp>_84 );_84};_84_84export default App;
Hook up the callbacks for the onPasscodeRequested
event.
We previously created the hooks into onPasscodeRequested()
that we need.
Set up the state required to open and close the PIN dialog as well as handle the data passing.
Include the PIN dialog in the application, displayed in a modal.
Hook up the callbacks for the onPasscodeRequested
event.
_104import {_104 BrowserVault,_104 Vault,_104 VaultType,_104 DeviceSecurityType,_104 IdentityVaultConfig,_104} from '@ionic-enterprise/identity-vault';_104import { createVault } from './vault-factory';_104import { Session } from '../models/Session';_104import { setState } from './session-store';_104_104export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage' | 'CustomPasscode';_104_104const vault: Vault | BrowserVault = createVault();_104_104export const initializeVault = async (): Promise<void> => {_104 try {_104 await vault.initialize({_104 key: 'io.ionic.gettingstartedivreact',_104 type: VaultType.SecureStorage,_104 deviceSecurityType: DeviceSecurityType.None,_104 lockAfterBackgrounded: 2000,_104 });_104 } catch (e: unknown) {_104 await vault.clear();_104 await updateUnlockMode('SecureStorage');_104 }_104_104 vault.onLock(() => {_104 setState({ session: null });_104 });_104};_104_104export const addOnPasscodeRequested = (_104 callback: (isPasscodeSetRequest: boolean, onComplete: (code: string) => void) => void,_104) => {_104 vault.onPasscodeRequested(callback);_104};_104_104export const removeOnPasscodeRequested = () => {_104 vault.onPasscodeRequested(() => {});_104};_104_104export const storeSession = async (session: Session): Promise<void> => {_104 vault.setValue('session', session);_104 setState({ session });_104};_104_104export const restoreSession = async (): Promise<Session | null> => {_104 let session: Session | null = null;_104 if (!(await vault.isEmpty())) {_104 session = await vault.getValue<Session>('session');_104 }_104 setState({ session });_104 return session;_104};_104_104export const clearSession = async (): Promise<void> => {_104 await vault.clear();_104 setState({ session: null });_104};_104_104export const sessionIsLocked = async (): Promise<boolean> => {_104 return (_104 vault.config?.type !== VaultType.SecureStorage &&_104 vault.config?.type !== VaultType.InMemory &&_104 !(await vault.isEmpty()) &&_104 (await vault.isLocked())_104 );_104};_104_104export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {_104 const newConfig = { ...(vault.config as IdentityVaultConfig) };_104_104 switch (mode) {_104 case 'BiometricsWithPasscode': {_104 newConfig.type = VaultType.DeviceSecurity;_104 newConfig.deviceSecurityType = DeviceSecurityType.Both;_104 break;_104 }_104 case 'InMemory': {_104 newConfig.type = VaultType.InMemory;_104 newConfig.deviceSecurityType = DeviceSecurityType.None;_104 break;_104 }_104 case 'CustomPasscode': {_104 newConfig.type = VaultType.CustomPasscode;_104 newConfig.deviceSecurityType = DeviceSecurityType.None;_104 break;_104 }_104 default: {_104 newConfig.type = VaultType.SecureStorage;_104 newConfig.deviceSecurityType = DeviceSecurityType.None;_104 break;_104 }_104 }_104_104 await vault.updateConfig(newConfig);_104};_104_104export const lockSession = async (): Promise<void> => {_104 await vault.lock();_104 setState({ session: null });_104};
At this point, we can test the Custom Passcode workflows via the "Use Custom Passcode" button.
Advanced Implementations
The Custom Passcode is very flexible, and you do not have to use the basic PIN Entry implementation we have outlined here. We do not have demos for these, but would be happy to assist you in your implementation if needed.
Here are a few ideas for other implementations.
Custom Passcode Entry
We created a PIN Entry component here. We could have created any sort of component we wanted so long as that component generates a unique key based on user interaction. Here are some other ideas:
- Ask the user to provide a passphrase.
- Ask the user to enter a pattern on a grid.
- Ask the user to select a combination of pictures or colors.
- Use a voice recognition to ask the user for a secret word (accuracy of the voice recognition will obviously be very important).
The input mechanism is up to you so long as it results in a string that can be used to generate a key.
Using with a Device
Vault
The suggested configuration for a Device
vault is to use DeviceSecurityType.Both
to allow Biometrics to be used with
a System Passcode backup. You can, however, use Custom Passcode as a backup mechanism. To do this, you will need two vaults,
a Device
vault using DeviceSecurityType.Biometrics
(Biometrics-only) and a CustomPasscode
vault.
The CustomPasscode
vault is the source of truth for the authentication session information.
The Device
vault stores the passcode for the CustomPasscode
vault.
When unlocking the application, the following workflow is used:
- User uses Biometrics to unlock the
Device
vault. - The Passcode is obtained from the unlocked
Device
vault. - The stored Passcode is used to unlock the
CustomPasscode
vault to obtain the session information.
In cases where the user fails to unlock the Device
, the application can use the CustomPasscode
vault directly, asking
the user to enter the passcode in order to unlock the vault.
Best Practices
Favor Device
Vaults
While CustomPasscode
vaults are secure, a Device
vault is able to take full advantage of the device's hardware-based
encryption and secure storage mechanisms. For this reason, Device
vaults should be favored on devices that support them.
For the most secure applications, the following types of vaults are suggested:
- Use a
Device
type vault withDeviceSecurityType.Both
. If the user does not have Biometrics configured, this configuration will fall back to using the System Passcode. - Use an
InMemory
vault if a user has neither Biometrics nor a System Passcode configured on their device.
With this configuration, users who have secured their devices properly will be able to utilize their device security to remain logged in to your application. For users who have not properly secured their devices, your application will still be secure since users will need to provide their application credentials every time the application is locked or launched.
Favor System Passcode Backup
Using a Custom Passcode as backup for Biometrics has the following disadvantages:
- It is non-standard. The standard fallback for Biometrics failing when unlocking the device itself, for example, is to use the System Passcode. As such, that is what the user will expect.
- Using the System Passcode is built in to Identity Vault. As such, it requires far less code in your application.
- Using the System Passcode takes full advantage of the device's encryption hardware area.
As such, while using a Custom Passcode is secure, using a System Passcode as the fallback mechanism for failing Biometrics maximizes both security and usability while minimizing complexity within your application.