Skip to main content
Version: 5.0

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:

  1. When the vault is created, the user supplies a passcode.
  2. The passcode is used to generate a key and the passcode is discarded.
  3. This key becomes part of the current vault configuration so the vault can later be locked.
  4. When the vault is locked the key is destroyed, resulting in a new key being required to unlock the vault.
  5. When the user wants to unlock the vault, they supply a passcode.
  6. The passcode is used to generate a key and the passcode is discarded.
  7. 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

src/util/session-vault.ts
src/pages/Tab1.tsx

_93
import {
_93
BrowserVault,
_93
Vault,
_93
VaultType,
_93
DeviceSecurityType,
_93
IdentityVaultConfig,
_93
} from '@ionic-enterprise/identity-vault';
_93
import { createVault } from './vault-factory';
_93
import { Session } from '../models/Session';
_93
import { setState } from './session-store';
_93
_93
export type UnlockMode =
_93
'BiometricsWithPasscode' |
_93
'InMemory' |
_93
'SecureStorage' |
_93
'CustomPasscode';
_93
_93
const vault: Vault | BrowserVault = createVault();
_93
_93
export 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
_93
export const storeSession = async (session: Session): Promise<void> => {
_93
vault.setValue('session', session);
_93
setState({ session });
_93
};
_93
_93
export 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
_93
export const clearSession = async (): Promise<void> => {
_93
await vault.clear();
_93
setState({ session: null });
_93
};
_93
_93
export 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
_93
export 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
_93
export const lockSession = async (): Promise<void> => {
_93
await vault.lock();
_93
setState({ session: null });
_93
};

Add CustomPasscode to the UnlockMode.

src/util/session-vault.ts
src/pages/Tab1.tsx

_98
import {
_98
BrowserVault,
_98
Vault,
_98
VaultType,
_98
DeviceSecurityType,
_98
IdentityVaultConfig,
_98
} from '@ionic-enterprise/identity-vault';
_98
import { createVault } from './vault-factory';
_98
import { Session } from '../models/Session';
_98
import { setState } from './session-store';
_98
_98
export type UnlockMode =
_98
'BiometricsWithPasscode' |
_98
'InMemory' |
_98
'SecureStorage' |
_98
'CustomPasscode';
_98
_98
const vault: Vault | BrowserVault = createVault();
_98
_98
export 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
_98
export const storeSession = async (session: Session): Promise<void> => {
_98
vault.setValue('session', session);
_98
setState({ session });
_98
};
_98
_98
export 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
_98
export const clearSession = async (): Promise<void> => {
_98
await vault.clear();
_98
setState({ session: null });
_98
};
_98
_98
export 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
_98
export 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
_98
export const lockSession = async (): Promise<void> => {
_98
await vault.lock();
_98
setState({ session: null });
_98
};

Modify the updateUnlockMode function to include a case for CustomPasscode.

src/util/session-vault.ts
src/pages/Tab1.tsx

_108
import {
_108
BrowserVault,
_108
Vault,
_108
VaultType,
_108
DeviceSecurityType,
_108
IdentityVaultConfig,
_108
} from '@ionic-enterprise/identity-vault';
_108
import { createVault } from './vault-factory';
_108
import { Session } from '../models/Session';
_108
import { setState } from './session-store';
_108
_108
export type UnlockMode =
_108
'BiometricsWithPasscode' |
_108
'InMemory' |
_108
'SecureStorage' |
_108
'CustomPasscode';
_108
_108
const vault: Vault | BrowserVault = createVault();
_108
_108
export 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
_108
export const addOnPasscodeRequested = (
_108
callback: (isPasscodeSetRequest: boolean, onComplete: (code: string) => void) => void,
_108
) => {
_108
vault.onPasscodeRequested(callback);
_108
};
_108
_108
export const removeOnPasscodeRequested = () => {
_108
vault.onPasscodeRequested(() => {});
_108
};
_108
_108
export const storeSession = async (session: Session): Promise<void> => {
_108
vault.setValue('session', session);
_108
setState({ session });
_108
};
_108
_108
export 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
_108
export const clearSession = async (): Promise<void> => {
_108
await vault.clear();
_108
setState({ session: null });
_108
};
_108
_108
export 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
_108
export 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
_108
export 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.

src/util/session-vault.ts
src/pages/Tab1.tsx

_98
import {
_98
IonButton,
_98
IonContent,
_98
IonHeader,
_98
IonItem,
_98
IonLabel,
_98
IonList,
_98
IonPage,
_98
IonTitle,
_98
IonToolbar,
_98
} from '@ionic/react';
_98
import { useHistory } from 'react-router';
_98
import { logout } from '../util/authentication';
_98
import { useSession } from '../util/session-store';
_98
import { updateUnlockMode } from '../util/session-vault';
_98
import './Tab1.css';
_98
_98
const 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
_98
export 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.

src/util/session-vault.ts
src/pages/Tab1.tsx

_93
import {
_93
BrowserVault,
_93
Vault,
_93
VaultType,
_93
DeviceSecurityType,
_93
IdentityVaultConfig,
_93
} from '@ionic-enterprise/identity-vault';
_93
import { createVault } from './vault-factory';
_93
import { Session } from '../models/Session';
_93
import { setState } from './session-store';
_93
_93
export type UnlockMode =
_93
'BiometricsWithPasscode' |
_93
'InMemory' |
_93
'SecureStorage' |
_93
'CustomPasscode';
_93
_93
const vault: Vault | BrowserVault = createVault();
_93
_93
export 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
_93
export const storeSession = async (session: Session): Promise<void> => {
_93
vault.setValue('session', session);
_93
setState({ session });
_93
};
_93
_93
export 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
_93
export const clearSession = async (): Promise<void> => {
_93
await vault.clear();
_93
setState({ session: null });
_93
};
_93
_93
export 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
_93
export 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
_93
export 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:

  1. Create the passcode that is used to lock the vault.
  2. Obtain the passcode that is used to attempt to unlock the vault.

The basic elements for all workflows is:

  1. Respond to the onPasscodeRequested event.
  2. Gather the passcode.
  3. 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:

  1. The user enters a PIN and taps the "Enter" button.
  2. The user is asked to enter the PIN again to verify.
  3. The user enters a PIN and taps the "Enter" button.
  4. If the PINs do not match an error is displayed.
  5. If the PINs match the modal is closed, passing back the PIN that was entered.
  6. 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:

  1. The user enters a PIN and taps the "Enter" button.
  2. The modal is closed, passing back the PIN that was entered.
  3. 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.

src/components/PinDialog.tsx
src/components/PinDialog.css

_155
import {
_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';
_155
import { useRef, useState } from 'react';
_155
import { backspace } from 'ionicons/icons';
_155
import './PinDialog.css';
_155
_155
type PinDialogProperties = {
_155
setPasscodeMode: boolean;
_155
onDismiss: (data: string | null) => void;
_155
};
_155
_155
const 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
_155
export default PinDialog;

Cancel and Submit buttons.

Do not allow the user to cancel if they are creating a passcode.

src/components/PinDialog.tsx
src/components/PinDialog.css

_155
import {
_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';
_155
import { useRef, useState } from 'react';
_155
import { backspace } from 'ionicons/icons';
_155
import './PinDialog.css';
_155
_155
type PinDialogProperties = {
_155
setPasscodeMode: boolean;
_155
onDismiss: (data: string | null) => void;
_155
};
_155
_155
const 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
_155
export default PinDialog;

A user feedback area so they know what is going on.

src/components/PinDialog.tsx
src/components/PinDialog.css

_155
import {
_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';
_155
import { useRef, useState } from 'react';
_155
import { backspace } from 'ionicons/icons';
_155
import './PinDialog.css';
_155
_155
type PinDialogProperties = {
_155
setPasscodeMode: boolean;
_155
onDismiss: (data: string | null) => void;
_155
};
_155
_155
const 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
_155
export default PinDialog;

The keypad.

src/components/PinDialog.tsx
src/components/PinDialog.css

_155
import {
_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';
_155
import { useRef, useState } from 'react';
_155
import { backspace } from 'ionicons/icons';
_155
import './PinDialog.css';
_155
_155
type PinDialogProperties = {
_155
setPasscodeMode: boolean;
_155
onDismiss: (data: string | null) => void;
_155
};
_155
_155
const 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
_155
export default PinDialog;

We do not want to display the PIN, so we calculate a string of asterisks instead.

src/components/PinDialog.tsx
src/components/PinDialog.css

_155
import {
_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';
_155
import { useRef, useState } from 'react';
_155
import { backspace } from 'ionicons/icons';
_155
import './PinDialog.css';
_155
_155
type PinDialogProperties = {
_155
setPasscodeMode: boolean;
_155
onDismiss: (data: string | null) => void;
_155
};
_155
_155
const 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
_155
export default PinDialog;

When the modal opens, the user is either creating a PIN or entering a PIN to unlock the vault.

src/components/PinDialog.tsx
src/components/PinDialog.css

_155
import {
_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';
_155
import { useRef, useState } from 'react';
_155
import { backspace } from 'ionicons/icons';
_155
import './PinDialog.css';
_155
_155
type PinDialogProperties = {
_155
setPasscodeMode: boolean;
_155
onDismiss: (data: string | null) => void;
_155
};
_155
_155
const 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
_155
export 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.

src/components/PinDialog.tsx
src/components/PinDialog.css

_155
import {
_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';
_155
import { useRef, useState } from 'react';
_155
import { backspace } from 'ionicons/icons';
_155
import './PinDialog.css';
_155
_155
type PinDialogProperties = {
_155
setPasscodeMode: boolean;
_155
onDismiss: (data: string | null) => void;
_155
};
_155
_155
const 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
_155
export default PinDialog;

We need to update the PIN value when the user presses a number button or the delete button on the keypad.

src/components/PinDialog.tsx
src/components/PinDialog.css

_155
import {
_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';
_155
import { useRef, useState } from 'react';
_155
import { backspace } from 'ionicons/icons';
_155
import './PinDialog.css';
_155
_155
type PinDialogProperties = {
_155
setPasscodeMode: boolean;
_155
onDismiss: (data: string | null) => void;
_155
};
_155
_155
const 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
_155
export default PinDialog;

When entering a PIN to unlock the vault, close the modal returning the entered PIN.

src/components/PinDialog.tsx
src/components/PinDialog.css

_155
import {
_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';
_155
import { useRef, useState } from 'react';
_155
import { backspace } from 'ionicons/icons';
_155
import './PinDialog.css';
_155
_155
type PinDialogProperties = {
_155
setPasscodeMode: boolean;
_155
onDismiss: (data: string | null) => void;
_155
};
_155
_155
const 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
_155
export 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.

src/components/PinDialog.tsx
src/components/PinDialog.css

_155
import {
_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';
_155
import { useRef, useState } from 'react';
_155
import { backspace } from 'ionicons/icons';
_155
import './PinDialog.css';
_155
_155
type PinDialogProperties = {
_155
setPasscodeMode: boolean;
_155
onDismiss: (data: string | null) => void;
_155
};
_155
_155
const 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
_155
export default PinDialog;

When the user taps the Enter button, we need to handle the proper flow.

src/components/PinDialog.tsx
src/components/PinDialog.css

_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.

src/components/PinDialog.tsx
src/components/PinDialog.css

_155
import {
_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';
_155
import { useRef, useState } from 'react';
_155
import { backspace } from 'ionicons/icons';
_155
import './PinDialog.css';
_155
_155
type PinDialogProperties = {
_155
setPasscodeMode: boolean;
_155
onDismiss: (data: string | null) => void;
_155
};
_155
_155
const 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
_155
export 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.

src/util/session-vault.ts
src/App.tsx

_104
import {
_104
BrowserVault,
_104
Vault,
_104
VaultType,
_104
DeviceSecurityType,
_104
IdentityVaultConfig,
_104
} from '@ionic-enterprise/identity-vault';
_104
import { createVault } from './vault-factory';
_104
import { Session } from '../models/Session';
_104
import { setState } from './session-store';
_104
_104
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage' | 'CustomPasscode';
_104
_104
const vault: Vault | BrowserVault = createVault();
_104
_104
export 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
_104
export const addOnPasscodeRequested = (
_104
callback: (isPasscodeSetRequest: boolean, onComplete: (code: string) => void) => void,
_104
) => {
_104
vault.onPasscodeRequested(callback);
_104
};
_104
_104
export const removeOnPasscodeRequested = () => {
_104
vault.onPasscodeRequested(() => {});
_104
};
_104
_104
export const storeSession = async (session: Session): Promise<void> => {
_104
vault.setValue('session', session);
_104
setState({ session });
_104
};
_104
_104
export 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
_104
export const clearSession = async (): Promise<void> => {
_104
await vault.clear();
_104
setState({ session: null });
_104
};
_104
_104
export 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
_104
export 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
_104
export const lockSession = async (): Promise<void> => {
_104
await vault.lock();
_104
setState({ session: null });
_104
};

We previously created the hooks into onPasscodeRequested() that we need.

src/util/session-vault.ts
src/App.tsx

_60
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
_60
import { IonReactRouter } from '@ionic/react-router';
_60
import { useRef, useState } from 'react';
_60
import { Route } from 'react-router-dom';
_60
import Tabs from './components/Tabs';
_60
import Login from './pages/Login';
_60
import Start from './pages/Start';
_60
import Unlock from './pages/Unlock';
_60
_60
/* Core CSS required for Ionic components to work properly */
_60
import '@ionic/react/css/core.css';
_60
_60
/* Basic CSS for apps built with Ionic */
_60
import '@ionic/react/css/normalize.css';
_60
import '@ionic/react/css/structure.css';
_60
import '@ionic/react/css/typography.css';
_60
_60
/* Optional CSS utils that can be commented out */
_60
import '@ionic/react/css/display.css';
_60
import '@ionic/react/css/flex-utils.css';
_60
import '@ionic/react/css/float-elements.css';
_60
import '@ionic/react/css/padding.css';
_60
import '@ionic/react/css/text-alignment.css';
_60
import '@ionic/react/css/text-transformation.css';
_60
_60
import '@ionic/react/css/palettes/dark.system.css';
_60
_60
/* Theme variables */
_60
import './theme/variables.css';
_60
_60
setupIonicReact();
_60
_60
const 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
_60
export default App;

Set up the state required to open and close the PIN dialog as well as handle the data passing.

src/util/session-vault.ts
src/App.tsx

_73
import { IonApp, IonModal, IonRouterOutlet, setupIonicReact } from '@ionic/react';
_73
import { IonReactRouter } from '@ionic/react-router';
_73
import { useRef, useState } from 'react';
_73
import { Route } from 'react-router-dom';
_73
import PinDialog from './components/PinDialog';
_73
import Tabs from './components/Tabs';
_73
import Login from './pages/Login';
_73
import Start from './pages/Start';
_73
import Unlock from './pages/Unlock';
_73
_73
/* Core CSS required for Ionic components to work properly */
_73
import '@ionic/react/css/core.css';
_73
_73
/* Basic CSS for apps built with Ionic */
_73
import '@ionic/react/css/normalize.css';
_73
import '@ionic/react/css/structure.css';
_73
import '@ionic/react/css/typography.css';
_73
_73
/* Optional CSS utils that can be commented out */
_73
import '@ionic/react/css/display.css';
_73
import '@ionic/react/css/flex-utils.css';
_73
import '@ionic/react/css/float-elements.css';
_73
import '@ionic/react/css/padding.css';
_73
import '@ionic/react/css/text-alignment.css';
_73
import '@ionic/react/css/text-transformation.css';
_73
_73
import '@ionic/react/css/palettes/dark.system.css';
_73
_73
/* Theme variables */
_73
import './theme/variables.css';
_73
_73
setupIonicReact();
_73
_73
const 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
_73
export default App;

Include the PIN dialog in the application, displayed in a modal.

src/util/session-vault.ts
src/App.tsx

_84
import { IonApp, IonModal, IonRouterOutlet, setupIonicReact } from '@ionic/react';
_84
import { IonReactRouter } from '@ionic/react-router';
_84
import { useEffect, useRef, useState } from 'react';
_84
import { Route } from 'react-router-dom';
_84
import PinDialog from './components/PinDialog';
_84
import Tabs from './components/Tabs';
_84
import Login from './pages/Login';
_84
import Start from './pages/Start';
_84
import Unlock from './pages/Unlock';
_84
import { addOnPasscodeRequested, removeOnPasscodeRequested } from './util/session-vault';
_84
_84
/* Core CSS required for Ionic components to work properly */
_84
import '@ionic/react/css/core.css';
_84
_84
/* Basic CSS for apps built with Ionic */
_84
import '@ionic/react/css/normalize.css';
_84
import '@ionic/react/css/structure.css';
_84
import '@ionic/react/css/typography.css';
_84
_84
/* Optional CSS utils that can be commented out */
_84
import '@ionic/react/css/display.css';
_84
import '@ionic/react/css/flex-utils.css';
_84
import '@ionic/react/css/float-elements.css';
_84
import '@ionic/react/css/padding.css';
_84
import '@ionic/react/css/text-alignment.css';
_84
import '@ionic/react/css/text-transformation.css';
_84
_84
import '@ionic/react/css/palettes/dark.system.css';
_84
_84
/* Theme variables */
_84
import './theme/variables.css';
_84
_84
setupIonicReact();
_84
_84
const 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
_84
export 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.

src/util/session-vault.ts
src/App.tsx

_104
import {
_104
BrowserVault,
_104
Vault,
_104
VaultType,
_104
DeviceSecurityType,
_104
IdentityVaultConfig,
_104
} from '@ionic-enterprise/identity-vault';
_104
import { createVault } from './vault-factory';
_104
import { Session } from '../models/Session';
_104
import { setState } from './session-store';
_104
_104
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage' | 'CustomPasscode';
_104
_104
const vault: Vault | BrowserVault = createVault();
_104
_104
export 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
_104
export const addOnPasscodeRequested = (
_104
callback: (isPasscodeSetRequest: boolean, onComplete: (code: string) => void) => void,
_104
) => {
_104
vault.onPasscodeRequested(callback);
_104
};
_104
_104
export const removeOnPasscodeRequested = () => {
_104
vault.onPasscodeRequested(() => {});
_104
};
_104
_104
export const storeSession = async (session: Session): Promise<void> => {
_104
vault.setValue('session', session);
_104
setState({ session });
_104
};
_104
_104
export 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
_104
export const clearSession = async (): Promise<void> => {
_104
await vault.clear();
_104
setState({ session: null });
_104
};
_104
_104
export 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
_104
export 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
_104
export 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:

  1. User uses Biometrics to unlock the Device vault.
  2. The Passcode is obtained from the unlocked Device vault.
  3. 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:

  1. Use a Device type vault with DeviceSecurityType.Both. If the user does not have Biometrics configured, this configuration will fall back to using the System Passcode.
  2. 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:

  1. 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.
  2. Using the System Passcode is built in to Identity Vault. As such, it requires far less code in your application.
  3. 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.