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/app/core/session-vault.service.ts
src/app/tab1/tab1.page.html

_94
import { Injectable } from '@angular/core';
_94
import {
_94
BrowserVault,
_94
DeviceSecurityType,
_94
IdentityVaultConfig,
_94
Vault,
_94
VaultType,
_94
} from '@ionic-enterprise/identity-vault';
_94
import { Session } from '../models/session';
_94
import { VaultFactory } from './vault.factory';
_94
import { Observable, Subject } from 'rxjs';
_94
_94
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_94
_94
@Injectable({
_94
providedIn: 'root',
_94
})
_94
export class SessionVaultService {
_94
private lockedSubject: Subject<boolean>;
_94
private vault: BrowserVault | Vault;
_94
_94
constructor() {
_94
this.vault = VaultFactory.create();
_94
this.lockedSubject = new Subject<boolean>();
_94
}
_94
_94
get locked$(): Observable<boolean> {
_94
return this.lockedSubject.asObservable();
_94
}
_94
_94
async initialize(): Promise<void> {
_94
try {
_94
await this.vault.initialize({
_94
key: 'io.ionic.gettingstartediv',
_94
type: VaultType.SecureStorage,
_94
deviceSecurityType: DeviceSecurityType.None,
_94
lockAfterBackgrounded: 2000,
_94
});
_94
} catch (e: unknown) {
_94
await this.vault.clear();
_94
await this.updateUnlockMode('SecureStorage');
_94
}
_94
_94
this.vault.onLock(() => this.lockedSubject.next(true));
_94
this.vault.onUnlock(() => this.lockedSubject.next(false));
_94
}
_94
_94
async storeSession(session: Session): Promise<void> {
_94
this.vault.setValue('session', session);
_94
}
_94
_94
async getSession(): Promise<Session | null> {
_94
if (await this.vault.isEmpty()) {
_94
return null;
_94
}
_94
return this.vault.getValue<Session>('session');
_94
}
_94
_94
async clearSession(): Promise<void> {
_94
await this.vault.clear();
_94
}
_94
_94
async lock(): Promise<void> {
_94
await this.vault.lock();
_94
}
_94
_94
async unlock(): Promise<void> {
_94
await this.vault.unlock();
_94
}
_94
_94
async isLocked(): Promise<boolean> {
_94
return (
_94
this.vault.config?.type !== VaultType.SecureStorage &&
_94
this.vault.config?.type !== VaultType.InMemory &&
_94
!(await this.vault.isEmpty()) &&
_94
(await this.vault.isLocked())
_94
);
_94
}
_94
_94
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_94
const type =
_94
mode === 'BiometricsWithPasscode'
_94
? VaultType.DeviceSecurity
_94
: mode === 'InMemory'
_94
? VaultType.InMemory
_94
: VaultType.SecureStorage;
_94
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_94
await this.vault.updateConfig({
_94
...(this.vault.config as IdentityVaultConfig),
_94
type,
_94
deviceSecurityType,
_94
});
_94
}
_94
}

Add CustomPasscode to the UnlockMode.

src/app/core/session-vault.service.ts
src/app/tab1/tab1.page.html

_96
import { Injectable } from '@angular/core';
_96
import {
_96
BrowserVault,
_96
DeviceSecurityType,
_96
IdentityVaultConfig,
_96
Vault,
_96
VaultType,
_96
} from '@ionic-enterprise/identity-vault';
_96
import { Session } from '../models/session';
_96
import { VaultFactory } from './vault.factory';
_96
import { Observable, Subject } from 'rxjs';
_96
_96
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_96
_96
@Injectable({
_96
providedIn: 'root',
_96
})
_96
export class SessionVaultService {
_96
private lockedSubject: Subject<boolean>;
_96
private vault: BrowserVault | Vault;
_96
_96
constructor() {
_96
this.vault = VaultFactory.create();
_96
this.lockedSubject = new Subject<boolean>();
_96
}
_96
_96
get locked$(): Observable<boolean> {
_96
return this.lockedSubject.asObservable();
_96
}
_96
_96
async initialize(): Promise<void> {
_96
try {
_96
await this.vault.initialize({
_96
key: 'io.ionic.gettingstartediv',
_96
type: VaultType.SecureStorage,
_96
deviceSecurityType: DeviceSecurityType.None,
_96
lockAfterBackgrounded: 2000,
_96
});
_96
} catch (e: unknown) {
_96
await this.vault.clear();
_96
await this.updateUnlockMode('SecureStorage');
_96
}
_96
_96
this.vault.onLock(() => this.lockedSubject.next(true));
_96
this.vault.onUnlock(() => this.lockedSubject.next(false));
_96
}
_96
_96
async storeSession(session: Session): Promise<void> {
_96
this.vault.setValue('session', session);
_96
}
_96
_96
async getSession(): Promise<Session | null> {
_96
if (await this.vault.isEmpty()) {
_96
return null;
_96
}
_96
return this.vault.getValue<Session>('session');
_96
}
_96
_96
async clearSession(): Promise<void> {
_96
await this.vault.clear();
_96
}
_96
_96
async lock(): Promise<void> {
_96
await this.vault.lock();
_96
}
_96
_96
async unlock(): Promise<void> {
_96
await this.vault.unlock();
_96
}
_96
_96
async isLocked(): Promise<boolean> {
_96
return (
_96
this.vault.config?.type !== VaultType.SecureStorage &&
_96
this.vault.config?.type !== VaultType.InMemory &&
_96
!(await this.vault.isEmpty()) &&
_96
(await this.vault.isLocked())
_96
);
_96
}
_96
_96
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_96
const type =
_96
mode === 'BiometricsWithPasscode'
_96
? VaultType.DeviceSecurity
_96
: mode === 'CustomPasscode'
_96
? VaultType.CustomPasscode
_96
: mode === 'InMemory'
_96
? VaultType.InMemory
_96
: VaultType.SecureStorage;
_96
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_96
await this.vault.updateConfig({
_96
...(this.vault.config as IdentityVaultConfig),
_96
type,
_96
deviceSecurityType,
_96
});
_96
}
_96
}

Update the calculation that determines the type for the vault.

src/app/core/session-vault.service.ts
src/app/tab1/tab1.page.html

_100
import { Injectable } from '@angular/core';
_100
import {
_100
BrowserVault,
_100
DeviceSecurityType,
_100
IdentityVaultConfig,
_100
Vault,
_100
VaultType,
_100
} from '@ionic-enterprise/identity-vault';
_100
import { Session } from '../models/session';
_100
import { VaultFactory } from './vault.factory';
_100
import { Observable, Subject } from 'rxjs';
_100
_100
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_100
_100
@Injectable({
_100
providedIn: 'root',
_100
})
_100
export class SessionVaultService {
_100
private lockedSubject: Subject<boolean>;
_100
private vault: BrowserVault | Vault;
_100
_100
constructor() {
_100
this.vault = VaultFactory.create();
_100
this.lockedSubject = new Subject<boolean>();
_100
}
_100
_100
get locked$(): Observable<boolean> {
_100
return this.lockedSubject.asObservable();
_100
}
_100
_100
async initialize(): Promise<void> {
_100
try {
_100
await this.vault.initialize({
_100
key: 'io.ionic.gettingstartediv',
_100
type: VaultType.SecureStorage,
_100
deviceSecurityType: DeviceSecurityType.None,
_100
lockAfterBackgrounded: 2000,
_100
});
_100
} catch (e: unknown) {
_100
await this.vault.clear();
_100
await this.updateUnlockMode('SecureStorage');
_100
}
_100
_100
this.vault.onLock(() => this.lockedSubject.next(true));
_100
this.vault.onUnlock(() => this.lockedSubject.next(false));
_100
_100
this.vault.onPasscodeRequested(async (isPasscodeSetRequest: boolean) => {
_100
await this.vault.setCustomPasscode('1234');
_100
});
_100
}
_100
_100
async storeSession(session: Session): Promise<void> {
_100
this.vault.setValue('session', session);
_100
}
_100
_100
async getSession(): Promise<Session | null> {
_100
if (await this.vault.isEmpty()) {
_100
return null;
_100
}
_100
return this.vault.getValue<Session>('session');
_100
}
_100
_100
async clearSession(): Promise<void> {
_100
await this.vault.clear();
_100
}
_100
_100
async lock(): Promise<void> {
_100
await this.vault.lock();
_100
}
_100
_100
async unlock(): Promise<void> {
_100
await this.vault.unlock();
_100
}
_100
_100
async isLocked(): Promise<boolean> {
_100
return (
_100
this.vault.config?.type !== VaultType.SecureStorage &&
_100
this.vault.config?.type !== VaultType.InMemory &&
_100
!(await this.vault.isEmpty()) &&
_100
(await this.vault.isLocked())
_100
);
_100
}
_100
_100
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_100
const type =
_100
mode === 'BiometricsWithPasscode'
_100
? VaultType.DeviceSecurity
_100
: mode === 'CustomPasscode'
_100
? VaultType.CustomPasscode
_100
: mode === 'InMemory'
_100
? VaultType.InMemory
_100
: VaultType.SecureStorage;
_100
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_100
await this.vault.updateConfig({
_100
...(this.vault.config as IdentityVaultConfig),
_100
type,
_100
deviceSecurityType,
_100
});
_100
}
_100
}

We need to handle the workflow when the vault needs to request a custom passcode. Return a hard-coded value for now.

src/app/core/session-vault.service.ts
src/app/tab1/tab1.page.html

_60
<ion-header [translucent]="true">
_60
<ion-toolbar>
_60
<ion-title> Tab1 </ion-title>
_60
</ion-toolbar>
_60
</ion-header>
_60
_60
<ion-content [fullscreen]="true">
_60
<ion-header collapse="condense">
_60
<ion-toolbar>
_60
<ion-title size="large">Tab1</ion-title>
_60
</ion-toolbar>
_60
</ion-header>
_60
_60
<ion-list>
_60
<ion-item>
_60
<ion-label>
_60
<ion-button expand="block" color="danger" (click)="logout()">Logout</ion-button>
_60
</ion-label>
_60
</ion-item>
_60
<ion-item>
_60
<ion-label>
_60
<ion-button expand="block" color="secondary" (click)="changeUnlockMode('BiometricsWithPasscode')"
_60
>Use Biometrics</ion-button
_60
>
_60
</ion-label>
_60
</ion-item>
_60
<ion-item>
_60
<ion-label>
_60
<ion-button expand="block" color="secondary" (click)="changeUnlockMode('CustomPasscode')"
_60
>Use Custom Passcode</ion-button
_60
>
_60
</ion-label>
_60
</ion-item>
_60
<ion-item>
_60
<ion-label>
_60
<ion-button expand="block" color="secondary" (click)="changeUnlockMode('InMemory')">Use In Memory</ion-button>
_60
</ion-label>
_60
</ion-item>
_60
<ion-item>
_60
<ion-label>
_60
<ion-button expand="block" color="secondary" (click)="changeUnlockMode('SecureStorage')"
_60
>Use Secure Storage</ion-button
_60
>
_60
</ion-label>
_60
</ion-item>
_60
<ion-item>
_60
<ion-label>
_60
<ion-button expand="block" color="warning" (click)="lock()">Lock</ion-button>
_60
</ion-label>
_60
</ion-item>
_60
<ion-item>
_60
<div>
_60
<div>{{session?.email}}</div>
_60
<div>{{session?.firstName}} {{session?.lastName}}</div>
_60
<div>{{session?.accessToken}}</div>
_60
<div>{{session?.refreshToken}}</div>
_60
</div>
_60
</ion-item>
_60
</ion-list>
_60
</ion-content>

Create a button on Tab1Page to use the custom passcode.

Add CustomPasscode to the UnlockMode.

Update the calculation that determines the type for the vault.

We need to handle the workflow when the vault needs to request a custom passcode. Return a hard-coded value for now.

Create a button on Tab1Page to use the custom passcode.

src/app/core/session-vault.service.ts
src/app/tab1/tab1.page.html

_94
import { Injectable } from '@angular/core';
_94
import {
_94
BrowserVault,
_94
DeviceSecurityType,
_94
IdentityVaultConfig,
_94
Vault,
_94
VaultType,
_94
} from '@ionic-enterprise/identity-vault';
_94
import { Session } from '../models/session';
_94
import { VaultFactory } from './vault.factory';
_94
import { Observable, Subject } from 'rxjs';
_94
_94
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_94
_94
@Injectable({
_94
providedIn: 'root',
_94
})
_94
export class SessionVaultService {
_94
private lockedSubject: Subject<boolean>;
_94
private vault: BrowserVault | Vault;
_94
_94
constructor() {
_94
this.vault = VaultFactory.create();
_94
this.lockedSubject = new Subject<boolean>();
_94
}
_94
_94
get locked$(): Observable<boolean> {
_94
return this.lockedSubject.asObservable();
_94
}
_94
_94
async initialize(): Promise<void> {
_94
try {
_94
await this.vault.initialize({
_94
key: 'io.ionic.gettingstartediv',
_94
type: VaultType.SecureStorage,
_94
deviceSecurityType: DeviceSecurityType.None,
_94
lockAfterBackgrounded: 2000,
_94
});
_94
} catch (e: unknown) {
_94
await this.vault.clear();
_94
await this.updateUnlockMode('SecureStorage');
_94
}
_94
_94
this.vault.onLock(() => this.lockedSubject.next(true));
_94
this.vault.onUnlock(() => this.lockedSubject.next(false));
_94
}
_94
_94
async storeSession(session: Session): Promise<void> {
_94
this.vault.setValue('session', session);
_94
}
_94
_94
async getSession(): Promise<Session | null> {
_94
if (await this.vault.isEmpty()) {
_94
return null;
_94
}
_94
return this.vault.getValue<Session>('session');
_94
}
_94
_94
async clearSession(): Promise<void> {
_94
await this.vault.clear();
_94
}
_94
_94
async lock(): Promise<void> {
_94
await this.vault.lock();
_94
}
_94
_94
async unlock(): Promise<void> {
_94
await this.vault.unlock();
_94
}
_94
_94
async isLocked(): Promise<boolean> {
_94
return (
_94
this.vault.config?.type !== VaultType.SecureStorage &&
_94
this.vault.config?.type !== VaultType.InMemory &&
_94
!(await this.vault.isEmpty()) &&
_94
(await this.vault.isLocked())
_94
);
_94
}
_94
_94
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_94
const type =
_94
mode === 'BiometricsWithPasscode'
_94
? VaultType.DeviceSecurity
_94
: mode === 'InMemory'
_94
? VaultType.InMemory
_94
: VaultType.SecureStorage;
_94
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_94
await this.vault.updateConfig({
_94
...(this.vault.config as IdentityVaultConfig),
_94
type,
_94
deviceSecurityType,
_94
});
_94
}
_94
}

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 a key to lock the vault.

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/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_99
<ion-header>
_99
<ion-toolbar>
_99
<ion-title>{{ title }}</ion-title>
_99
@if (!setPasscodeMode) {
_99
<ion-buttons slot="start">
_99
<ion-button (click)="cancel()" data-testid="cancel-button"> Cancel </ion-button>
_99
</ion-buttons>
_99
}
_99
<ion-buttons slot="end">
_99
<ion-button [strong]="true" data-testid="submit-button" (click)="submit()" [disabled]="disableEnter"
_99
>Enter
_99
</ion-button>
_99
</ion-buttons>
_99
</ion-toolbar>
_99
</ion-header>
_99
_99
<ion-content class="ion-padding ion-text-center">
_99
<ion-label data-testid="prompt">
_99
<div class="prompt">{{ prompt }}</div>
_99
</ion-label>
_99
<ion-label data-testid="display-pin">
_99
<div class="pin">{{ displayPin }}</div>
_99
</ion-label>
_99
<ion-label color="danger" data-testid="error-message">
_99
<div class="error">{{ errorMessage }}</div>
_99
</ion-label>
_99
</ion-content>
_99
_99
<ion-footer>
_99
<ion-grid>
_99
<ion-row>
_99
@for (n of [1, 2, 3]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
@for (n of [4, 5, 6]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
@for (n of [7, 8, 9]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
<ion-col> </ion-col>
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(0)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>0</ion-button
_99
>
_99
</ion-col>
_99
<ion-col>
_99
<ion-button
_99
icon-only
_99
color="tertiary"
_99
expand="block"
_99
(click)="remove()"
_99
[disabled]="disableDelete"
_99
data-testid="delete-button"
_99
>
_99
<ion-icon name="backspace"></ion-icon>
_99
</ion-button>
_99
</ion-col>
_99
</ion-row>
_99
</ion-grid>
_99
</ion-footer>

Cancel and Submit buttons.

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

src/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_99
<ion-header>
_99
<ion-toolbar>
_99
<ion-title>{{ title }}</ion-title>
_99
@if (!setPasscodeMode) {
_99
<ion-buttons slot="start">
_99
<ion-button (click)="cancel()" data-testid="cancel-button"> Cancel </ion-button>
_99
</ion-buttons>
_99
}
_99
<ion-buttons slot="end">
_99
<ion-button [strong]="true" data-testid="submit-button" (click)="submit()" [disabled]="disableEnter"
_99
>Enter
_99
</ion-button>
_99
</ion-buttons>
_99
</ion-toolbar>
_99
</ion-header>
_99
_99
<ion-content class="ion-padding ion-text-center">
_99
<ion-label data-testid="prompt">
_99
<div class="prompt">{{ prompt }}</div>
_99
</ion-label>
_99
<ion-label data-testid="display-pin">
_99
<div class="pin">{{ displayPin }}</div>
_99
</ion-label>
_99
<ion-label color="danger" data-testid="error-message">
_99
<div class="error">{{ errorMessage }}</div>
_99
</ion-label>
_99
</ion-content>
_99
_99
<ion-footer>
_99
<ion-grid>
_99
<ion-row>
_99
@for (n of [1, 2, 3]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
@for (n of [4, 5, 6]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
@for (n of [7, 8, 9]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
<ion-col> </ion-col>
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(0)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>0</ion-button
_99
>
_99
</ion-col>
_99
<ion-col>
_99
<ion-button
_99
icon-only
_99
color="tertiary"
_99
expand="block"
_99
(click)="remove()"
_99
[disabled]="disableDelete"
_99
data-testid="delete-button"
_99
>
_99
<ion-icon name="backspace"></ion-icon>
_99
</ion-button>
_99
</ion-col>
_99
</ion-row>
_99
</ion-grid>
_99
</ion-footer>

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

src/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_99
<ion-header>
_99
<ion-toolbar>
_99
<ion-title>{{ title }}</ion-title>
_99
@if (!setPasscodeMode) {
_99
<ion-buttons slot="start">
_99
<ion-button (click)="cancel()" data-testid="cancel-button"> Cancel </ion-button>
_99
</ion-buttons>
_99
}
_99
<ion-buttons slot="end">
_99
<ion-button [strong]="true" data-testid="submit-button" (click)="submit()" [disabled]="disableEnter"
_99
>Enter
_99
</ion-button>
_99
</ion-buttons>
_99
</ion-toolbar>
_99
</ion-header>
_99
_99
<ion-content class="ion-padding ion-text-center">
_99
<ion-label data-testid="prompt">
_99
<div class="prompt">{{ prompt }}</div>
_99
</ion-label>
_99
<ion-label data-testid="display-pin">
_99
<div class="pin">{{ displayPin }}</div>
_99
</ion-label>
_99
<ion-label color="danger" data-testid="error-message">
_99
<div class="error">{{ errorMessage }}</div>
_99
</ion-label>
_99
</ion-content>
_99
_99
<ion-footer>
_99
<ion-grid>
_99
<ion-row>
_99
@for (n of [1, 2, 3]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
@for (n of [4, 5, 6]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
@for (n of [7, 8, 9]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
<ion-col> </ion-col>
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(0)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>0</ion-button
_99
>
_99
</ion-col>
_99
<ion-col>
_99
<ion-button
_99
icon-only
_99
color="tertiary"
_99
expand="block"
_99
(click)="remove()"
_99
[disabled]="disableDelete"
_99
data-testid="delete-button"
_99
>
_99
<ion-icon name="backspace"></ion-icon>
_99
</ion-button>
_99
</ion-col>
_99
</ion-row>
_99
</ion-grid>
_99
</ion-footer>

The keypad.

src/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_140
import { Component, Input, OnInit } from '@angular/core';
_140
import {
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
ModalController,
_140
} from '@ionic/angular/standalone';
_140
import { addIcons } from 'ionicons';
_140
import { backspace } from 'ionicons/icons';
_140
_140
@Component({
_140
selector: 'app-pin-dialog',
_140
templateUrl: './pin-dialog.component.html',
_140
styleUrls: ['./pin-dialog.component.scss'],
_140
imports: [
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
],
_140
standalone: true,
_140
})
_140
export class PinDialogComponent implements OnInit {
_140
@Input() setPasscodeMode: boolean = false;
_140
_140
displayPin: string = '';
_140
errorMessage: string = '';
_140
pin: string = '';
_140
prompt: string = '';
_140
title: string = '';
_140
_140
private verifyPin: string = '';
_140
_140
constructor(private modalController: ModalController) {
_140
addIcons({ backspace });
_140
}
_140
_140
get disableEnter(): boolean {
_140
return !(this.pin.length > 2);
_140
}
_140
_140
get disableDelete(): boolean {
_140
return !this.pin.length;
_140
}
_140
_140
get disableInput(): boolean {
_140
return !!(this.pin.length > 8);
_140
}
_140
_140
ngOnInit() {
_140
if (this.setPasscodeMode) {
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.initUnlockMode();
_140
}
_140
}
_140
_140
append(n: number) {
_140
this.errorMessage = '';
_140
this.pin = this.pin.concat(n.toString());
_140
this.setDisplayPin();
_140
}
_140
_140
remove() {
_140
if (this.pin) {
_140
this.pin = this.pin.slice(0, this.pin.length - 1);
_140
}
_140
this.setDisplayPin();
_140
}
_140
_140
cancel() {
_140
this.modalController.dismiss(undefined, 'cancel');
_140
}
_140
_140
submit() {
_140
if (this.setPasscodeMode) {
_140
this.handleSetPasscodeFlow();
_140
} else {
_140
this.handleGetPasscodeFlow();
_140
}
_140
}
_140
_140
private handleGetPasscodeFlow() {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
_140
private handleSetPasscodeFlow() {
_140
if (!this.verifyPin) {
_140
this.initVerifyMode();
_140
} else if (this.verifyPin !== this.pin) {
_140
this.errorMessage = 'PINS do not match';
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
}
_140
_140
private initSetPasscodeMode() {
_140
this.prompt = 'Create Session PIN';
_140
this.title = 'Create PIN';
_140
this.verifyPin = '';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initUnlockMode() {
_140
this.prompt = 'Enter PIN to Unlock';
_140
this.title = 'Unlock';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initVerifyMode() {
_140
this.prompt = 'Verify PIN';
_140
this.verifyPin = this.pin;
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private setDisplayPin() {
_140
this.displayPin = '*********'.slice(0, this.pin.length);
_140
}
_140
}

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

src/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_140
import { Component, Input, OnInit } from '@angular/core';
_140
import {
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
ModalController,
_140
} from '@ionic/angular/standalone';
_140
import { addIcons } from 'ionicons';
_140
import { backspace } from 'ionicons/icons';
_140
_140
@Component({
_140
selector: 'app-pin-dialog',
_140
templateUrl: './pin-dialog.component.html',
_140
styleUrls: ['./pin-dialog.component.scss'],
_140
imports: [
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
],
_140
standalone: true,
_140
})
_140
export class PinDialogComponent implements OnInit {
_140
@Input() setPasscodeMode: boolean = false;
_140
_140
displayPin: string = '';
_140
errorMessage: string = '';
_140
pin: string = '';
_140
prompt: string = '';
_140
title: string = '';
_140
_140
private verifyPin: string = '';
_140
_140
constructor(private modalController: ModalController) {
_140
addIcons({ backspace });
_140
}
_140
_140
get disableEnter(): boolean {
_140
return !(this.pin.length > 2);
_140
}
_140
_140
get disableDelete(): boolean {
_140
return !this.pin.length;
_140
}
_140
_140
get disableInput(): boolean {
_140
return !!(this.pin.length > 8);
_140
}
_140
_140
ngOnInit() {
_140
if (this.setPasscodeMode) {
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.initUnlockMode();
_140
}
_140
}
_140
_140
append(n: number) {
_140
this.errorMessage = '';
_140
this.pin = this.pin.concat(n.toString());
_140
this.setDisplayPin();
_140
}
_140
_140
remove() {
_140
if (this.pin) {
_140
this.pin = this.pin.slice(0, this.pin.length - 1);
_140
}
_140
this.setDisplayPin();
_140
}
_140
_140
cancel() {
_140
this.modalController.dismiss(undefined, 'cancel');
_140
}
_140
_140
submit() {
_140
if (this.setPasscodeMode) {
_140
this.handleSetPasscodeFlow();
_140
} else {
_140
this.handleGetPasscodeFlow();
_140
}
_140
}
_140
_140
private handleGetPasscodeFlow() {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
_140
private handleSetPasscodeFlow() {
_140
if (!this.verifyPin) {
_140
this.initVerifyMode();
_140
} else if (this.verifyPin !== this.pin) {
_140
this.errorMessage = 'PINS do not match';
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
}
_140
_140
private initSetPasscodeMode() {
_140
this.prompt = 'Create Session PIN';
_140
this.title = 'Create PIN';
_140
this.verifyPin = '';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initUnlockMode() {
_140
this.prompt = 'Enter PIN to Unlock';
_140
this.title = 'Unlock';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initVerifyMode() {
_140
this.prompt = 'Verify PIN';
_140
this.verifyPin = this.pin;
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private setDisplayPin() {
_140
this.displayPin = '*********'.slice(0, this.pin.length);
_140
}
_140
}

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

src/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_140
import { Component, Input, OnInit } from '@angular/core';
_140
import {
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
ModalController,
_140
} from '@ionic/angular/standalone';
_140
import { addIcons } from 'ionicons';
_140
import { backspace } from 'ionicons/icons';
_140
_140
@Component({
_140
selector: 'app-pin-dialog',
_140
templateUrl: './pin-dialog.component.html',
_140
styleUrls: ['./pin-dialog.component.scss'],
_140
imports: [
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
],
_140
standalone: true,
_140
})
_140
export class PinDialogComponent implements OnInit {
_140
@Input() setPasscodeMode: boolean = false;
_140
_140
displayPin: string = '';
_140
errorMessage: string = '';
_140
pin: string = '';
_140
prompt: string = '';
_140
title: string = '';
_140
_140
private verifyPin: string = '';
_140
_140
constructor(private modalController: ModalController) {
_140
addIcons({ backspace });
_140
}
_140
_140
get disableEnter(): boolean {
_140
return !(this.pin.length > 2);
_140
}
_140
_140
get disableDelete(): boolean {
_140
return !this.pin.length;
_140
}
_140
_140
get disableInput(): boolean {
_140
return !!(this.pin.length > 8);
_140
}
_140
_140
ngOnInit() {
_140
if (this.setPasscodeMode) {
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.initUnlockMode();
_140
}
_140
}
_140
_140
append(n: number) {
_140
this.errorMessage = '';
_140
this.pin = this.pin.concat(n.toString());
_140
this.setDisplayPin();
_140
}
_140
_140
remove() {
_140
if (this.pin) {
_140
this.pin = this.pin.slice(0, this.pin.length - 1);
_140
}
_140
this.setDisplayPin();
_140
}
_140
_140
cancel() {
_140
this.modalController.dismiss(undefined, 'cancel');
_140
}
_140
_140
submit() {
_140
if (this.setPasscodeMode) {
_140
this.handleSetPasscodeFlow();
_140
} else {
_140
this.handleGetPasscodeFlow();
_140
}
_140
}
_140
_140
private handleGetPasscodeFlow() {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
_140
private handleSetPasscodeFlow() {
_140
if (!this.verifyPin) {
_140
this.initVerifyMode();
_140
} else if (this.verifyPin !== this.pin) {
_140
this.errorMessage = 'PINS do not match';
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
}
_140
_140
private initSetPasscodeMode() {
_140
this.prompt = 'Create Session PIN';
_140
this.title = 'Create PIN';
_140
this.verifyPin = '';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initUnlockMode() {
_140
this.prompt = 'Enter PIN to Unlock';
_140
this.title = 'Unlock';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initVerifyMode() {
_140
this.prompt = 'Verify PIN';
_140
this.verifyPin = this.pin;
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private setDisplayPin() {
_140
this.displayPin = '*********'.slice(0, this.pin.length);
_140
}
_140
}

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/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_140
import { Component, Input, OnInit } from '@angular/core';
_140
import {
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
ModalController,
_140
} from '@ionic/angular/standalone';
_140
import { addIcons } from 'ionicons';
_140
import { backspace } from 'ionicons/icons';
_140
_140
@Component({
_140
selector: 'app-pin-dialog',
_140
templateUrl: './pin-dialog.component.html',
_140
styleUrls: ['./pin-dialog.component.scss'],
_140
imports: [
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
],
_140
standalone: true,
_140
})
_140
export class PinDialogComponent implements OnInit {
_140
@Input() setPasscodeMode: boolean = false;
_140
_140
displayPin: string = '';
_140
errorMessage: string = '';
_140
pin: string = '';
_140
prompt: string = '';
_140
title: string = '';
_140
_140
private verifyPin: string = '';
_140
_140
constructor(private modalController: ModalController) {
_140
addIcons({ backspace });
_140
}
_140
_140
get disableEnter(): boolean {
_140
return !(this.pin.length > 2);
_140
}
_140
_140
get disableDelete(): boolean {
_140
return !this.pin.length;
_140
}
_140
_140
get disableInput(): boolean {
_140
return !!(this.pin.length > 8);
_140
}
_140
_140
ngOnInit() {
_140
if (this.setPasscodeMode) {
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.initUnlockMode();
_140
}
_140
}
_140
_140
append(n: number) {
_140
this.errorMessage = '';
_140
this.pin = this.pin.concat(n.toString());
_140
this.setDisplayPin();
_140
}
_140
_140
remove() {
_140
if (this.pin) {
_140
this.pin = this.pin.slice(0, this.pin.length - 1);
_140
}
_140
this.setDisplayPin();
_140
}
_140
_140
cancel() {
_140
this.modalController.dismiss(undefined, 'cancel');
_140
}
_140
_140
submit() {
_140
if (this.setPasscodeMode) {
_140
this.handleSetPasscodeFlow();
_140
} else {
_140
this.handleGetPasscodeFlow();
_140
}
_140
}
_140
_140
private handleGetPasscodeFlow() {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
_140
private handleSetPasscodeFlow() {
_140
if (!this.verifyPin) {
_140
this.initVerifyMode();
_140
} else if (this.verifyPin !== this.pin) {
_140
this.errorMessage = 'PINS do not match';
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
}
_140
_140
private initSetPasscodeMode() {
_140
this.prompt = 'Create Session PIN';
_140
this.title = 'Create PIN';
_140
this.verifyPin = '';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initUnlockMode() {
_140
this.prompt = 'Enter PIN to Unlock';
_140
this.title = 'Unlock';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initVerifyMode() {
_140
this.prompt = 'Verify PIN';
_140
this.verifyPin = this.pin;
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private setDisplayPin() {
_140
this.displayPin = '*********'.slice(0, this.pin.length);
_140
}
_140
}

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

src/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_140
import { Component, Input, OnInit } from '@angular/core';
_140
import {
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
ModalController,
_140
} from '@ionic/angular/standalone';
_140
import { addIcons } from 'ionicons';
_140
import { backspace } from 'ionicons/icons';
_140
_140
@Component({
_140
selector: 'app-pin-dialog',
_140
templateUrl: './pin-dialog.component.html',
_140
styleUrls: ['./pin-dialog.component.scss'],
_140
imports: [
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
],
_140
standalone: true,
_140
})
_140
export class PinDialogComponent implements OnInit {
_140
@Input() setPasscodeMode: boolean = false;
_140
_140
displayPin: string = '';
_140
errorMessage: string = '';
_140
pin: string = '';
_140
prompt: string = '';
_140
title: string = '';
_140
_140
private verifyPin: string = '';
_140
_140
constructor(private modalController: ModalController) {
_140
addIcons({ backspace });
_140
}
_140
_140
get disableEnter(): boolean {
_140
return !(this.pin.length > 2);
_140
}
_140
_140
get disableDelete(): boolean {
_140
return !this.pin.length;
_140
}
_140
_140
get disableInput(): boolean {
_140
return !!(this.pin.length > 8);
_140
}
_140
_140
ngOnInit() {
_140
if (this.setPasscodeMode) {
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.initUnlockMode();
_140
}
_140
}
_140
_140
append(n: number) {
_140
this.errorMessage = '';
_140
this.pin = this.pin.concat(n.toString());
_140
this.setDisplayPin();
_140
}
_140
_140
remove() {
_140
if (this.pin) {
_140
this.pin = this.pin.slice(0, this.pin.length - 1);
_140
}
_140
this.setDisplayPin();
_140
}
_140
_140
cancel() {
_140
this.modalController.dismiss(undefined, 'cancel');
_140
}
_140
_140
submit() {
_140
if (this.setPasscodeMode) {
_140
this.handleSetPasscodeFlow();
_140
} else {
_140
this.handleGetPasscodeFlow();
_140
}
_140
}
_140
_140
private handleGetPasscodeFlow() {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
_140
private handleSetPasscodeFlow() {
_140
if (!this.verifyPin) {
_140
this.initVerifyMode();
_140
} else if (this.verifyPin !== this.pin) {
_140
this.errorMessage = 'PINS do not match';
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
}
_140
_140
private initSetPasscodeMode() {
_140
this.prompt = 'Create Session PIN';
_140
this.title = 'Create PIN';
_140
this.verifyPin = '';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initUnlockMode() {
_140
this.prompt = 'Enter PIN to Unlock';
_140
this.title = 'Unlock';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initVerifyMode() {
_140
this.prompt = 'Verify PIN';
_140
this.verifyPin = this.pin;
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private setDisplayPin() {
_140
this.displayPin = '*********'.slice(0, this.pin.length);
_140
}
_140
}

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

src/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_140
import { Component, Input, OnInit } from '@angular/core';
_140
import {
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
ModalController,
_140
} from '@ionic/angular/standalone';
_140
import { addIcons } from 'ionicons';
_140
import { backspace } from 'ionicons/icons';
_140
_140
@Component({
_140
selector: 'app-pin-dialog',
_140
templateUrl: './pin-dialog.component.html',
_140
styleUrls: ['./pin-dialog.component.scss'],
_140
imports: [
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
],
_140
standalone: true,
_140
})
_140
export class PinDialogComponent implements OnInit {
_140
@Input() setPasscodeMode: boolean = false;
_140
_140
displayPin: string = '';
_140
errorMessage: string = '';
_140
pin: string = '';
_140
prompt: string = '';
_140
title: string = '';
_140
_140
private verifyPin: string = '';
_140
_140
constructor(private modalController: ModalController) {
_140
addIcons({ backspace });
_140
}
_140
_140
get disableEnter(): boolean {
_140
return !(this.pin.length > 2);
_140
}
_140
_140
get disableDelete(): boolean {
_140
return !this.pin.length;
_140
}
_140
_140
get disableInput(): boolean {
_140
return !!(this.pin.length > 8);
_140
}
_140
_140
ngOnInit() {
_140
if (this.setPasscodeMode) {
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.initUnlockMode();
_140
}
_140
}
_140
_140
append(n: number) {
_140
this.errorMessage = '';
_140
this.pin = this.pin.concat(n.toString());
_140
this.setDisplayPin();
_140
}
_140
_140
remove() {
_140
if (this.pin) {
_140
this.pin = this.pin.slice(0, this.pin.length - 1);
_140
}
_140
this.setDisplayPin();
_140
}
_140
_140
cancel() {
_140
this.modalController.dismiss(undefined, 'cancel');
_140
}
_140
_140
submit() {
_140
if (this.setPasscodeMode) {
_140
this.handleSetPasscodeFlow();
_140
} else {
_140
this.handleGetPasscodeFlow();
_140
}
_140
}
_140
_140
private handleGetPasscodeFlow() {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
_140
private handleSetPasscodeFlow() {
_140
if (!this.verifyPin) {
_140
this.initVerifyMode();
_140
} else if (this.verifyPin !== this.pin) {
_140
this.errorMessage = 'PINS do not match';
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
}
_140
_140
private initSetPasscodeMode() {
_140
this.prompt = 'Create Session PIN';
_140
this.title = 'Create PIN';
_140
this.verifyPin = '';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initUnlockMode() {
_140
this.prompt = 'Enter PIN to Unlock';
_140
this.title = 'Unlock';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initVerifyMode() {
_140
this.prompt = 'Verify PIN';
_140
this.verifyPin = this.pin;
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private setDisplayPin() {
_140
this.displayPin = '*********'.slice(0, this.pin.length);
_140
}
_140
}

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/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_140
import { Component, Input, OnInit } from '@angular/core';
_140
import {
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
ModalController,
_140
} from '@ionic/angular/standalone';
_140
import { addIcons } from 'ionicons';
_140
import { backspace } from 'ionicons/icons';
_140
_140
@Component({
_140
selector: 'app-pin-dialog',
_140
templateUrl: './pin-dialog.component.html',
_140
styleUrls: ['./pin-dialog.component.scss'],
_140
imports: [
_140
IonButton,
_140
IonButtons,
_140
IonCol,
_140
IonContent,
_140
IonFooter,
_140
IonGrid,
_140
IonHeader,
_140
IonIcon,
_140
IonLabel,
_140
IonRow,
_140
IonTitle,
_140
IonToolbar,
_140
],
_140
standalone: true,
_140
})
_140
export class PinDialogComponent implements OnInit {
_140
@Input() setPasscodeMode: boolean = false;
_140
_140
displayPin: string = '';
_140
errorMessage: string = '';
_140
pin: string = '';
_140
prompt: string = '';
_140
title: string = '';
_140
_140
private verifyPin: string = '';
_140
_140
constructor(private modalController: ModalController) {
_140
addIcons({ backspace });
_140
}
_140
_140
get disableEnter(): boolean {
_140
return !(this.pin.length > 2);
_140
}
_140
_140
get disableDelete(): boolean {
_140
return !this.pin.length;
_140
}
_140
_140
get disableInput(): boolean {
_140
return !!(this.pin.length > 8);
_140
}
_140
_140
ngOnInit() {
_140
if (this.setPasscodeMode) {
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.initUnlockMode();
_140
}
_140
}
_140
_140
append(n: number) {
_140
this.errorMessage = '';
_140
this.pin = this.pin.concat(n.toString());
_140
this.setDisplayPin();
_140
}
_140
_140
remove() {
_140
if (this.pin) {
_140
this.pin = this.pin.slice(0, this.pin.length - 1);
_140
}
_140
this.setDisplayPin();
_140
}
_140
_140
cancel() {
_140
this.modalController.dismiss(undefined, 'cancel');
_140
}
_140
_140
submit() {
_140
if (this.setPasscodeMode) {
_140
this.handleSetPasscodeFlow();
_140
} else {
_140
this.handleGetPasscodeFlow();
_140
}
_140
}
_140
_140
private handleGetPasscodeFlow() {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
_140
private handleSetPasscodeFlow() {
_140
if (!this.verifyPin) {
_140
this.initVerifyMode();
_140
} else if (this.verifyPin !== this.pin) {
_140
this.errorMessage = 'PINS do not match';
_140
this.initSetPasscodeMode();
_140
} else {
_140
this.modalController.dismiss(this.pin);
_140
}
_140
}
_140
_140
private initSetPasscodeMode() {
_140
this.prompt = 'Create Session PIN';
_140
this.title = 'Create PIN';
_140
this.verifyPin = '';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initUnlockMode() {
_140
this.prompt = 'Enter PIN to Unlock';
_140
this.title = 'Unlock';
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private initVerifyMode() {
_140
this.prompt = 'Verify PIN';
_140
this.verifyPin = this.pin;
_140
this.displayPin = '';
_140
this.pin = '';
_140
}
_140
_140
private setDisplayPin() {
_140
this.displayPin = '*********'.slice(0, this.pin.length);
_140
}
_140
}

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

src/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_18
.prompt {
_18
font-size: 2rem;
_18
font-weight: bold;
_18
}
_18
_18
.pin {
_18
font-size: 3rem;
_18
font-weight: bold;
_18
}
_18
_18
.error {
_18
font-size: 1.5rem;
_18
font-weight: bold;
_18
}
_18
_18
ion-grid {
_18
padding-bottom: 32px;
_18
}

The component needs a little styling to make it all look nice.

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 need to update the PIN value when the user presses a number button or the delete button on the keypad.

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

There are three modes. The user is either creating a PIN, verifying the newly created PIN, or entering a PIN to unlock the vault.

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

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

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 entering a PIN to unlock the vault, close the modal returning the entered PIN.

The component needs a little styling to make it all look nice.

src/app/pin-entry/pin-entry.component.html
src/app/pin-entry/pin-entry.component.ts
src/app/pin-entry/pin-entry.component.scss

_99
<ion-header>
_99
<ion-toolbar>
_99
<ion-title>{{ title }}</ion-title>
_99
@if (!setPasscodeMode) {
_99
<ion-buttons slot="start">
_99
<ion-button (click)="cancel()" data-testid="cancel-button"> Cancel </ion-button>
_99
</ion-buttons>
_99
}
_99
<ion-buttons slot="end">
_99
<ion-button [strong]="true" data-testid="submit-button" (click)="submit()" [disabled]="disableEnter"
_99
>Enter
_99
</ion-button>
_99
</ion-buttons>
_99
</ion-toolbar>
_99
</ion-header>
_99
_99
<ion-content class="ion-padding ion-text-center">
_99
<ion-label data-testid="prompt">
_99
<div class="prompt">{{ prompt }}</div>
_99
</ion-label>
_99
<ion-label data-testid="display-pin">
_99
<div class="pin">{{ displayPin }}</div>
_99
</ion-label>
_99
<ion-label color="danger" data-testid="error-message">
_99
<div class="error">{{ errorMessage }}</div>
_99
</ion-label>
_99
</ion-content>
_99
_99
<ion-footer>
_99
<ion-grid>
_99
<ion-row>
_99
@for (n of [1, 2, 3]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
@for (n of [4, 5, 6]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
@for (n of [7, 8, 9]; track n) {
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(n)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>{{ n }}</ion-button
_99
>
_99
</ion-col>
_99
}
_99
</ion-row>
_99
<ion-row>
_99
<ion-col> </ion-col>
_99
<ion-col>
_99
<ion-button
_99
expand="block"
_99
fill="outline"
_99
(click)="append(0)"
_99
[disabled]="disableInput"
_99
data-testclass="number-button"
_99
>0</ion-button
_99
>
_99
</ion-col>
_99
<ion-col>
_99
<ion-button
_99
icon-only
_99
color="tertiary"
_99
expand="block"
_99
(click)="remove()"
_99
[disabled]="disableDelete"
_99
data-testid="delete-button"
_99
>
_99
<ion-icon name="backspace"></ion-icon>
_99
</ion-button>
_99
</ion-col>
_99
</ion-row>
_99
</ion-grid>
_99
</ion-footer>

Hook Up the Modal

When the vault needs a PIN it will call onPasscodeRequested(), passing a Boolean flag indicating if the passcode is being used to set a new passcode (true) or unlock the vault (false). We use this callback function to display the PIN Entry Component in a modal dialog, await a response from it, and pass that response back to the vault.

src/app/core/session-vault.service.ts

_102
import { Injectable } from '@angular/core';
_102
import {
_102
BrowserVault,
_102
DeviceSecurityType,
_102
IdentityVaultConfig,
_102
Vault,
_102
VaultType,
_102
} from '@ionic-enterprise/identity-vault';
_102
import { Session } from '../models/session';
_102
import { VaultFactory } from './vault.factory';
_102
import { Observable, Subject } from 'rxjs';
_102
import { PinDialogComponent } from '../pin-dialog/pin-dialog.component';
_102
import { ModalController } from '@ionic/angular/standalone';
_102
_102
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_102
_102
@Injectable({
_102
providedIn: 'root',
_102
})
_102
export class SessionVaultService {
_102
private lockedSubject: Subject<boolean>;
_102
private vault: BrowserVault | Vault;
_102
_102
constructor(private modalController: ModalController) {
_102
this.vault = VaultFactory.create();
_102
this.lockedSubject = new Subject<boolean>();
_102
}
_102
_102
get locked$(): Observable<boolean> {
_102
return this.lockedSubject.asObservable();
_102
}
_102
_102
async initialize(): Promise<void> {
_102
try {
_102
await this.vault.initialize({
_102
key: 'io.ionic.gettingstartediv',
_102
type: VaultType.SecureStorage,
_102
deviceSecurityType: DeviceSecurityType.None,
_102
lockAfterBackgrounded: 2000,
_102
});
_102
} catch (e: unknown) {
_102
await this.vault.clear();
_102
await this.updateUnlockMode('SecureStorage');
_102
}
_102
_102
this.vault.onLock(() => this.lockedSubject.next(true));
_102
this.vault.onUnlock(() => this.lockedSubject.next(false));
_102
_102
this.vault.onPasscodeRequested(async (isPasscodeSetRequest: boolean) => {
_102
await this.vault.setCustomPasscode('1234');
_102
});
_102
}
_102
_102
async storeSession(session: Session): Promise<void> {
_102
this.vault.setValue('session', session);
_102
}
_102
_102
async getSession(): Promise<Session | null> {
_102
if (await this.vault.isEmpty()) {
_102
return null;
_102
}
_102
return this.vault.getValue<Session>('session');
_102
}
_102
_102
async clearSession(): Promise<void> {
_102
await this.vault.clear();
_102
}
_102
_102
async lock(): Promise<void> {
_102
await this.vault.lock();
_102
}
_102
_102
async unlock(): Promise<void> {
_102
await this.vault.unlock();
_102
}
_102
_102
async isLocked(): Promise<boolean> {
_102
return (
_102
this.vault.config?.type !== VaultType.SecureStorage &&
_102
this.vault.config?.type !== VaultType.InMemory &&
_102
!(await this.vault.isEmpty()) &&
_102
(await this.vault.isLocked())
_102
);
_102
}
_102
_102
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_102
const type =
_102
mode === 'BiometricsWithPasscode'
_102
? VaultType.DeviceSecurity
_102
: mode === 'CustomPasscode'
_102
? VaultType.CustomPasscode
_102
: mode === 'InMemory'
_102
? VaultType.InMemory
_102
: VaultType.SecureStorage;
_102
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_102
await this.vault.updateConfig({
_102
...(this.vault.config as IdentityVaultConfig),
_102
type,
_102
deviceSecurityType,
_102
});
_102
}
_102
}

Import the modal controller and the PIN dialog component.

Inject the modal controller.

src/app/core/session-vault.service.ts

_110
import { Injectable } from '@angular/core';
_110
import {
_110
BrowserVault,
_110
DeviceSecurityType,
_110
IdentityVaultConfig,
_110
Vault,
_110
VaultType,
_110
} from '@ionic-enterprise/identity-vault';
_110
import { Session } from '../models/session';
_110
import { VaultFactory } from './vault.factory';
_110
import { Observable, Subject } from 'rxjs';
_110
import { PinDialogComponent } from '../pin-dialog/pin-dialog.component';
_110
import { ModalController } from '@ionic/angular/standalone';
_110
_110
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_110
_110
@Injectable({
_110
providedIn: 'root',
_110
})
_110
export class SessionVaultService {
_110
private lockedSubject: Subject<boolean>;
_110
private vault: BrowserVault | Vault;
_110
_110
constructor(private modalController: ModalController) {
_110
this.vault = VaultFactory.create();
_110
this.lockedSubject = new Subject<boolean>();
_110
}
_110
_110
get locked$(): Observable<boolean> {
_110
return this.lockedSubject.asObservable();
_110
}
_110
_110
async initialize(): Promise<void> {
_110
try {
_110
await this.vault.initialize({
_110
key: 'io.ionic.gettingstartediv',
_110
type: VaultType.SecureStorage,
_110
deviceSecurityType: DeviceSecurityType.None,
_110
lockAfterBackgrounded: 2000,
_110
});
_110
} catch (e: unknown) {
_110
await this.vault.clear();
_110
await this.updateUnlockMode('SecureStorage');
_110
}
_110
_110
this.vault.onLock(() => this.lockedSubject.next(true));
_110
this.vault.onUnlock(() => this.lockedSubject.next(false));
_110
_110
this.vault.onPasscodeRequested(async (isPasscodeSetRequest: boolean) => {
_110
const modal = await this.modalController.create({
_110
component: PinDialogComponent,
_110
backdropDismiss: false,
_110
componentProps: {
_110
setPasscodeMode: isPasscodeSetRequest,
_110
},
_110
});
_110
await modal.present();
_110
await this.vault.setCustomPasscode('1234');
_110
});
_110
}
_110
_110
async storeSession(session: Session): Promise<void> {
_110
this.vault.setValue('session', session);
_110
}
_110
_110
async getSession(): Promise<Session | null> {
_110
if (await this.vault.isEmpty()) {
_110
return null;
_110
}
_110
return this.vault.getValue<Session>('session');
_110
}
_110
_110
async clearSession(): Promise<void> {
_110
await this.vault.clear();
_110
}
_110
_110
async lock(): Promise<void> {
_110
await this.vault.lock();
_110
}
_110
_110
async unlock(): Promise<void> {
_110
await this.vault.unlock();
_110
}
_110
_110
async isLocked(): Promise<boolean> {
_110
return (
_110
this.vault.config?.type !== VaultType.SecureStorage &&
_110
this.vault.config?.type !== VaultType.InMemory &&
_110
!(await this.vault.isEmpty()) &&
_110
(await this.vault.isLocked())
_110
);
_110
}
_110
_110
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_110
const type =
_110
mode === 'BiometricsWithPasscode'
_110
? VaultType.DeviceSecurity
_110
: mode === 'CustomPasscode'
_110
? VaultType.CustomPasscode
_110
: mode === 'InMemory'
_110
? VaultType.InMemory
_110
: VaultType.SecureStorage;
_110
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_110
await this.vault.updateConfig({
_110
...(this.vault.config as IdentityVaultConfig),
_110
type,
_110
deviceSecurityType,
_110
});
_110
}
_110
}

Create and present the modal.

src/app/core/session-vault.service.ts

_111
import { Injectable } from '@angular/core';
_111
import {
_111
BrowserVault,
_111
DeviceSecurityType,
_111
IdentityVaultConfig,
_111
Vault,
_111
VaultType,
_111
} from '@ionic-enterprise/identity-vault';
_111
import { Session } from '../models/session';
_111
import { VaultFactory } from './vault.factory';
_111
import { Observable, Subject } from 'rxjs';
_111
import { PinDialogComponent } from '../pin-dialog/pin-dialog.component';
_111
import { ModalController } from '@ionic/angular/standalone';
_111
_111
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_111
_111
@Injectable({
_111
providedIn: 'root',
_111
})
_111
export class SessionVaultService {
_111
private lockedSubject: Subject<boolean>;
_111
private vault: BrowserVault | Vault;
_111
_111
constructor(private modalController: ModalController) {
_111
this.vault = VaultFactory.create();
_111
this.lockedSubject = new Subject<boolean>();
_111
}
_111
_111
get locked$(): Observable<boolean> {
_111
return this.lockedSubject.asObservable();
_111
}
_111
_111
async initialize(): Promise<void> {
_111
try {
_111
await this.vault.initialize({
_111
key: 'io.ionic.gettingstartediv',
_111
type: VaultType.SecureStorage,
_111
deviceSecurityType: DeviceSecurityType.None,
_111
lockAfterBackgrounded: 2000,
_111
});
_111
} catch (e: unknown) {
_111
await this.vault.clear();
_111
await this.updateUnlockMode('SecureStorage');
_111
}
_111
_111
this.vault.onLock(() => this.lockedSubject.next(true));
_111
this.vault.onUnlock(() => this.lockedSubject.next(false));
_111
_111
this.vault.onPasscodeRequested(async (isPasscodeSetRequest: boolean) => {
_111
const modal = await this.modalController.create({
_111
component: PinDialogComponent,
_111
backdropDismiss: false,
_111
componentProps: {
_111
setPasscodeMode: isPasscodeSetRequest,
_111
},
_111
});
_111
await modal.present();
_111
const { data } = await modal.onDidDismiss();
_111
this.vault.setCustomPasscode(data || '');
_111
});
_111
}
_111
_111
async storeSession(session: Session): Promise<void> {
_111
this.vault.setValue('session', session);
_111
}
_111
_111
async getSession(): Promise<Session | null> {
_111
if (await this.vault.isEmpty()) {
_111
return null;
_111
}
_111
return this.vault.getValue<Session>('session');
_111
}
_111
_111
async clearSession(): Promise<void> {
_111
await this.vault.clear();
_111
}
_111
_111
async lock(): Promise<void> {
_111
await this.vault.lock();
_111
}
_111
_111
async unlock(): Promise<void> {
_111
await this.vault.unlock();
_111
}
_111
_111
async isLocked(): Promise<boolean> {
_111
return (
_111
this.vault.config?.type !== VaultType.SecureStorage &&
_111
this.vault.config?.type !== VaultType.InMemory &&
_111
!(await this.vault.isEmpty()) &&
_111
(await this.vault.isLocked())
_111
);
_111
}
_111
_111
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_111
const type =
_111
mode === 'BiometricsWithPasscode'
_111
? VaultType.DeviceSecurity
_111
: mode === 'CustomPasscode'
_111
? VaultType.CustomPasscode
_111
: mode === 'InMemory'
_111
? VaultType.InMemory
_111
: VaultType.SecureStorage;
_111
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_111
await this.vault.updateConfig({
_111
...(this.vault.config as IdentityVaultConfig),
_111
type,
_111
deviceSecurityType,
_111
});
_111
}
_111
}

Set the passcode for the vault based on the data sent back from the PIN dialog.

Import the modal controller and the PIN dialog component.

Inject the modal controller.

Create and present the modal.

Set the passcode for the vault based on the data sent back from the PIN dialog.

src/app/core/session-vault.service.ts

_102
import { Injectable } from '@angular/core';
_102
import {
_102
BrowserVault,
_102
DeviceSecurityType,
_102
IdentityVaultConfig,
_102
Vault,
_102
VaultType,
_102
} from '@ionic-enterprise/identity-vault';
_102
import { Session } from '../models/session';
_102
import { VaultFactory } from './vault.factory';
_102
import { Observable, Subject } from 'rxjs';
_102
import { PinDialogComponent } from '../pin-dialog/pin-dialog.component';
_102
import { ModalController } from '@ionic/angular/standalone';
_102
_102
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_102
_102
@Injectable({
_102
providedIn: 'root',
_102
})
_102
export class SessionVaultService {
_102
private lockedSubject: Subject<boolean>;
_102
private vault: BrowserVault | Vault;
_102
_102
constructor(private modalController: ModalController) {
_102
this.vault = VaultFactory.create();
_102
this.lockedSubject = new Subject<boolean>();
_102
}
_102
_102
get locked$(): Observable<boolean> {
_102
return this.lockedSubject.asObservable();
_102
}
_102
_102
async initialize(): Promise<void> {
_102
try {
_102
await this.vault.initialize({
_102
key: 'io.ionic.gettingstartediv',
_102
type: VaultType.SecureStorage,
_102
deviceSecurityType: DeviceSecurityType.None,
_102
lockAfterBackgrounded: 2000,
_102
});
_102
} catch (e: unknown) {
_102
await this.vault.clear();
_102
await this.updateUnlockMode('SecureStorage');
_102
}
_102
_102
this.vault.onLock(() => this.lockedSubject.next(true));
_102
this.vault.onUnlock(() => this.lockedSubject.next(false));
_102
_102
this.vault.onPasscodeRequested(async (isPasscodeSetRequest: boolean) => {
_102
await this.vault.setCustomPasscode('1234');
_102
});
_102
}
_102
_102
async storeSession(session: Session): Promise<void> {
_102
this.vault.setValue('session', session);
_102
}
_102
_102
async getSession(): Promise<Session | null> {
_102
if (await this.vault.isEmpty()) {
_102
return null;
_102
}
_102
return this.vault.getValue<Session>('session');
_102
}
_102
_102
async clearSession(): Promise<void> {
_102
await this.vault.clear();
_102
}
_102
_102
async lock(): Promise<void> {
_102
await this.vault.lock();
_102
}
_102
_102
async unlock(): Promise<void> {
_102
await this.vault.unlock();
_102
}
_102
_102
async isLocked(): Promise<boolean> {
_102
return (
_102
this.vault.config?.type !== VaultType.SecureStorage &&
_102
this.vault.config?.type !== VaultType.InMemory &&
_102
!(await this.vault.isEmpty()) &&
_102
(await this.vault.isLocked())
_102
);
_102
}
_102
_102
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_102
const type =
_102
mode === 'BiometricsWithPasscode'
_102
? VaultType.DeviceSecurity
_102
: mode === 'CustomPasscode'
_102
? VaultType.CustomPasscode
_102
: mode === 'InMemory'
_102
? VaultType.InMemory
_102
: VaultType.SecureStorage;
_102
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_102
await this.vault.updateConfig({
_102
...(this.vault.config as IdentityVaultConfig),
_102
type,
_102
deviceSecurityType,
_102
});
_102
}
_102
}

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.