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/composables/session-vault.ts
src/views/Tab1Page.vue

_96
import { useVaultFactory } from '@/composables/vault-factory';
_96
import { Session } from '@/models/session';
_96
import {
_96
BrowserVault,
_96
DeviceSecurityType,
_96
IdentityVaultConfig,
_96
Vault,
_96
VaultType,
_96
} from '@ionic-enterprise/identity-vault';
_96
import { ref } from 'vue';
_96
_96
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_96
_96
const { createVault } = useVaultFactory();
_96
const vault: Vault | BrowserVault = createVault();
_96
const session = ref<Session | null>(null);
_96
_96
const initializeVault = async (): Promise<void> => {
_96
try {
_96
await 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 vault.clear();
_96
await updateUnlockMode('SecureStorage');
_96
}
_96
_96
vault.onLock(() => (session.value = null));
_96
};
_96
_96
const storeSession = async (s: Session): Promise<void> => {
_96
vault.setValue('session', s);
_96
session.value = s;
_96
};
_96
_96
const getSession = async (): Promise<void> => {
_96
if (await vault.isEmpty()) {
_96
session.value = null;
_96
} else {
_96
session.value = await vault.getValue<Session>('session');
_96
}
_96
};
_96
_96
const clearSession = async (): Promise<void> => {
_96
await vault.clear();
_96
session.value = null;
_96
};
_96
_96
const lockSession = async (): Promise<void> => {
_96
await vault.lock();
_96
session.value = null;
_96
};
_96
_96
const unlockSession = async (): Promise<void> => {
_96
await vault.unlock();
_96
session.value = await vault.getValue<Session>('session');
_96
};
_96
_96
const sessionIsLocked = async (): Promise<boolean> => {
_96
return (
_96
vault.config?.type !== VaultType.SecureStorage &&
_96
vault.config?.type !== VaultType.InMemory &&
_96
!(await vault.isEmpty()) &&
_96
(await vault.isLocked())
_96
);
_96
};
_96
_96
const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_96
const type =
_96
mode === 'BiometricsWithPasscode'
_96
? VaultType.DeviceSecurity
_96
: mode === 'InMemory'
_96
? VaultType.InMemory
_96
: VaultType.SecureStorage;
_96
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_96
await vault.updateConfig({
_96
...(vault.config as IdentityVaultConfig),
_96
type,
_96
deviceSecurityType,
_96
});
_96
};
_96
_96
export const useSessionVault = (): any => ({
_96
clearSession,
_96
getSession,
_96
initializeVault,
_96
lockSession,
_96
session,
_96
sessionIsLocked,
_96
storeSession,
_96
unlockSession,
_96
updateUnlockMode,
_96
});

Add CustomPasscode to the UnlockMode.

src/composables/session-vault.ts
src/views/Tab1Page.vue

_98
import { useVaultFactory } from '@/composables/vault-factory';
_98
import { Session } from '@/models/session';
_98
import {
_98
BrowserVault,
_98
DeviceSecurityType,
_98
IdentityVaultConfig,
_98
Vault,
_98
VaultType,
_98
} from '@ionic-enterprise/identity-vault';
_98
import { ref } from 'vue';
_98
_98
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_98
_98
const { createVault } = useVaultFactory();
_98
const vault: Vault | BrowserVault = createVault();
_98
const session = ref<Session | null>(null);
_98
_98
const initializeVault = async (): Promise<void> => {
_98
try {
_98
await vault.initialize({
_98
key: 'io.ionic.gettingstartediv',
_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(() => (session.value = null));
_98
};
_98
_98
const storeSession = async (s: Session): Promise<void> => {
_98
vault.setValue('session', s);
_98
session.value = s;
_98
};
_98
_98
const getSession = async (): Promise<void> => {
_98
if (await vault.isEmpty()) {
_98
session.value = null;
_98
} else {
_98
session.value = await vault.getValue<Session>('session');
_98
}
_98
};
_98
_98
const clearSession = async (): Promise<void> => {
_98
await vault.clear();
_98
session.value = null;
_98
};
_98
_98
const lockSession = async (): Promise<void> => {
_98
await vault.lock();
_98
session.value = null;
_98
};
_98
_98
const unlockSession = async (): Promise<void> => {
_98
await vault.unlock();
_98
session.value = await vault.getValue<Session>('session');
_98
};
_98
_98
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
const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_98
const type =
_98
mode === 'BiometricsWithPasscode'
_98
? VaultType.DeviceSecurity
_98
: mode === 'CustomPasscode'
_98
? VaultType.CustomPasscode
_98
: mode === 'InMemory'
_98
? VaultType.InMemory
_98
: VaultType.SecureStorage;
_98
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_98
await vault.updateConfig({
_98
...(vault.config as IdentityVaultConfig),
_98
type,
_98
deviceSecurityType,
_98
});
_98
};
_98
_98
export const useSessionVault = (): any => ({
_98
clearSession,
_98
getSession,
_98
initializeVault,
_98
lockSession,
_98
session,
_98
sessionIsLocked,
_98
storeSession,
_98
unlockSession,
_98
updateUnlockMode,
_98
});

Update the calculation that determines the type for the vault.

src/composables/session-vault.ts
src/views/Tab1Page.vue

_102
import { useVaultFactory } from '@/composables/vault-factory';
_102
import { Session } from '@/models/session';
_102
import {
_102
BrowserVault,
_102
DeviceSecurityType,
_102
IdentityVaultConfig,
_102
Vault,
_102
VaultType,
_102
} from '@ionic-enterprise/identity-vault';
_102
import { ref } from 'vue';
_102
_102
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_102
_102
const { createVault } = useVaultFactory();
_102
const vault: Vault | BrowserVault = createVault();
_102
const session = ref<Session | null>(null);
_102
_102
const initializeVault = async (): Promise<void> => {
_102
try {
_102
await 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 vault.clear();
_102
await updateUnlockMode('SecureStorage');
_102
}
_102
_102
vault.onLock(() => (session.value = null));
_102
_102
vault.onPasscodeRequested(async (isPasscodeSetRequest: boolean) => {
_102
await vault.setCustomPasscode('1234');
_102
});
_102
};
_102
_102
const storeSession = async (s: Session): Promise<void> => {
_102
vault.setValue('session', s);
_102
session.value = s;
_102
};
_102
_102
const getSession = async (): Promise<void> => {
_102
if (await vault.isEmpty()) {
_102
session.value = null;
_102
} else {
_102
session.value = await vault.getValue<Session>('session');
_102
}
_102
};
_102
_102
const clearSession = async (): Promise<void> => {
_102
await vault.clear();
_102
session.value = null;
_102
};
_102
_102
const lockSession = async (): Promise<void> => {
_102
await vault.lock();
_102
session.value = null;
_102
};
_102
_102
const unlockSession = async (): Promise<void> => {
_102
await vault.unlock();
_102
session.value = await vault.getValue<Session>('session');
_102
};
_102
_102
const sessionIsLocked = async (): Promise<boolean> => {
_102
return (
_102
vault.config?.type !== VaultType.SecureStorage &&
_102
vault.config?.type !== VaultType.InMemory &&
_102
!(await vault.isEmpty()) &&
_102
(await vault.isLocked())
_102
);
_102
};
_102
_102
const updateUnlockMode = async (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 vault.updateConfig({
_102
...(vault.config as IdentityVaultConfig),
_102
type,
_102
deviceSecurityType,
_102
});
_102
};
_102
_102
export const useSessionVault = (): any => ({
_102
clearSession,
_102
getSession,
_102
initializeVault,
_102
lockSession,
_102
session,
_102
sessionIsLocked,
_102
storeSession,
_102
unlockSession,
_102
updateUnlockMode,
_102
});

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

src/composables/session-vault.ts
src/views/Tab1Page.vue

_84
<template>
_84
<ion-page>
_84
<ion-header>
_84
<ion-toolbar>
_84
<ion-title>Tab 1</ion-title>
_84
</ion-toolbar>
_84
</ion-header>
_84
<ion-content :fullscreen="true">
_84
<ion-header collapse="condense">
_84
<ion-toolbar>
_84
<ion-title size="large">Tab 1</ion-title>
_84
</ion-toolbar>
_84
</ion-header>
_84
_84
<ion-list>
_84
<ion-item>
_84
<ion-label>
_84
<ion-button expand="block" color="danger" @click="logoutClicked" data-testid="logout">Logout</ion-button>
_84
</ion-label>
_84
</ion-item>
_84
<ion-item>
_84
<ion-label>
_84
<ion-button expand="block" color="secondary" @click="updateUnlockMode('BiometricsWithPasscode')"
_84
data-testid="use-biometrics">Use
_84
Biometrics</ion-button>
_84
</ion-label>
_84
</ion-item>
_84
<ion-item>
_84
<ion-label>
_84
<ion-button expand="block" color="secondary" @click="updateUnlockMode('CustomPasscode')"
_84
data-testid="use-in-memory">Use Custom Passcode</ion-button>
_84
</ion-label>
_84
</ion-item>
_84
<ion-item>
_84
<ion-label>
_84
<ion-button expand="block" color="secondary" @click="updateUnlockMode('InMemory')"
_84
data-testid="use-in-memory">Use In Memory</ion-button>
_84
</ion-label>
_84
</ion-item>
_84
<ion-item>
_84
<ion-label>
_84
<ion-button expand="block" color="secondary" @click="updateUnlockMode('SecureStorage')"
_84
data-testid="use-secure-storage">Use Secure
_84
Storage</ion-button>
_84
</ion-label>
_84
</ion-item>
_84
<ion-item>
_84
<div data-testid="session">
_84
<div>{{ session?.email }}</div>
_84
<div>{{ session?.firstName }} {{ session?.lastName }}</div>
_84
<div>{{ session?.accessToken }}</div>
_84
<div>{{ session?.refreshToken }}</div>
_84
</div>
_84
</ion-item>
_84
</ion-list>
_84
</ion-content>
_84
</ion-page>
_84
</template>
_84
_84
<script setup lang="ts">
_84
import { useAuthentication } from '@/composables/authentication';
_84
import { useSessionVault } from '@/composables/session-vault';
_84
import {
_84
IonButton,
_84
IonContent,
_84
IonHeader,
_84
IonItem,
_84
IonLabel,
_84
IonList,
_84
IonPage,
_84
IonTitle,
_84
IonToolbar,
_84
} from '@ionic/vue';
_84
import { useRouter } from 'vue-router';
_84
_84
const { logout } = useAuthentication();
_84
const { session, updateUnlockMode } = useSessionVault();
_84
const router = useRouter();
_84
_84
const logoutClicked = async (): Promise<void> => {
_84
await logout();
_84
router.replace('/');
_84
}
_84
</script>

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/composables/session-vault.ts
src/views/Tab1Page.vue

_96
import { useVaultFactory } from '@/composables/vault-factory';
_96
import { Session } from '@/models/session';
_96
import {
_96
BrowserVault,
_96
DeviceSecurityType,
_96
IdentityVaultConfig,
_96
Vault,
_96
VaultType,
_96
} from '@ionic-enterprise/identity-vault';
_96
import { ref } from 'vue';
_96
_96
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_96
_96
const { createVault } = useVaultFactory();
_96
const vault: Vault | BrowserVault = createVault();
_96
const session = ref<Session | null>(null);
_96
_96
const initializeVault = async (): Promise<void> => {
_96
try {
_96
await 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 vault.clear();
_96
await updateUnlockMode('SecureStorage');
_96
}
_96
_96
vault.onLock(() => (session.value = null));
_96
};
_96
_96
const storeSession = async (s: Session): Promise<void> => {
_96
vault.setValue('session', s);
_96
session.value = s;
_96
};
_96
_96
const getSession = async (): Promise<void> => {
_96
if (await vault.isEmpty()) {
_96
session.value = null;
_96
} else {
_96
session.value = await vault.getValue<Session>('session');
_96
}
_96
};
_96
_96
const clearSession = async (): Promise<void> => {
_96
await vault.clear();
_96
session.value = null;
_96
};
_96
_96
const lockSession = async (): Promise<void> => {
_96
await vault.lock();
_96
session.value = null;
_96
};
_96
_96
const unlockSession = async (): Promise<void> => {
_96
await vault.unlock();
_96
session.value = await vault.getValue<Session>('session');
_96
};
_96
_96
const sessionIsLocked = async (): Promise<boolean> => {
_96
return (
_96
vault.config?.type !== VaultType.SecureStorage &&
_96
vault.config?.type !== VaultType.InMemory &&
_96
!(await vault.isEmpty()) &&
_96
(await vault.isLocked())
_96
);
_96
};
_96
_96
const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_96
const type =
_96
mode === 'BiometricsWithPasscode'
_96
? VaultType.DeviceSecurity
_96
: mode === 'InMemory'
_96
? VaultType.InMemory
_96
: VaultType.SecureStorage;
_96
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_96
await vault.updateConfig({
_96
...(vault.config as IdentityVaultConfig),
_96
type,
_96
deviceSecurityType,
_96
});
_96
};
_96
_96
export const useSessionVault = (): any => ({
_96
clearSession,
_96
getSession,
_96
initializeVault,
_96
lockSession,
_96
session,
_96
sessionIsLocked,
_96
storeSession,
_96
unlockSession,
_96
updateUnlockMode,
_96
});

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.

All 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/components/AppPinDialog.vue

_179
<template>
_179
<ion-header>
_179
<ion-toolbar>
_179
<ion-title>{{ title }}</ion-title>
_179
<ion-buttons v-if="!setPasscodeMode" slot="start">
_179
<ion-button @click="cancel" data-testid="cancel-button">
_179
Cancel
_179
</ion-button>
_179
</ion-buttons>
_179
<ion-buttons slot="end">
_179
<ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter
_179
</ion-button>
_179
</ion-buttons>
_179
</ion-toolbar>
_179
</ion-header>
_179
_179
<ion-content class="ion-padding ion-text-center">
_179
<ion-label data-testid="prompt">
_179
<div class="prompt">{{ prompt }}</div>
_179
</ion-label>
_179
<ion-label data-testid="display-pin">
_179
<div class="pin">{{ displayPin }}</div>
_179
</ion-label>
_179
<ion-label color="danger" data-testid="error-message">
_179
<div class="error">{{ errorMessage }}</div>
_179
</ion-label>
_179
</ion-content>
_179
_179
<ion-footer>
_179
<ion-grid>
_179
<ion-row>
_179
<ion-col v-for="n of [1, 2, 3]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [4, 5, 6]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [7, 8, 9]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"
_179
data-testclass="number-button">0</ion-button>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"
_179
data-testid="delete-button">
_179
<ion-icon :icon="backspace"></ion-icon>
_179
</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
</ion-grid>
_179
</ion-footer>
_179
</template>
_179
_179
<script setup lang="ts">
_179
import {
_179
IonButton,
_179
IonButtons,
_179
IonCol,
_179
IonContent,
_179
IonFooter,
_179
IonGrid,
_179
IonHeader,
_179
IonLabel,
_179
IonRow,
_179
IonTitle,
_179
IonToolbar,
_179
modalController,
_179
} from '@ionic/vue';
_179
import { computed, ref } from 'vue';
_179
import { backspace } from 'ionicons/icons';
_179
_179
// eslint-disable-next-line no-undef
_179
const props = defineProps({
_179
setPasscodeMode: Boolean,
_179
});
_179
_179
let verifyPin = '';
_179
_179
const disableDelete = computed(() => !pin.value.length);
_179
const disableEnter = computed(() => !(pin.value.length > 3));
_179
const disableInput = computed(() => pin.value.length > 8);
_179
_179
const errorMessage = ref('');
_179
const pin = ref('');
_179
const prompt = ref('');
_179
const title = ref('');
_179
_179
const displayPin = computed(() => '*********'.slice(0, pin.value.length));
_179
_179
const append = (n: number) => {
_179
errorMessage.value = '';
_179
pin.value = pin.value.concat(n.toString());
_179
};
_179
_179
const remove = () => {
_179
if (pin.value) {
_179
pin.value = pin.value.slice(0, pin.value.length - 1);
_179
}
_179
};
_179
_179
const initSetPasscodeMode = () => {
_179
title.value = 'Create PIN';
_179
prompt.value = 'Create Session PIN';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initUnlockMode = () => {
_179
title.value = 'Unlock';
_179
prompt.value = 'Enter PIN to Unlock';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initVerifyMode = () => {
_179
prompt.value = 'Verify PIN';
_179
verifyPin = pin.value;
_179
pin.value = '';
_179
};
_179
_179
const cancel = () => {
_179
modalController.dismiss(undefined, 'cancel');
_179
};
_179
_179
const submit = () => {
_179
if (props.setPasscodeMode) {
_179
if (!verifyPin) {
_179
initVerifyMode();
_179
} else if (verifyPin === pin.value) {
_179
modalController.dismiss(pin.value);
_179
} else {
_179
errorMessage.value = 'PINs do not match';
_179
initSetPasscodeMode();
_179
}
_179
} else {
_179
modalController.dismiss(pin.value);
_179
}
_179
};
_179
_179
if (props.setPasscodeMode) {
_179
initSetPasscodeMode();
_179
} else {
_179
initUnlockMode();
_179
}
_179
</script>
_179
_179
<style scoped>
_179
.prompt {
_179
font-size: 2rem;
_179
font-weight: bold;
_179
}
_179
_179
.pin {
_179
font-size: 3rem;
_179
font-weight: bold;
_179
}
_179
_179
.error {
_179
font-size: 1.5rem;
_179
font-weight: bold;
_179
}
_179
_179
ion-grid {
_179
padding-bottom: 32px;
_179
}
_179
</style>

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

src/components/AppPinDialog.vue

_179
<template>
_179
<ion-header>
_179
<ion-toolbar>
_179
<ion-title>{{ title }}</ion-title>
_179
<ion-buttons v-if="!setPasscodeMode" slot="start">
_179
<ion-button @click="cancel" data-testid="cancel-button">
_179
Cancel
_179
</ion-button>
_179
</ion-buttons>
_179
<ion-buttons slot="end">
_179
<ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter
_179
</ion-button>
_179
</ion-buttons>
_179
</ion-toolbar>
_179
</ion-header>
_179
_179
<ion-content class="ion-padding ion-text-center">
_179
<ion-label data-testid="prompt">
_179
<div class="prompt">{{ prompt }}</div>
_179
</ion-label>
_179
<ion-label data-testid="display-pin">
_179
<div class="pin">{{ displayPin }}</div>
_179
</ion-label>
_179
<ion-label color="danger" data-testid="error-message">
_179
<div class="error">{{ errorMessage }}</div>
_179
</ion-label>
_179
</ion-content>
_179
_179
<ion-footer>
_179
<ion-grid>
_179
<ion-row>
_179
<ion-col v-for="n of [1, 2, 3]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [4, 5, 6]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [7, 8, 9]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"
_179
data-testclass="number-button">0</ion-button>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"
_179
data-testid="delete-button">
_179
<ion-icon :icon="backspace"></ion-icon>
_179
</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
</ion-grid>
_179
</ion-footer>
_179
</template>
_179
_179
<script setup lang="ts">
_179
import {
_179
IonButton,
_179
IonButtons,
_179
IonCol,
_179
IonContent,
_179
IonFooter,
_179
IonGrid,
_179
IonHeader,
_179
IonLabel,
_179
IonRow,
_179
IonTitle,
_179
IonToolbar,
_179
modalController,
_179
} from '@ionic/vue';
_179
import { computed, ref } from 'vue';
_179
import { backspace } from 'ionicons/icons';
_179
_179
// eslint-disable-next-line no-undef
_179
const props = defineProps({
_179
setPasscodeMode: Boolean,
_179
});
_179
_179
let verifyPin = '';
_179
_179
const disableDelete = computed(() => !pin.value.length);
_179
const disableEnter = computed(() => !(pin.value.length > 3));
_179
const disableInput = computed(() => pin.value.length > 8);
_179
_179
const errorMessage = ref('');
_179
const pin = ref('');
_179
const prompt = ref('');
_179
const title = ref('');
_179
_179
const displayPin = computed(() => '*********'.slice(0, pin.value.length));
_179
_179
const append = (n: number) => {
_179
errorMessage.value = '';
_179
pin.value = pin.value.concat(n.toString());
_179
};
_179
_179
const remove = () => {
_179
if (pin.value) {
_179
pin.value = pin.value.slice(0, pin.value.length - 1);
_179
}
_179
};
_179
_179
const initSetPasscodeMode = () => {
_179
title.value = 'Create PIN';
_179
prompt.value = 'Create Session PIN';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initUnlockMode = () => {
_179
title.value = 'Unlock';
_179
prompt.value = 'Enter PIN to Unlock';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initVerifyMode = () => {
_179
prompt.value = 'Verify PIN';
_179
verifyPin = pin.value;
_179
pin.value = '';
_179
};
_179
_179
const cancel = () => {
_179
modalController.dismiss(undefined, 'cancel');
_179
};
_179
_179
const submit = () => {
_179
if (props.setPasscodeMode) {
_179
if (!verifyPin) {
_179
initVerifyMode();
_179
} else if (verifyPin === pin.value) {
_179
modalController.dismiss(pin.value);
_179
} else {
_179
errorMessage.value = 'PINs do not match';
_179
initSetPasscodeMode();
_179
}
_179
} else {
_179
modalController.dismiss(pin.value);
_179
}
_179
};
_179
_179
if (props.setPasscodeMode) {
_179
initSetPasscodeMode();
_179
} else {
_179
initUnlockMode();
_179
}
_179
</script>
_179
_179
<style scoped>
_179
.prompt {
_179
font-size: 2rem;
_179
font-weight: bold;
_179
}
_179
_179
.pin {
_179
font-size: 3rem;
_179
font-weight: bold;
_179
}
_179
_179
.error {
_179
font-size: 1.5rem;
_179
font-weight: bold;
_179
}
_179
_179
ion-grid {
_179
padding-bottom: 32px;
_179
}
_179
</style>

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

src/components/AppPinDialog.vue

_179
<template>
_179
<ion-header>
_179
<ion-toolbar>
_179
<ion-title>{{ title }}</ion-title>
_179
<ion-buttons v-if="!setPasscodeMode" slot="start">
_179
<ion-button @click="cancel" data-testid="cancel-button">
_179
Cancel
_179
</ion-button>
_179
</ion-buttons>
_179
<ion-buttons slot="end">
_179
<ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter
_179
</ion-button>
_179
</ion-buttons>
_179
</ion-toolbar>
_179
</ion-header>
_179
_179
<ion-content class="ion-padding ion-text-center">
_179
<ion-label data-testid="prompt">
_179
<div class="prompt">{{ prompt }}</div>
_179
</ion-label>
_179
<ion-label data-testid="display-pin">
_179
<div class="pin">{{ displayPin }}</div>
_179
</ion-label>
_179
<ion-label color="danger" data-testid="error-message">
_179
<div class="error">{{ errorMessage }}</div>
_179
</ion-label>
_179
</ion-content>
_179
_179
<ion-footer>
_179
<ion-grid>
_179
<ion-row>
_179
<ion-col v-for="n of [1, 2, 3]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [4, 5, 6]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [7, 8, 9]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"
_179
data-testclass="number-button">0</ion-button>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"
_179
data-testid="delete-button">
_179
<ion-icon :icon="backspace"></ion-icon>
_179
</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
</ion-grid>
_179
</ion-footer>
_179
</template>
_179
_179
<script setup lang="ts">
_179
import {
_179
IonButton,
_179
IonButtons,
_179
IonCol,
_179
IonContent,
_179
IonFooter,
_179
IonGrid,
_179
IonHeader,
_179
IonLabel,
_179
IonRow,
_179
IonTitle,
_179
IonToolbar,
_179
modalController,
_179
} from '@ionic/vue';
_179
import { computed, ref } from 'vue';
_179
import { backspace } from 'ionicons/icons';
_179
_179
// eslint-disable-next-line no-undef
_179
const props = defineProps({
_179
setPasscodeMode: Boolean,
_179
});
_179
_179
let verifyPin = '';
_179
_179
const disableDelete = computed(() => !pin.value.length);
_179
const disableEnter = computed(() => !(pin.value.length > 3));
_179
const disableInput = computed(() => pin.value.length > 8);
_179
_179
const errorMessage = ref('');
_179
const pin = ref('');
_179
const prompt = ref('');
_179
const title = ref('');
_179
_179
const displayPin = computed(() => '*********'.slice(0, pin.value.length));
_179
_179
const append = (n: number) => {
_179
errorMessage.value = '';
_179
pin.value = pin.value.concat(n.toString());
_179
};
_179
_179
const remove = () => {
_179
if (pin.value) {
_179
pin.value = pin.value.slice(0, pin.value.length - 1);
_179
}
_179
};
_179
_179
const initSetPasscodeMode = () => {
_179
title.value = 'Create PIN';
_179
prompt.value = 'Create Session PIN';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initUnlockMode = () => {
_179
title.value = 'Unlock';
_179
prompt.value = 'Enter PIN to Unlock';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initVerifyMode = () => {
_179
prompt.value = 'Verify PIN';
_179
verifyPin = pin.value;
_179
pin.value = '';
_179
};
_179
_179
const cancel = () => {
_179
modalController.dismiss(undefined, 'cancel');
_179
};
_179
_179
const submit = () => {
_179
if (props.setPasscodeMode) {
_179
if (!verifyPin) {
_179
initVerifyMode();
_179
} else if (verifyPin === pin.value) {
_179
modalController.dismiss(pin.value);
_179
} else {
_179
errorMessage.value = 'PINs do not match';
_179
initSetPasscodeMode();
_179
}
_179
} else {
_179
modalController.dismiss(pin.value);
_179
}
_179
};
_179
_179
if (props.setPasscodeMode) {
_179
initSetPasscodeMode();
_179
} else {
_179
initUnlockMode();
_179
}
_179
</script>
_179
_179
<style scoped>
_179
.prompt {
_179
font-size: 2rem;
_179
font-weight: bold;
_179
}
_179
_179
.pin {
_179
font-size: 3rem;
_179
font-weight: bold;
_179
}
_179
_179
.error {
_179
font-size: 1.5rem;
_179
font-weight: bold;
_179
}
_179
_179
ion-grid {
_179
padding-bottom: 32px;
_179
}
_179
</style>

The keypad.

src/components/AppPinDialog.vue

_179
<template>
_179
<ion-header>
_179
<ion-toolbar>
_179
<ion-title>{{ title }}</ion-title>
_179
<ion-buttons v-if="!setPasscodeMode" slot="start">
_179
<ion-button @click="cancel" data-testid="cancel-button">
_179
Cancel
_179
</ion-button>
_179
</ion-buttons>
_179
<ion-buttons slot="end">
_179
<ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter
_179
</ion-button>
_179
</ion-buttons>
_179
</ion-toolbar>
_179
</ion-header>
_179
_179
<ion-content class="ion-padding ion-text-center">
_179
<ion-label data-testid="prompt">
_179
<div class="prompt">{{ prompt }}</div>
_179
</ion-label>
_179
<ion-label data-testid="display-pin">
_179
<div class="pin">{{ displayPin }}</div>
_179
</ion-label>
_179
<ion-label color="danger" data-testid="error-message">
_179
<div class="error">{{ errorMessage }}</div>
_179
</ion-label>
_179
</ion-content>
_179
_179
<ion-footer>
_179
<ion-grid>
_179
<ion-row>
_179
<ion-col v-for="n of [1, 2, 3]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [4, 5, 6]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [7, 8, 9]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"
_179
data-testclass="number-button">0</ion-button>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"
_179
data-testid="delete-button">
_179
<ion-icon :icon="backspace"></ion-icon>
_179
</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
</ion-grid>
_179
</ion-footer>
_179
</template>
_179
_179
<script setup lang="ts">
_179
import {
_179
IonButton,
_179
IonButtons,
_179
IonCol,
_179
IonContent,
_179
IonFooter,
_179
IonGrid,
_179
IonHeader,
_179
IonLabel,
_179
IonRow,
_179
IonTitle,
_179
IonToolbar,
_179
modalController,
_179
} from '@ionic/vue';
_179
import { computed, ref } from 'vue';
_179
import { backspace } from 'ionicons/icons';
_179
_179
// eslint-disable-next-line no-undef
_179
const props = defineProps({
_179
setPasscodeMode: Boolean,
_179
});
_179
_179
let verifyPin = '';
_179
_179
const disableDelete = computed(() => !pin.value.length);
_179
const disableEnter = computed(() => !(pin.value.length > 3));
_179
const disableInput = computed(() => pin.value.length > 8);
_179
_179
const errorMessage = ref('');
_179
const pin = ref('');
_179
const prompt = ref('');
_179
const title = ref('');
_179
_179
const displayPin = computed(() => '*********'.slice(0, pin.value.length));
_179
_179
const append = (n: number) => {
_179
errorMessage.value = '';
_179
pin.value = pin.value.concat(n.toString());
_179
};
_179
_179
const remove = () => {
_179
if (pin.value) {
_179
pin.value = pin.value.slice(0, pin.value.length - 1);
_179
}
_179
};
_179
_179
const initSetPasscodeMode = () => {
_179
title.value = 'Create PIN';
_179
prompt.value = 'Create Session PIN';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initUnlockMode = () => {
_179
title.value = 'Unlock';
_179
prompt.value = 'Enter PIN to Unlock';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initVerifyMode = () => {
_179
prompt.value = 'Verify PIN';
_179
verifyPin = pin.value;
_179
pin.value = '';
_179
};
_179
_179
const cancel = () => {
_179
modalController.dismiss(undefined, 'cancel');
_179
};
_179
_179
const submit = () => {
_179
if (props.setPasscodeMode) {
_179
if (!verifyPin) {
_179
initVerifyMode();
_179
} else if (verifyPin === pin.value) {
_179
modalController.dismiss(pin.value);
_179
} else {
_179
errorMessage.value = 'PINs do not match';
_179
initSetPasscodeMode();
_179
}
_179
} else {
_179
modalController.dismiss(pin.value);
_179
}
_179
};
_179
_179
if (props.setPasscodeMode) {
_179
initSetPasscodeMode();
_179
} else {
_179
initUnlockMode();
_179
}
_179
</script>
_179
_179
<style scoped>
_179
.prompt {
_179
font-size: 2rem;
_179
font-weight: bold;
_179
}
_179
_179
.pin {
_179
font-size: 3rem;
_179
font-weight: bold;
_179
}
_179
_179
.error {
_179
font-size: 1.5rem;
_179
font-weight: bold;
_179
}
_179
_179
ion-grid {
_179
padding-bottom: 32px;
_179
}
_179
</style>

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

src/components/AppPinDialog.vue

_179
<template>
_179
<ion-header>
_179
<ion-toolbar>
_179
<ion-title>{{ title }}</ion-title>
_179
<ion-buttons v-if="!setPasscodeMode" slot="start">
_179
<ion-button @click="cancel" data-testid="cancel-button">
_179
Cancel
_179
</ion-button>
_179
</ion-buttons>
_179
<ion-buttons slot="end">
_179
<ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter
_179
</ion-button>
_179
</ion-buttons>
_179
</ion-toolbar>
_179
</ion-header>
_179
_179
<ion-content class="ion-padding ion-text-center">
_179
<ion-label data-testid="prompt">
_179
<div class="prompt">{{ prompt }}</div>
_179
</ion-label>
_179
<ion-label data-testid="display-pin">
_179
<div class="pin">{{ displayPin }}</div>
_179
</ion-label>
_179
<ion-label color="danger" data-testid="error-message">
_179
<div class="error">{{ errorMessage }}</div>
_179
</ion-label>
_179
</ion-content>
_179
_179
<ion-footer>
_179
<ion-grid>
_179
<ion-row>
_179
<ion-col v-for="n of [1, 2, 3]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [4, 5, 6]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [7, 8, 9]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"
_179
data-testclass="number-button">0</ion-button>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"
_179
data-testid="delete-button">
_179
<ion-icon :icon="backspace"></ion-icon>
_179
</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
</ion-grid>
_179
</ion-footer>
_179
</template>
_179
_179
<script setup lang="ts">
_179
import {
_179
IonButton,
_179
IonButtons,
_179
IonCol,
_179
IonContent,
_179
IonFooter,
_179
IonGrid,
_179
IonHeader,
_179
IonLabel,
_179
IonRow,
_179
IonTitle,
_179
IonToolbar,
_179
modalController,
_179
} from '@ionic/vue';
_179
import { computed, ref } from 'vue';
_179
import { backspace } from 'ionicons/icons';
_179
_179
// eslint-disable-next-line no-undef
_179
const props = defineProps({
_179
setPasscodeMode: Boolean,
_179
});
_179
_179
let verifyPin = '';
_179
_179
const disableDelete = computed(() => !pin.value.length);
_179
const disableEnter = computed(() => !(pin.value.length > 3));
_179
const disableInput = computed(() => pin.value.length > 8);
_179
_179
const errorMessage = ref('');
_179
const pin = ref('');
_179
const prompt = ref('');
_179
const title = ref('');
_179
_179
const displayPin = computed(() => '*********'.slice(0, pin.value.length));
_179
_179
const append = (n: number) => {
_179
errorMessage.value = '';
_179
pin.value = pin.value.concat(n.toString());
_179
};
_179
_179
const remove = () => {
_179
if (pin.value) {
_179
pin.value = pin.value.slice(0, pin.value.length - 1);
_179
}
_179
};
_179
_179
const initSetPasscodeMode = () => {
_179
title.value = 'Create PIN';
_179
prompt.value = 'Create Session PIN';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initUnlockMode = () => {
_179
title.value = 'Unlock';
_179
prompt.value = 'Enter PIN to Unlock';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initVerifyMode = () => {
_179
prompt.value = 'Verify PIN';
_179
verifyPin = pin.value;
_179
pin.value = '';
_179
};
_179
_179
const cancel = () => {
_179
modalController.dismiss(undefined, 'cancel');
_179
};
_179
_179
const submit = () => {
_179
if (props.setPasscodeMode) {
_179
if (!verifyPin) {
_179
initVerifyMode();
_179
} else if (verifyPin === pin.value) {
_179
modalController.dismiss(pin.value);
_179
} else {
_179
errorMessage.value = 'PINs do not match';
_179
initSetPasscodeMode();
_179
}
_179
} else {
_179
modalController.dismiss(pin.value);
_179
}
_179
};
_179
_179
if (props.setPasscodeMode) {
_179
initSetPasscodeMode();
_179
} else {
_179
initUnlockMode();
_179
}
_179
</script>
_179
_179
<style scoped>
_179
.prompt {
_179
font-size: 2rem;
_179
font-weight: bold;
_179
}
_179
_179
.pin {
_179
font-size: 3rem;
_179
font-weight: bold;
_179
}
_179
_179
.error {
_179
font-size: 1.5rem;
_179
font-weight: bold;
_179
}
_179
_179
ion-grid {
_179
padding-bottom: 32px;
_179
}
_179
</style>

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

src/components/AppPinDialog.vue

_179
<template>
_179
<ion-header>
_179
<ion-toolbar>
_179
<ion-title>{{ title }}</ion-title>
_179
<ion-buttons v-if="!setPasscodeMode" slot="start">
_179
<ion-button @click="cancel" data-testid="cancel-button">
_179
Cancel
_179
</ion-button>
_179
</ion-buttons>
_179
<ion-buttons slot="end">
_179
<ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter
_179
</ion-button>
_179
</ion-buttons>
_179
</ion-toolbar>
_179
</ion-header>
_179
_179
<ion-content class="ion-padding ion-text-center">
_179
<ion-label data-testid="prompt">
_179
<div class="prompt">{{ prompt }}</div>
_179
</ion-label>
_179
<ion-label data-testid="display-pin">
_179
<div class="pin">{{ displayPin }}</div>
_179
</ion-label>
_179
<ion-label color="danger" data-testid="error-message">
_179
<div class="error">{{ errorMessage }}</div>
_179
</ion-label>
_179
</ion-content>
_179
_179
<ion-footer>
_179
<ion-grid>
_179
<ion-row>
_179
<ion-col v-for="n of [1, 2, 3]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [4, 5, 6]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [7, 8, 9]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"
_179
data-testclass="number-button">0</ion-button>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"
_179
data-testid="delete-button">
_179
<ion-icon :icon="backspace"></ion-icon>
_179
</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
</ion-grid>
_179
</ion-footer>
_179
</template>
_179
_179
<script setup lang="ts">
_179
import {
_179
IonButton,
_179
IonButtons,
_179
IonCol,
_179
IonContent,
_179
IonFooter,
_179
IonGrid,
_179
IonHeader,
_179
IonLabel,
_179
IonRow,
_179
IonTitle,
_179
IonToolbar,
_179
modalController,
_179
} from '@ionic/vue';
_179
import { computed, ref } from 'vue';
_179
import { backspace } from 'ionicons/icons';
_179
_179
// eslint-disable-next-line no-undef
_179
const props = defineProps({
_179
setPasscodeMode: Boolean,
_179
});
_179
_179
let verifyPin = '';
_179
_179
const disableDelete = computed(() => !pin.value.length);
_179
const disableEnter = computed(() => !(pin.value.length > 3));
_179
const disableInput = computed(() => pin.value.length > 8);
_179
_179
const errorMessage = ref('');
_179
const pin = ref('');
_179
const prompt = ref('');
_179
const title = ref('');
_179
_179
const displayPin = computed(() => '*********'.slice(0, pin.value.length));
_179
_179
const append = (n: number) => {
_179
errorMessage.value = '';
_179
pin.value = pin.value.concat(n.toString());
_179
};
_179
_179
const remove = () => {
_179
if (pin.value) {
_179
pin.value = pin.value.slice(0, pin.value.length - 1);
_179
}
_179
};
_179
_179
const initSetPasscodeMode = () => {
_179
title.value = 'Create PIN';
_179
prompt.value = 'Create Session PIN';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initUnlockMode = () => {
_179
title.value = 'Unlock';
_179
prompt.value = 'Enter PIN to Unlock';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initVerifyMode = () => {
_179
prompt.value = 'Verify PIN';
_179
verifyPin = pin.value;
_179
pin.value = '';
_179
};
_179
_179
const cancel = () => {
_179
modalController.dismiss(undefined, 'cancel');
_179
};
_179
_179
const submit = () => {
_179
if (props.setPasscodeMode) {
_179
if (!verifyPin) {
_179
initVerifyMode();
_179
} else if (verifyPin === pin.value) {
_179
modalController.dismiss(pin.value);
_179
} else {
_179
errorMessage.value = 'PINs do not match';
_179
initSetPasscodeMode();
_179
}
_179
} else {
_179
modalController.dismiss(pin.value);
_179
}
_179
};
_179
_179
if (props.setPasscodeMode) {
_179
initSetPasscodeMode();
_179
} else {
_179
initUnlockMode();
_179
}
_179
</script>
_179
_179
<style scoped>
_179
.prompt {
_179
font-size: 2rem;
_179
font-weight: bold;
_179
}
_179
_179
.pin {
_179
font-size: 3rem;
_179
font-weight: bold;
_179
}
_179
_179
.error {
_179
font-size: 1.5rem;
_179
font-weight: bold;
_179
}
_179
_179
ion-grid {
_179
padding-bottom: 32px;
_179
}
_179
</style>

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/AppPinDialog.vue

_179
<template>
_179
<ion-header>
_179
<ion-toolbar>
_179
<ion-title>{{ title }}</ion-title>
_179
<ion-buttons v-if="!setPasscodeMode" slot="start">
_179
<ion-button @click="cancel" data-testid="cancel-button">
_179
Cancel
_179
</ion-button>
_179
</ion-buttons>
_179
<ion-buttons slot="end">
_179
<ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter
_179
</ion-button>
_179
</ion-buttons>
_179
</ion-toolbar>
_179
</ion-header>
_179
_179
<ion-content class="ion-padding ion-text-center">
_179
<ion-label data-testid="prompt">
_179
<div class="prompt">{{ prompt }}</div>
_179
</ion-label>
_179
<ion-label data-testid="display-pin">
_179
<div class="pin">{{ displayPin }}</div>
_179
</ion-label>
_179
<ion-label color="danger" data-testid="error-message">
_179
<div class="error">{{ errorMessage }}</div>
_179
</ion-label>
_179
</ion-content>
_179
_179
<ion-footer>
_179
<ion-grid>
_179
<ion-row>
_179
<ion-col v-for="n of [1, 2, 3]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [4, 5, 6]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [7, 8, 9]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"
_179
data-testclass="number-button">0</ion-button>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"
_179
data-testid="delete-button">
_179
<ion-icon :icon="backspace"></ion-icon>
_179
</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
</ion-grid>
_179
</ion-footer>
_179
</template>
_179
_179
<script setup lang="ts">
_179
import {
_179
IonButton,
_179
IonButtons,
_179
IonCol,
_179
IonContent,
_179
IonFooter,
_179
IonGrid,
_179
IonHeader,
_179
IonLabel,
_179
IonRow,
_179
IonTitle,
_179
IonToolbar,
_179
modalController,
_179
} from '@ionic/vue';
_179
import { computed, ref } from 'vue';
_179
import { backspace } from 'ionicons/icons';
_179
_179
// eslint-disable-next-line no-undef
_179
const props = defineProps({
_179
setPasscodeMode: Boolean,
_179
});
_179
_179
let verifyPin = '';
_179
_179
const disableDelete = computed(() => !pin.value.length);
_179
const disableEnter = computed(() => !(pin.value.length > 3));
_179
const disableInput = computed(() => pin.value.length > 8);
_179
_179
const errorMessage = ref('');
_179
const pin = ref('');
_179
const prompt = ref('');
_179
const title = ref('');
_179
_179
const displayPin = computed(() => '*********'.slice(0, pin.value.length));
_179
_179
const append = (n: number) => {
_179
errorMessage.value = '';
_179
pin.value = pin.value.concat(n.toString());
_179
};
_179
_179
const remove = () => {
_179
if (pin.value) {
_179
pin.value = pin.value.slice(0, pin.value.length - 1);
_179
}
_179
};
_179
_179
const initSetPasscodeMode = () => {
_179
title.value = 'Create PIN';
_179
prompt.value = 'Create Session PIN';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initUnlockMode = () => {
_179
title.value = 'Unlock';
_179
prompt.value = 'Enter PIN to Unlock';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initVerifyMode = () => {
_179
prompt.value = 'Verify PIN';
_179
verifyPin = pin.value;
_179
pin.value = '';
_179
};
_179
_179
const cancel = () => {
_179
modalController.dismiss(undefined, 'cancel');
_179
};
_179
_179
const submit = () => {
_179
if (props.setPasscodeMode) {
_179
if (!verifyPin) {
_179
initVerifyMode();
_179
} else if (verifyPin === pin.value) {
_179
modalController.dismiss(pin.value);
_179
} else {
_179
errorMessage.value = 'PINs do not match';
_179
initSetPasscodeMode();
_179
}
_179
} else {
_179
modalController.dismiss(pin.value);
_179
}
_179
};
_179
_179
if (props.setPasscodeMode) {
_179
initSetPasscodeMode();
_179
} else {
_179
initUnlockMode();
_179
}
_179
</script>
_179
_179
<style scoped>
_179
.prompt {
_179
font-size: 2rem;
_179
font-weight: bold;
_179
}
_179
_179
.pin {
_179
font-size: 3rem;
_179
font-weight: bold;
_179
}
_179
_179
.error {
_179
font-size: 1.5rem;
_179
font-weight: bold;
_179
}
_179
_179
ion-grid {
_179
padding-bottom: 32px;
_179
}
_179
</style>

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

src/components/AppPinDialog.vue

_179
<template>
_179
<ion-header>
_179
<ion-toolbar>
_179
<ion-title>{{ title }}</ion-title>
_179
<ion-buttons v-if="!setPasscodeMode" slot="start">
_179
<ion-button @click="cancel" data-testid="cancel-button">
_179
Cancel
_179
</ion-button>
_179
</ion-buttons>
_179
<ion-buttons slot="end">
_179
<ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter
_179
</ion-button>
_179
</ion-buttons>
_179
</ion-toolbar>
_179
</ion-header>
_179
_179
<ion-content class="ion-padding ion-text-center">
_179
<ion-label data-testid="prompt">
_179
<div class="prompt">{{ prompt }}</div>
_179
</ion-label>
_179
<ion-label data-testid="display-pin">
_179
<div class="pin">{{ displayPin }}</div>
_179
</ion-label>
_179
<ion-label color="danger" data-testid="error-message">
_179
<div class="error">{{ errorMessage }}</div>
_179
</ion-label>
_179
</ion-content>
_179
_179
<ion-footer>
_179
<ion-grid>
_179
<ion-row>
_179
<ion-col v-for="n of [1, 2, 3]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [4, 5, 6]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [7, 8, 9]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"
_179
data-testclass="number-button">0</ion-button>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"
_179
data-testid="delete-button">
_179
<ion-icon :icon="backspace"></ion-icon>
_179
</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
</ion-grid>
_179
</ion-footer>
_179
</template>
_179
_179
<script setup lang="ts">
_179
import {
_179
IonButton,
_179
IonButtons,
_179
IonCol,
_179
IonContent,
_179
IonFooter,
_179
IonGrid,
_179
IonHeader,
_179
IonLabel,
_179
IonRow,
_179
IonTitle,
_179
IonToolbar,
_179
modalController,
_179
} from '@ionic/vue';
_179
import { computed, ref } from 'vue';
_179
import { backspace } from 'ionicons/icons';
_179
_179
// eslint-disable-next-line no-undef
_179
const props = defineProps({
_179
setPasscodeMode: Boolean,
_179
});
_179
_179
let verifyPin = '';
_179
_179
const disableDelete = computed(() => !pin.value.length);
_179
const disableEnter = computed(() => !(pin.value.length > 3));
_179
const disableInput = computed(() => pin.value.length > 8);
_179
_179
const errorMessage = ref('');
_179
const pin = ref('');
_179
const prompt = ref('');
_179
const title = ref('');
_179
_179
const displayPin = computed(() => '*********'.slice(0, pin.value.length));
_179
_179
const append = (n: number) => {
_179
errorMessage.value = '';
_179
pin.value = pin.value.concat(n.toString());
_179
};
_179
_179
const remove = () => {
_179
if (pin.value) {
_179
pin.value = pin.value.slice(0, pin.value.length - 1);
_179
}
_179
};
_179
_179
const initSetPasscodeMode = () => {
_179
title.value = 'Create PIN';
_179
prompt.value = 'Create Session PIN';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initUnlockMode = () => {
_179
title.value = 'Unlock';
_179
prompt.value = 'Enter PIN to Unlock';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initVerifyMode = () => {
_179
prompt.value = 'Verify PIN';
_179
verifyPin = pin.value;
_179
pin.value = '';
_179
};
_179
_179
const cancel = () => {
_179
modalController.dismiss(undefined, 'cancel');
_179
};
_179
_179
const submit = () => {
_179
if (props.setPasscodeMode) {
_179
if (!verifyPin) {
_179
initVerifyMode();
_179
} else if (verifyPin === pin.value) {
_179
modalController.dismiss(pin.value);
_179
} else {
_179
errorMessage.value = 'PINs do not match';
_179
initSetPasscodeMode();
_179
}
_179
} else {
_179
modalController.dismiss(pin.value);
_179
}
_179
};
_179
_179
if (props.setPasscodeMode) {
_179
initSetPasscodeMode();
_179
} else {
_179
initUnlockMode();
_179
}
_179
</script>
_179
_179
<style scoped>
_179
.prompt {
_179
font-size: 2rem;
_179
font-weight: bold;
_179
}
_179
_179
.pin {
_179
font-size: 3rem;
_179
font-weight: bold;
_179
}
_179
_179
.error {
_179
font-size: 1.5rem;
_179
font-weight: bold;
_179
}
_179
_179
ion-grid {
_179
padding-bottom: 32px;
_179
}
_179
</style>

When the user taps submit, move on to "verify" mode, display an error, or dismiss the modal returning the entered PIN.

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.

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

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 submit, move on to "verify" mode, display an error, or dismiss the modal returning the entered PIN.

src/components/AppPinDialog.vue

_179
<template>
_179
<ion-header>
_179
<ion-toolbar>
_179
<ion-title>{{ title }}</ion-title>
_179
<ion-buttons v-if="!setPasscodeMode" slot="start">
_179
<ion-button @click="cancel" data-testid="cancel-button">
_179
Cancel
_179
</ion-button>
_179
</ion-buttons>
_179
<ion-buttons slot="end">
_179
<ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter
_179
</ion-button>
_179
</ion-buttons>
_179
</ion-toolbar>
_179
</ion-header>
_179
_179
<ion-content class="ion-padding ion-text-center">
_179
<ion-label data-testid="prompt">
_179
<div class="prompt">{{ prompt }}</div>
_179
</ion-label>
_179
<ion-label data-testid="display-pin">
_179
<div class="pin">{{ displayPin }}</div>
_179
</ion-label>
_179
<ion-label color="danger" data-testid="error-message">
_179
<div class="error">{{ errorMessage }}</div>
_179
</ion-label>
_179
</ion-content>
_179
_179
<ion-footer>
_179
<ion-grid>
_179
<ion-row>
_179
<ion-col v-for="n of [1, 2, 3]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [4, 5, 6]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col v-for="n of [7, 8, 9]" :key="n">
_179
<ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"
_179
data-testclass="number-button">{{ n }}</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
<ion-row>
_179
<ion-col>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"
_179
data-testclass="number-button">0</ion-button>
_179
</ion-col>
_179
<ion-col>
_179
<ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"
_179
data-testid="delete-button">
_179
<ion-icon :icon="backspace"></ion-icon>
_179
</ion-button>
_179
</ion-col>
_179
</ion-row>
_179
</ion-grid>
_179
</ion-footer>
_179
</template>
_179
_179
<script setup lang="ts">
_179
import {
_179
IonButton,
_179
IonButtons,
_179
IonCol,
_179
IonContent,
_179
IonFooter,
_179
IonGrid,
_179
IonHeader,
_179
IonLabel,
_179
IonRow,
_179
IonTitle,
_179
IonToolbar,
_179
modalController,
_179
} from '@ionic/vue';
_179
import { computed, ref } from 'vue';
_179
import { backspace } from 'ionicons/icons';
_179
_179
// eslint-disable-next-line no-undef
_179
const props = defineProps({
_179
setPasscodeMode: Boolean,
_179
});
_179
_179
let verifyPin = '';
_179
_179
const disableDelete = computed(() => !pin.value.length);
_179
const disableEnter = computed(() => !(pin.value.length > 3));
_179
const disableInput = computed(() => pin.value.length > 8);
_179
_179
const errorMessage = ref('');
_179
const pin = ref('');
_179
const prompt = ref('');
_179
const title = ref('');
_179
_179
const displayPin = computed(() => '*********'.slice(0, pin.value.length));
_179
_179
const append = (n: number) => {
_179
errorMessage.value = '';
_179
pin.value = pin.value.concat(n.toString());
_179
};
_179
_179
const remove = () => {
_179
if (pin.value) {
_179
pin.value = pin.value.slice(0, pin.value.length - 1);
_179
}
_179
};
_179
_179
const initSetPasscodeMode = () => {
_179
title.value = 'Create PIN';
_179
prompt.value = 'Create Session PIN';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initUnlockMode = () => {
_179
title.value = 'Unlock';
_179
prompt.value = 'Enter PIN to Unlock';
_179
verifyPin = '';
_179
pin.value = '';
_179
};
_179
_179
const initVerifyMode = () => {
_179
prompt.value = 'Verify PIN';
_179
verifyPin = pin.value;
_179
pin.value = '';
_179
};
_179
_179
const cancel = () => {
_179
modalController.dismiss(undefined, 'cancel');
_179
};
_179
_179
const submit = () => {
_179
if (props.setPasscodeMode) {
_179
if (!verifyPin) {
_179
initVerifyMode();
_179
} else if (verifyPin === pin.value) {
_179
modalController.dismiss(pin.value);
_179
} else {
_179
errorMessage.value = 'PINs do not match';
_179
initSetPasscodeMode();
_179
}
_179
} else {
_179
modalController.dismiss(pin.value);
_179
}
_179
};
_179
_179
if (props.setPasscodeMode) {
_179
initSetPasscodeMode();
_179
} else {
_179
initUnlockMode();
_179
}
_179
</script>
_179
_179
<style scoped>
_179
.prompt {
_179
font-size: 2rem;
_179
font-weight: bold;
_179
}
_179
_179
.pin {
_179
font-size: 3rem;
_179
font-weight: bold;
_179
}
_179
_179
.error {
_179
font-size: 1.5rem;
_179
font-weight: bold;
_179
}
_179
_179
ion-grid {
_179
padding-bottom: 32px;
_179
}
_179
</style>

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/composables/session-vault.ts

_104
import { useVaultFactory } from '@/composables/vault-factory';
_104
import { Session } from '@/models/session';
_104
import {
_104
BrowserVault,
_104
DeviceSecurityType,
_104
IdentityVaultConfig,
_104
Vault,
_104
VaultType,
_104
} from '@ionic-enterprise/identity-vault';
_104
import { modalController } from '@ionic/vue';
_104
import { ref } from 'vue';
_104
import AppPinDialog from '@/components/AppPinDialog.vue';
_104
_104
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_104
_104
const { createVault } = useVaultFactory();
_104
const vault: Vault | BrowserVault = createVault();
_104
const session = ref<Session | null>(null);
_104
_104
const initializeVault = async (): Promise<void> => {
_104
try {
_104
await vault.initialize({
_104
key: 'io.ionic.gettingstartediv',
_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(() => (session.value = null));
_104
_104
vault.onPasscodeRequested(async (isPasscodeSetRequest: boolean) => {
_104
await vault.setCustomPasscode('1234');
_104
});
_104
};
_104
_104
const storeSession = async (s: Session): Promise<void> => {
_104
vault.setValue('session', s);
_104
session.value = s;
_104
};
_104
_104
const getSession = async (): Promise<void> => {
_104
if (await vault.isEmpty()) {
_104
session.value = null;
_104
} else {
_104
session.value = await vault.getValue<Session>('session');
_104
}
_104
};
_104
_104
const clearSession = async (): Promise<void> => {
_104
await vault.clear();
_104
session.value = null;
_104
};
_104
_104
const lockSession = async (): Promise<void> => {
_104
await vault.lock();
_104
session.value = null;
_104
};
_104
_104
const unlockSession = async (): Promise<void> => {
_104
await vault.unlock();
_104
session.value = await vault.getValue<Session>('session');
_104
};
_104
_104
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
const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_104
const type =
_104
mode === 'BiometricsWithPasscode'
_104
? VaultType.DeviceSecurity
_104
: mode === 'CustomPasscode'
_104
? VaultType.CustomPasscode
_104
: mode === 'InMemory'
_104
? VaultType.InMemory
_104
: VaultType.SecureStorage;
_104
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_104
await vault.updateConfig({
_104
...(vault.config as IdentityVaultConfig),
_104
type,
_104
deviceSecurityType,
_104
});
_104
};
_104
_104
export const useSessionVault = (): any => ({
_104
clearSession,
_104
getSession,
_104
initializeVault,
_104
lockSession,
_104
session,
_104
sessionIsLocked,
_104
storeSession,
_104
unlockSession,
_104
updateUnlockMode,
_104
});

Import the modal controller and the PIN dialog component.

src/composables/session-vault.ts

_112
import { useVaultFactory } from '@/composables/vault-factory';
_112
import { Session } from '@/models/session';
_112
import {
_112
BrowserVault,
_112
DeviceSecurityType,
_112
IdentityVaultConfig,
_112
Vault,
_112
VaultType,
_112
} from '@ionic-enterprise/identity-vault';
_112
import { modalController } from '@ionic/vue';
_112
import { ref } from 'vue';
_112
import AppPinDialog from '@/components/AppPinDialog.vue';
_112
_112
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_112
_112
const { createVault } = useVaultFactory();
_112
const vault: Vault | BrowserVault = createVault();
_112
const session = ref<Session | null>(null);
_112
_112
const initializeVault = async (): Promise<void> => {
_112
try {
_112
await vault.initialize({
_112
key: 'io.ionic.gettingstartediv',
_112
type: VaultType.SecureStorage,
_112
deviceSecurityType: DeviceSecurityType.None,
_112
lockAfterBackgrounded: 2000,
_112
});
_112
} catch (e: unknown) {
_112
await vault.clear();
_112
await updateUnlockMode('SecureStorage');
_112
}
_112
_112
vault.onLock(() => (session.value = null));
_112
_112
vault.onPasscodeRequested(async (isPasscodeSetRequest: boolean) => {
_112
const modal = await modalController.create({
_112
component: AppPinDialog,
_112
backdropDismiss: false,
_112
componentProps: {
_112
setPasscodeMode: isPasscodeSetRequest,
_112
},
_112
});
_112
await modal.present();
_112
await vault.setCustomPasscode('1234');
_112
});
_112
};
_112
_112
const storeSession = async (s: Session): Promise<void> => {
_112
vault.setValue('session', s);
_112
session.value = s;
_112
};
_112
_112
const getSession = async (): Promise<void> => {
_112
if (await vault.isEmpty()) {
_112
session.value = null;
_112
} else {
_112
session.value = await vault.getValue<Session>('session');
_112
}
_112
};
_112
_112
const clearSession = async (): Promise<void> => {
_112
await vault.clear();
_112
session.value = null;
_112
};
_112
_112
const lockSession = async (): Promise<void> => {
_112
await vault.lock();
_112
session.value = null;
_112
};
_112
_112
const unlockSession = async (): Promise<void> => {
_112
await vault.unlock();
_112
session.value = await vault.getValue<Session>('session');
_112
};
_112
_112
const sessionIsLocked = async (): Promise<boolean> => {
_112
return (
_112
vault.config?.type !== VaultType.SecureStorage &&
_112
vault.config?.type !== VaultType.InMemory &&
_112
!(await vault.isEmpty()) &&
_112
(await vault.isLocked())
_112
);
_112
};
_112
_112
const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_112
const type =
_112
mode === 'BiometricsWithPasscode'
_112
? VaultType.DeviceSecurity
_112
: mode === 'CustomPasscode'
_112
? VaultType.CustomPasscode
_112
: mode === 'InMemory'
_112
? VaultType.InMemory
_112
: VaultType.SecureStorage;
_112
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_112
await vault.updateConfig({
_112
...(vault.config as IdentityVaultConfig),
_112
type,
_112
deviceSecurityType,
_112
});
_112
};
_112
_112
export const useSessionVault = (): any => ({
_112
clearSession,
_112
getSession,
_112
initializeVault,
_112
lockSession,
_112
session,
_112
sessionIsLocked,
_112
storeSession,
_112
unlockSession,
_112
updateUnlockMode,
_112
});

Create and present the modal.

src/composables/session-vault.ts

_113
import { useVaultFactory } from '@/composables/vault-factory';
_113
import { Session } from '@/models/session';
_113
import {
_113
BrowserVault,
_113
DeviceSecurityType,
_113
IdentityVaultConfig,
_113
Vault,
_113
VaultType,
_113
} from '@ionic-enterprise/identity-vault';
_113
import { modalController } from '@ionic/vue';
_113
import { ref } from 'vue';
_113
import AppPinDialog from '@/components/AppPinDialog.vue';
_113
_113
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_113
_113
const { createVault } = useVaultFactory();
_113
const vault: Vault | BrowserVault = createVault();
_113
const session = ref<Session | null>(null);
_113
_113
const initializeVault = async (): Promise<void> => {
_113
try {
_113
await vault.initialize({
_113
key: 'io.ionic.gettingstartediv',
_113
type: VaultType.SecureStorage,
_113
deviceSecurityType: DeviceSecurityType.None,
_113
lockAfterBackgrounded: 2000,
_113
});
_113
} catch (e: unknown) {
_113
await vault.clear();
_113
await updateUnlockMode('SecureStorage');
_113
}
_113
_113
vault.onLock(() => (session.value = null));
_113
_113
vault.onPasscodeRequested(async (isPasscodeSetRequest: boolean) => {
_113
const modal = await modalController.create({
_113
component: AppPinDialog,
_113
backdropDismiss: false,
_113
componentProps: {
_113
setPasscodeMode: isPasscodeSetRequest,
_113
},
_113
});
_113
await modal.present();
_113
const { data } = await modal.onWillDismiss();
_113
await vault.setCustomPasscode(data || '');
_113
});
_113
};
_113
_113
const storeSession = async (s: Session): Promise<void> => {
_113
vault.setValue('session', s);
_113
session.value = s;
_113
};
_113
_113
const getSession = async (): Promise<void> => {
_113
if (await vault.isEmpty()) {
_113
session.value = null;
_113
} else {
_113
session.value = await vault.getValue<Session>('session');
_113
}
_113
};
_113
_113
const clearSession = async (): Promise<void> => {
_113
await vault.clear();
_113
session.value = null;
_113
};
_113
_113
const lockSession = async (): Promise<void> => {
_113
await vault.lock();
_113
session.value = null;
_113
};
_113
_113
const unlockSession = async (): Promise<void> => {
_113
await vault.unlock();
_113
session.value = await vault.getValue<Session>('session');
_113
};
_113
_113
const sessionIsLocked = async (): Promise<boolean> => {
_113
return (
_113
vault.config?.type !== VaultType.SecureStorage &&
_113
vault.config?.type !== VaultType.InMemory &&
_113
!(await vault.isEmpty()) &&
_113
(await vault.isLocked())
_113
);
_113
};
_113
_113
const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_113
const type =
_113
mode === 'BiometricsWithPasscode'
_113
? VaultType.DeviceSecurity
_113
: mode === 'CustomPasscode'
_113
? VaultType.CustomPasscode
_113
: mode === 'InMemory'
_113
? VaultType.InMemory
_113
: VaultType.SecureStorage;
_113
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_113
await vault.updateConfig({
_113
...(vault.config as IdentityVaultConfig),
_113
type,
_113
deviceSecurityType,
_113
});
_113
};
_113
_113
export const useSessionVault = (): any => ({
_113
clearSession,
_113
getSession,
_113
initializeVault,
_113
lockSession,
_113
session,
_113
sessionIsLocked,
_113
storeSession,
_113
unlockSession,
_113
updateUnlockMode,
_113
});

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.

Create and present the modal.

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

src/composables/session-vault.ts

_104
import { useVaultFactory } from '@/composables/vault-factory';
_104
import { Session } from '@/models/session';
_104
import {
_104
BrowserVault,
_104
DeviceSecurityType,
_104
IdentityVaultConfig,
_104
Vault,
_104
VaultType,
_104
} from '@ionic-enterprise/identity-vault';
_104
import { modalController } from '@ionic/vue';
_104
import { ref } from 'vue';
_104
import AppPinDialog from '@/components/AppPinDialog.vue';
_104
_104
export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';
_104
_104
const { createVault } = useVaultFactory();
_104
const vault: Vault | BrowserVault = createVault();
_104
const session = ref<Session | null>(null);
_104
_104
const initializeVault = async (): Promise<void> => {
_104
try {
_104
await vault.initialize({
_104
key: 'io.ionic.gettingstartediv',
_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(() => (session.value = null));
_104
_104
vault.onPasscodeRequested(async (isPasscodeSetRequest: boolean) => {
_104
await vault.setCustomPasscode('1234');
_104
});
_104
};
_104
_104
const storeSession = async (s: Session): Promise<void> => {
_104
vault.setValue('session', s);
_104
session.value = s;
_104
};
_104
_104
const getSession = async (): Promise<void> => {
_104
if (await vault.isEmpty()) {
_104
session.value = null;
_104
} else {
_104
session.value = await vault.getValue<Session>('session');
_104
}
_104
};
_104
_104
const clearSession = async (): Promise<void> => {
_104
await vault.clear();
_104
session.value = null;
_104
};
_104
_104
const lockSession = async (): Promise<void> => {
_104
await vault.lock();
_104
session.value = null;
_104
};
_104
_104
const unlockSession = async (): Promise<void> => {
_104
await vault.unlock();
_104
session.value = await vault.getValue<Session>('session');
_104
};
_104
_104
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
const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_104
const type =
_104
mode === 'BiometricsWithPasscode'
_104
? VaultType.DeviceSecurity
_104
: mode === 'CustomPasscode'
_104
? VaultType.CustomPasscode
_104
: mode === 'InMemory'
_104
? VaultType.InMemory
_104
: VaultType.SecureStorage;
_104
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_104
await vault.updateConfig({
_104
...(vault.config as IdentityVaultConfig),
_104
type,
_104
deviceSecurityType,
_104
});
_104
};
_104
_104
export const useSessionVault = (): any => ({
_104
clearSession,
_104
getSession,
_104
initializeVault,
_104
lockSession,
_104
session,
_104
sessionIsLocked,
_104
storeSession,
_104
unlockSession,
_104
updateUnlockMode,
_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.