Using Custom Passcode Vaults
Overview
Identity Vault allows you to create a Custom Passcode vault. With a custom passcode vault, your application needs to provide the mechanism used to collect the passcode from the user. The passcode is then used to generate a key that is used to lock and unlock the vault. The passcode is only used to generate a key, it is never stored anywhere by Identity Vault. Since the passcode is not stored, it cannot be stolen.
You can think of it working like this:
- When the vault is created, the user supplies a passcode.
- The passcode is used to generate a key and the passcode is discarded.
- This key becomes part of the current vault configuration so the vault can later be locked.
- When the vault is locked the key is destroyed, resulting in a new key being required to unlock the vault.
- When the user wants to unlock the vault, they supply a passcode.
- The passcode is used to generate a key and the passcode is discarded.
- If the new passcode was the same as the original passcode, the new key matches the old key and unlocks the vault, otherwise it does not.
The important items are: the passcode does not live beyond generating a key and the key does not live beyond locking the vault.
Let's Code
This tutorial builds upon the application created when doing the startup strategies tutorial. If you have the code from when you performed that tutorial, then you are ready to go. If you need the code you can make a copy from our GitHub repository.
Update the Vault Type
_96import { useVaultFactory } from '@/composables/vault-factory';_96import { Session } from '@/models/session';_96import {_96 BrowserVault,_96 DeviceSecurityType,_96 IdentityVaultConfig,_96 Vault,_96 VaultType,_96} from '@ionic-enterprise/identity-vault';_96import { ref } from 'vue';_96_96export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';_96_96const { createVault } = useVaultFactory();_96const vault: Vault | BrowserVault = createVault();_96const session = ref<Session | null>(null);_96_96const 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_96const storeSession = async (s: Session): Promise<void> => {_96 vault.setValue('session', s);_96 session.value = s;_96};_96_96const 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_96const clearSession = async (): Promise<void> => {_96 await vault.clear();_96 session.value = null;_96};_96_96const lockSession = async (): Promise<void> => {_96 await vault.lock();_96 session.value = null;_96};_96_96const unlockSession = async (): Promise<void> => {_96 await vault.unlock();_96 session.value = await vault.getValue<Session>('session');_96};_96_96const 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_96const 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_96export 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
.
_98import { useVaultFactory } from '@/composables/vault-factory';_98import { Session } from '@/models/session';_98import {_98 BrowserVault,_98 DeviceSecurityType,_98 IdentityVaultConfig,_98 Vault,_98 VaultType,_98} from '@ionic-enterprise/identity-vault';_98import { ref } from 'vue';_98_98export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';_98_98const { createVault } = useVaultFactory();_98const vault: Vault | BrowserVault = createVault();_98const session = ref<Session | null>(null);_98_98const 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_98const storeSession = async (s: Session): Promise<void> => {_98 vault.setValue('session', s);_98 session.value = s;_98};_98_98const 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_98const clearSession = async (): Promise<void> => {_98 await vault.clear();_98 session.value = null;_98};_98_98const lockSession = async (): Promise<void> => {_98 await vault.lock();_98 session.value = null;_98};_98_98const unlockSession = async (): Promise<void> => {_98 await vault.unlock();_98 session.value = await vault.getValue<Session>('session');_98};_98_98const 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_98const 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_98export 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.
_102import { useVaultFactory } from '@/composables/vault-factory';_102import { Session } from '@/models/session';_102import {_102 BrowserVault,_102 DeviceSecurityType,_102 IdentityVaultConfig,_102 Vault,_102 VaultType,_102} from '@ionic-enterprise/identity-vault';_102import { ref } from 'vue';_102_102export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';_102_102const { createVault } = useVaultFactory();_102const vault: Vault | BrowserVault = createVault();_102const session = ref<Session | null>(null);_102_102const 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_102const storeSession = async (s: Session): Promise<void> => {_102 vault.setValue('session', s);_102 session.value = s;_102};_102_102const 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_102const clearSession = async (): Promise<void> => {_102 await vault.clear();_102 session.value = null;_102};_102_102const lockSession = async (): Promise<void> => {_102 await vault.lock();_102 session.value = null;_102};_102_102const unlockSession = async (): Promise<void> => {_102 await vault.unlock();_102 session.value = await vault.getValue<Session>('session');_102};_102_102const 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_102const 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_102export 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.
_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">_84import { useAuthentication } from '@/composables/authentication';_84import { useSessionVault } from '@/composables/session-vault';_84import {_84 IonButton,_84 IonContent,_84 IonHeader,_84 IonItem,_84 IonLabel,_84 IonList,_84 IonPage,_84 IonTitle,_84 IonToolbar,_84} from '@ionic/vue';_84import { useRouter } from 'vue-router';_84_84const { logout } = useAuthentication();_84const { session, updateUnlockMode } = useSessionVault();_84const router = useRouter();_84_84const 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.
_96import { useVaultFactory } from '@/composables/vault-factory';_96import { Session } from '@/models/session';_96import {_96 BrowserVault,_96 DeviceSecurityType,_96 IdentityVaultConfig,_96 Vault,_96 VaultType,_96} from '@ionic-enterprise/identity-vault';_96import { ref } from 'vue';_96_96export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';_96_96const { createVault } = useVaultFactory();_96const vault: Vault | BrowserVault = createVault();_96const session = ref<Session | null>(null);_96_96const 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_96const storeSession = async (s: Session): Promise<void> => {_96 vault.setValue('session', s);_96 session.value = s;_96};_96_96const 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_96const clearSession = async (): Promise<void> => {_96 await vault.clear();_96 session.value = null;_96};_96_96const lockSession = async (): Promise<void> => {_96 await vault.lock();_96 session.value = null;_96};_96_96const unlockSession = async (): Promise<void> => {_96 await vault.unlock();_96 session.value = await vault.getValue<Session>('session');_96};_96_96const 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_96const 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_96export 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.
Workflows are used to accomplish two distinct tasks:
- Create the passcode that is used to lock the vault.
- Obtain the passcode that is used to attempt to unlock the vault.
The basic elements for all workflows is:
- Respond to the onPasscodeRequested event.
- Gather the passcode.
- Either use the
onComplete
callback from the onPasscodeRequested event or call setCustomPasscode.
In the next section we will look at one example of a workflow we can use to gather a PIN style passcode from the user.
A PIN Entry Workflow
One type of workflow we can use is a PIN entry workflow. With this workflow, the user will use a Personal Identification Number consisting of four to nine digits to lock and unlock the vault.
When creating a new PIN:
- The user enters a PIN and taps the "Enter" button.
- The user is asked to enter the PIN again to verify.
- The user enters a PIN and taps the "Enter" button.
- If the PINs do not match an error is displayed.
- If the PINs match the modal is closed, passing back the PIN that was entered.
- The calling code passes the PIN to the vault so it can be used to generate the key that is used when the vault locks.
The user cannot cancel the creation of a PIN, they must complete the operation.
When unlocking the vault:
- The user enters a PIN and taps the "Enter" button.
- The modal is closed, passing back the PIN that was entered.
- The calling code passes the PIN to the vault so it can be used to generate a key to unlock the vault.
The user can cancel the PIN entry. If the user cancels, the modal is closed without any data being passed back.
To accomplish this, we will create a PIN Entry Component that encapsulates the user interaction required to enter a PIN. This dialog is displayed in a modal when the vault requests a passcode.
Create a PIN Entry Component
The PIN entry component is a numeric keypad that handles the PIN entry flows for both PIN creation and for unlocking the vault. The full code can be found in our GitHub demo repository. We walk through the key points below.
_187<template>_187 <ion-header>_187 <ion-toolbar>_187 <ion-title>{{ title }}</ion-title>_187 <ion-buttons v-if="!setPasscodeMode" slot="start">_187 <ion-button @click="cancel" data-testid="cancel-button">_187 Cancel_187 </ion-button>_187 </ion-buttons>_187 <ion-buttons slot="end">_187 <ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter_187 </ion-button>_187 </ion-buttons>_187 </ion-toolbar>_187 </ion-header>_187_187 <ion-content class="ion-padding ion-text-center">_187 <ion-label data-testid="prompt">_187 <div class="prompt">{{ prompt }}</div>_187 </ion-label>_187 <ion-label data-testid="display-pin">_187 <div class="pin">{{ displayPin }}</div>_187 </ion-label>_187 <ion-label color="danger" data-testid="error-message">_187 <div class="error">{{ errorMessage }}</div>_187 </ion-label>_187 </ion-content>_187_187 <ion-footer>_187 <ion-grid>_187 <ion-row>_187 <ion-col v-for="n of [1, 2, 3]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [4, 5, 6]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [7, 8, 9]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col>_187 </ion-col>_187 <ion-col>_187 <ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"_187 data-testclass="number-button">0</ion-button>_187 </ion-col>_187 <ion-col>_187 <ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"_187 data-testid="delete-button">_187 <ion-icon :icon="backspace"></ion-icon>_187 </ion-button>_187 </ion-col>_187 </ion-row>_187 </ion-grid>_187 </ion-footer>_187</template>_187_187<script setup lang="ts">_187import {_187 IonButton,_187 IonButtons,_187 IonCol,_187 IonContent,_187 IonFooter,_187 IonGrid,_187 IonHeader,_187 IonLabel,_187 IonRow,_187 IonTitle,_187 IonToolbar,_187 modalController,_187} from '@ionic/vue';_187import { computed, ref } from 'vue';_187import { backspace } from 'ionicons/icons';_187_187// eslint-disable-next-line no-undef_187const props = defineProps({_187 setPasscodeMode: Boolean,_187});_187_187let verifyPin = '';_187_187const disableDelete = computed(() => !pin.value.length);_187const disableEnter = computed(() => !(pin.value.length > 3));_187const disableInput = computed(() => pin.value.length > 8);_187_187const errorMessage = ref('');_187const pin = ref('');_187const prompt = ref('');_187const title = ref('');_187_187const displayPin = computed(() => '*********'.slice(0, pin.value.length));_187_187const append = (n: number) => {_187 errorMessage.value = '';_187 pin.value = pin.value.concat(n.toString());_187};_187_187const remove = () => {_187 if (pin.value) {_187 pin.value = pin.value.slice(0, pin.value.length - 1);_187 }_187};_187_187const handleGetPasscodeFlow = () => {_187 modalController.dismiss(pin.value);_187}_187_187const handleSetPasscodeFlow = () => {_187 if (!verifyPin) {_187 initVerifyMode();_187 } else if (verifyPin === pin.value) {_187 modalController.dismiss(pin.value);_187 } else {_187 errorMessage.value = 'PINs do not match';_187 initSetPasscodeMode();_187 }_187}_187 _187const initSetPasscodeMode = () => {_187 title.value = 'Create PIN';_187 prompt.value = 'Create Session PIN';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initUnlockMode = () => {_187 title.value = 'Unlock';_187 prompt.value = 'Enter PIN to Unlock';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initVerifyMode = () => {_187 prompt.value = 'Verify PIN';_187 verifyPin = pin.value;_187 pin.value = '';_187};_187_187const cancel = () => {_187 modalController.dismiss(undefined, 'cancel');_187};_187_187const submit = () => {_187 if (props.setPasscodeMode) {_187 handleSetPasscodeFlow();_187 } else {_187 handleGetPasscodeFlow();_187 }_187};_187_187if (props.setPasscodeMode) {_187 initSetPasscodeMode();_187} else {_187 initUnlockMode();_187}_187</script>_187_187<style scoped>_187.prompt {_187 font-size: 2rem;_187 font-weight: bold;_187}_187_187.pin {_187 font-size: 3rem;_187 font-weight: bold;_187}_187_187.error {_187 font-size: 1.5rem;_187 font-weight: bold;_187}_187_187ion-grid {_187 padding-bottom: 32px;_187}_187</style>
Cancel and Submit buttons.
Do not allow the user to cancel if they are creating a passcode.
_187<template>_187 <ion-header>_187 <ion-toolbar>_187 <ion-title>{{ title }}</ion-title>_187 <ion-buttons v-if="!setPasscodeMode" slot="start">_187 <ion-button @click="cancel" data-testid="cancel-button">_187 Cancel_187 </ion-button>_187 </ion-buttons>_187 <ion-buttons slot="end">_187 <ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter_187 </ion-button>_187 </ion-buttons>_187 </ion-toolbar>_187 </ion-header>_187_187 <ion-content class="ion-padding ion-text-center">_187 <ion-label data-testid="prompt">_187 <div class="prompt">{{ prompt }}</div>_187 </ion-label>_187 <ion-label data-testid="display-pin">_187 <div class="pin">{{ displayPin }}</div>_187 </ion-label>_187 <ion-label color="danger" data-testid="error-message">_187 <div class="error">{{ errorMessage }}</div>_187 </ion-label>_187 </ion-content>_187_187 <ion-footer>_187 <ion-grid>_187 <ion-row>_187 <ion-col v-for="n of [1, 2, 3]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [4, 5, 6]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [7, 8, 9]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col>_187 </ion-col>_187 <ion-col>_187 <ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"_187 data-testclass="number-button">0</ion-button>_187 </ion-col>_187 <ion-col>_187 <ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"_187 data-testid="delete-button">_187 <ion-icon :icon="backspace"></ion-icon>_187 </ion-button>_187 </ion-col>_187 </ion-row>_187 </ion-grid>_187 </ion-footer>_187</template>_187_187<script setup lang="ts">_187import {_187 IonButton,_187 IonButtons,_187 IonCol,_187 IonContent,_187 IonFooter,_187 IonGrid,_187 IonHeader,_187 IonLabel,_187 IonRow,_187 IonTitle,_187 IonToolbar,_187 modalController,_187} from '@ionic/vue';_187import { computed, ref } from 'vue';_187import { backspace } from 'ionicons/icons';_187_187// eslint-disable-next-line no-undef_187const props = defineProps({_187 setPasscodeMode: Boolean,_187});_187_187let verifyPin = '';_187_187const disableDelete = computed(() => !pin.value.length);_187const disableEnter = computed(() => !(pin.value.length > 3));_187const disableInput = computed(() => pin.value.length > 8);_187_187const errorMessage = ref('');_187const pin = ref('');_187const prompt = ref('');_187const title = ref('');_187_187const displayPin = computed(() => '*********'.slice(0, pin.value.length));_187_187const append = (n: number) => {_187 errorMessage.value = '';_187 pin.value = pin.value.concat(n.toString());_187};_187_187const remove = () => {_187 if (pin.value) {_187 pin.value = pin.value.slice(0, pin.value.length - 1);_187 }_187};_187_187const handleGetPasscodeFlow = () => {_187 modalController.dismiss(pin.value);_187}_187_187const handleSetPasscodeFlow = () => {_187 if (!verifyPin) {_187 initVerifyMode();_187 } else if (verifyPin === pin.value) {_187 modalController.dismiss(pin.value);_187 } else {_187 errorMessage.value = 'PINs do not match';_187 initSetPasscodeMode();_187 }_187}_187_187const initSetPasscodeMode = () => {_187 title.value = 'Create PIN';_187 prompt.value = 'Create Session PIN';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initUnlockMode = () => {_187 title.value = 'Unlock';_187 prompt.value = 'Enter PIN to Unlock';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initVerifyMode = () => {_187 prompt.value = 'Verify PIN';_187 verifyPin = pin.value;_187 pin.value = '';_187};_187_187const cancel = () => {_187 modalController.dismiss(undefined, 'cancel');_187};_187_187const submit = () => {_187 if (props.setPasscodeMode) {_187 handleSetPasscodeFlow();_187 } else {_187 handleGetPasscodeFlow();_187 }_187};_187_187if (props.setPasscodeMode) {_187 initSetPasscodeMode();_187} else {_187 initUnlockMode();_187}_187</script>_187_187<style scoped>_187.prompt {_187 font-size: 2rem;_187 font-weight: bold;_187}_187_187.pin {_187 font-size: 3rem;_187 font-weight: bold;_187}_187_187.error {_187 font-size: 1.5rem;_187 font-weight: bold;_187}_187_187ion-grid {_187 padding-bottom: 32px;_187}_187</style>
A user feedback area so they know what is going on.
_187<template>_187 <ion-header>_187 <ion-toolbar>_187 <ion-title>{{ title }}</ion-title>_187 <ion-buttons v-if="!setPasscodeMode" slot="start">_187 <ion-button @click="cancel" data-testid="cancel-button">_187 Cancel_187 </ion-button>_187 </ion-buttons>_187 <ion-buttons slot="end">_187 <ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter_187 </ion-button>_187 </ion-buttons>_187 </ion-toolbar>_187 </ion-header>_187_187 <ion-content class="ion-padding ion-text-center">_187 <ion-label data-testid="prompt">_187 <div class="prompt">{{ prompt }}</div>_187 </ion-label>_187 <ion-label data-testid="display-pin">_187 <div class="pin">{{ displayPin }}</div>_187 </ion-label>_187 <ion-label color="danger" data-testid="error-message">_187 <div class="error">{{ errorMessage }}</div>_187 </ion-label>_187 </ion-content>_187_187 <ion-footer>_187 <ion-grid>_187 <ion-row>_187 <ion-col v-for="n of [1, 2, 3]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [4, 5, 6]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [7, 8, 9]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col>_187 </ion-col>_187 <ion-col>_187 <ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"_187 data-testclass="number-button">0</ion-button>_187 </ion-col>_187 <ion-col>_187 <ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"_187 data-testid="delete-button">_187 <ion-icon :icon="backspace"></ion-icon>_187 </ion-button>_187 </ion-col>_187 </ion-row>_187 </ion-grid>_187 </ion-footer>_187</template>_187_187<script setup lang="ts">_187import {_187 IonButton,_187 IonButtons,_187 IonCol,_187 IonContent,_187 IonFooter,_187 IonGrid,_187 IonHeader,_187 IonLabel,_187 IonRow,_187 IonTitle,_187 IonToolbar,_187 modalController,_187} from '@ionic/vue';_187import { computed, ref } from 'vue';_187import { backspace } from 'ionicons/icons';_187_187// eslint-disable-next-line no-undef_187const props = defineProps({_187 setPasscodeMode: Boolean,_187});_187_187let verifyPin = '';_187_187const disableDelete = computed(() => !pin.value.length);_187const disableEnter = computed(() => !(pin.value.length > 3));_187const disableInput = computed(() => pin.value.length > 8);_187_187const errorMessage = ref('');_187const pin = ref('');_187const prompt = ref('');_187const title = ref('');_187_187const displayPin = computed(() => '*********'.slice(0, pin.value.length));_187_187const append = (n: number) => {_187 errorMessage.value = '';_187 pin.value = pin.value.concat(n.toString());_187};_187_187const remove = () => {_187 if (pin.value) {_187 pin.value = pin.value.slice(0, pin.value.length - 1);_187 }_187};_187_187const handleGetPasscodeFlow = () => {_187 modalController.dismiss(pin.value);_187}_187_187const handleSetPasscodeFlow = () => {_187 if (!verifyPin) {_187 initVerifyMode();_187 } else if (verifyPin === pin.value) {_187 modalController.dismiss(pin.value);_187 } else {_187 errorMessage.value = 'PINs do not match';_187 initSetPasscodeMode();_187 }_187}_187_187const initSetPasscodeMode = () => {_187 title.value = 'Create PIN';_187 prompt.value = 'Create Session PIN';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initUnlockMode = () => {_187 title.value = 'Unlock';_187 prompt.value = 'Enter PIN to Unlock';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initVerifyMode = () => {_187 prompt.value = 'Verify PIN';_187 verifyPin = pin.value;_187 pin.value = '';_187};_187_187const cancel = () => {_187 modalController.dismiss(undefined, 'cancel');_187};_187_187const submit = () => {_187 if (props.setPasscodeMode) {_187 handleSetPasscodeFlow();_187 } else {_187 handleGetPasscodeFlow();_187 }_187};_187_187if (props.setPasscodeMode) {_187 initSetPasscodeMode();_187} else {_187 initUnlockMode();_187}_187</script>_187_187<style scoped>_187.prompt {_187 font-size: 2rem;_187 font-weight: bold;_187}_187_187.pin {_187 font-size: 3rem;_187 font-weight: bold;_187}_187_187.error {_187 font-size: 1.5rem;_187 font-weight: bold;_187}_187_187ion-grid {_187 padding-bottom: 32px;_187}_187</style>
The keypad.
_187<template>_187 <ion-header>_187 <ion-toolbar>_187 <ion-title>{{ title }}</ion-title>_187 <ion-buttons v-if="!setPasscodeMode" slot="start">_187 <ion-button @click="cancel" data-testid="cancel-button">_187 Cancel_187 </ion-button>_187 </ion-buttons>_187 <ion-buttons slot="end">_187 <ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter_187 </ion-button>_187 </ion-buttons>_187 </ion-toolbar>_187 </ion-header>_187_187 <ion-content class="ion-padding ion-text-center">_187 <ion-label data-testid="prompt">_187 <div class="prompt">{{ prompt }}</div>_187 </ion-label>_187 <ion-label data-testid="display-pin">_187 <div class="pin">{{ displayPin }}</div>_187 </ion-label>_187 <ion-label color="danger" data-testid="error-message">_187 <div class="error">{{ errorMessage }}</div>_187 </ion-label>_187 </ion-content>_187_187 <ion-footer>_187 <ion-grid>_187 <ion-row>_187 <ion-col v-for="n of [1, 2, 3]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [4, 5, 6]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [7, 8, 9]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col>_187 </ion-col>_187 <ion-col>_187 <ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"_187 data-testclass="number-button">0</ion-button>_187 </ion-col>_187 <ion-col>_187 <ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"_187 data-testid="delete-button">_187 <ion-icon :icon="backspace"></ion-icon>_187 </ion-button>_187 </ion-col>_187 </ion-row>_187 </ion-grid>_187 </ion-footer>_187</template>_187_187<script setup lang="ts">_187import {_187 IonButton,_187 IonButtons,_187 IonCol,_187 IonContent,_187 IonFooter,_187 IonGrid,_187 IonHeader,_187 IonLabel,_187 IonRow,_187 IonTitle,_187 IonToolbar,_187 modalController,_187} from '@ionic/vue';_187import { computed, ref } from 'vue';_187import { backspace } from 'ionicons/icons';_187_187// eslint-disable-next-line no-undef_187const props = defineProps({_187 setPasscodeMode: Boolean,_187});_187_187let verifyPin = '';_187_187const disableDelete = computed(() => !pin.value.length);_187const disableEnter = computed(() => !(pin.value.length > 3));_187const disableInput = computed(() => pin.value.length > 8);_187_187const errorMessage = ref('');_187const pin = ref('');_187const prompt = ref('');_187const title = ref('');_187_187const displayPin = computed(() => '*********'.slice(0, pin.value.length));_187_187const append = (n: number) => {_187 errorMessage.value = '';_187 pin.value = pin.value.concat(n.toString());_187};_187_187const remove = () => {_187 if (pin.value) {_187 pin.value = pin.value.slice(0, pin.value.length - 1);_187 }_187};_187_187const handleGetPasscodeFlow = () => {_187 modalController.dismiss(pin.value);_187}_187_187const handleSetPasscodeFlow = () => {_187 if (!verifyPin) {_187 initVerifyMode();_187 } else if (verifyPin === pin.value) {_187 modalController.dismiss(pin.value);_187 } else {_187 errorMessage.value = 'PINs do not match';_187 initSetPasscodeMode();_187 }_187}_187_187const initSetPasscodeMode = () => {_187 title.value = 'Create PIN';_187 prompt.value = 'Create Session PIN';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initUnlockMode = () => {_187 title.value = 'Unlock';_187 prompt.value = 'Enter PIN to Unlock';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initVerifyMode = () => {_187 prompt.value = 'Verify PIN';_187 verifyPin = pin.value;_187 pin.value = '';_187};_187_187const cancel = () => {_187 modalController.dismiss(undefined, 'cancel');_187};_187_187const submit = () => {_187 if (props.setPasscodeMode) {_187 handleSetPasscodeFlow();_187 } else {_187 handleGetPasscodeFlow();_187 }_187};_187_187if (props.setPasscodeMode) {_187 initSetPasscodeMode();_187} else {_187 initUnlockMode();_187}_187</script>_187_187<style scoped>_187.prompt {_187 font-size: 2rem;_187 font-weight: bold;_187}_187_187.pin {_187 font-size: 3rem;_187 font-weight: bold;_187}_187_187.error {_187 font-size: 1.5rem;_187 font-weight: bold;_187}_187_187ion-grid {_187 padding-bottom: 32px;_187}_187</style>
We do not want to display the PIN, so we calculate a string of asterisks instead.
_187<template>_187 <ion-header>_187 <ion-toolbar>_187 <ion-title>{{ title }}</ion-title>_187 <ion-buttons v-if="!setPasscodeMode" slot="start">_187 <ion-button @click="cancel" data-testid="cancel-button">_187 Cancel_187 </ion-button>_187 </ion-buttons>_187 <ion-buttons slot="end">_187 <ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter_187 </ion-button>_187 </ion-buttons>_187 </ion-toolbar>_187 </ion-header>_187_187 <ion-content class="ion-padding ion-text-center">_187 <ion-label data-testid="prompt">_187 <div class="prompt">{{ prompt }}</div>_187 </ion-label>_187 <ion-label data-testid="display-pin">_187 <div class="pin">{{ displayPin }}</div>_187 </ion-label>_187 <ion-label color="danger" data-testid="error-message">_187 <div class="error">{{ errorMessage }}</div>_187 </ion-label>_187 </ion-content>_187_187 <ion-footer>_187 <ion-grid>_187 <ion-row>_187 <ion-col v-for="n of [1, 2, 3]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [4, 5, 6]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [7, 8, 9]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col>_187 </ion-col>_187 <ion-col>_187 <ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"_187 data-testclass="number-button">0</ion-button>_187 </ion-col>_187 <ion-col>_187 <ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"_187 data-testid="delete-button">_187 <ion-icon :icon="backspace"></ion-icon>_187 </ion-button>_187 </ion-col>_187 </ion-row>_187 </ion-grid>_187 </ion-footer>_187</template>_187_187<script setup lang="ts">_187import {_187 IonButton,_187 IonButtons,_187 IonCol,_187 IonContent,_187 IonFooter,_187 IonGrid,_187 IonHeader,_187 IonLabel,_187 IonRow,_187 IonTitle,_187 IonToolbar,_187 modalController,_187} from '@ionic/vue';_187import { computed, ref } from 'vue';_187import { backspace } from 'ionicons/icons';_187_187// eslint-disable-next-line no-undef_187const props = defineProps({_187 setPasscodeMode: Boolean,_187});_187_187let verifyPin = '';_187_187const disableDelete = computed(() => !pin.value.length);_187const disableEnter = computed(() => !(pin.value.length > 3));_187const disableInput = computed(() => pin.value.length > 8);_187_187const errorMessage = ref('');_187const pin = ref('');_187const prompt = ref('');_187const title = ref('');_187_187const displayPin = computed(() => '*********'.slice(0, pin.value.length));_187_187const append = (n: number) => {_187 errorMessage.value = '';_187 pin.value = pin.value.concat(n.toString());_187};_187_187const remove = () => {_187 if (pin.value) {_187 pin.value = pin.value.slice(0, pin.value.length - 1);_187 }_187};_187_187const handleGetPasscodeFlow = () => {_187 modalController.dismiss(pin.value);_187}_187_187const handleSetPasscodeFlow = () => {_187 if (!verifyPin) {_187 initVerifyMode();_187 } else if (verifyPin === pin.value) {_187 modalController.dismiss(pin.value);_187 } else {_187 errorMessage.value = 'PINs do not match';_187 initSetPasscodeMode();_187 }_187}_187_187const initSetPasscodeMode = () => {_187 title.value = 'Create PIN';_187 prompt.value = 'Create Session PIN';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initUnlockMode = () => {_187 title.value = 'Unlock';_187 prompt.value = 'Enter PIN to Unlock';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initVerifyMode = () => {_187 prompt.value = 'Verify PIN';_187 verifyPin = pin.value;_187 pin.value = '';_187};_187_187const cancel = () => {_187 modalController.dismiss(undefined, 'cancel');_187};_187_187const submit = () => {_187 if (props.setPasscodeMode) {_187 handleSetPasscodeFlow();_187 } else {_187 handleGetPasscodeFlow();_187 }_187};_187_187if (props.setPasscodeMode) {_187 initSetPasscodeMode();_187} else {_187 initUnlockMode();_187}_187</script>_187_187<style scoped>_187.prompt {_187 font-size: 2rem;_187 font-weight: bold;_187}_187_187.pin {_187 font-size: 3rem;_187 font-weight: bold;_187}_187_187.error {_187 font-size: 1.5rem;_187 font-weight: bold;_187}_187_187ion-grid {_187 padding-bottom: 32px;_187}_187</style>
We need to update the PIN value when the user presses a number button or the delete button on the keypad.
_187<template>_187 <ion-header>_187 <ion-toolbar>_187 <ion-title>{{ title }}</ion-title>_187 <ion-buttons v-if="!setPasscodeMode" slot="start">_187 <ion-button @click="cancel" data-testid="cancel-button">_187 Cancel_187 </ion-button>_187 </ion-buttons>_187 <ion-buttons slot="end">_187 <ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter_187 </ion-button>_187 </ion-buttons>_187 </ion-toolbar>_187 </ion-header>_187_187 <ion-content class="ion-padding ion-text-center">_187 <ion-label data-testid="prompt">_187 <div class="prompt">{{ prompt }}</div>_187 </ion-label>_187 <ion-label data-testid="display-pin">_187 <div class="pin">{{ displayPin }}</div>_187 </ion-label>_187 <ion-label color="danger" data-testid="error-message">_187 <div class="error">{{ errorMessage }}</div>_187 </ion-label>_187 </ion-content>_187_187 <ion-footer>_187 <ion-grid>_187 <ion-row>_187 <ion-col v-for="n of [1, 2, 3]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [4, 5, 6]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [7, 8, 9]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col>_187 </ion-col>_187 <ion-col>_187 <ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"_187 data-testclass="number-button">0</ion-button>_187 </ion-col>_187 <ion-col>_187 <ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"_187 data-testid="delete-button">_187 <ion-icon :icon="backspace"></ion-icon>_187 </ion-button>_187 </ion-col>_187 </ion-row>_187 </ion-grid>_187 </ion-footer>_187</template>_187_187<script setup lang="ts">_187import {_187 IonButton,_187 IonButtons,_187 IonCol,_187 IonContent,_187 IonFooter,_187 IonGrid,_187 IonHeader,_187 IonLabel,_187 IonRow,_187 IonTitle,_187 IonToolbar,_187 modalController,_187} from '@ionic/vue';_187import { computed, ref } from 'vue';_187import { backspace } from 'ionicons/icons';_187_187// eslint-disable-next-line no-undef_187const props = defineProps({_187 setPasscodeMode: Boolean,_187});_187_187let verifyPin = '';_187_187const disableDelete = computed(() => !pin.value.length);_187const disableEnter = computed(() => !(pin.value.length > 3));_187const disableInput = computed(() => pin.value.length > 8);_187_187const errorMessage = ref('');_187const pin = ref('');_187const prompt = ref('');_187const title = ref('');_187_187const displayPin = computed(() => '*********'.slice(0, pin.value.length));_187_187const append = (n: number) => {_187 errorMessage.value = '';_187 pin.value = pin.value.concat(n.toString());_187};_187_187const remove = () => {_187 if (pin.value) {_187 pin.value = pin.value.slice(0, pin.value.length - 1);_187 }_187};_187_187const handleGetPasscodeFlow = () => {_187 modalController.dismiss(pin.value);_187}_187_187const handleSetPasscodeFlow = () => {_187 if (!verifyPin) {_187 initVerifyMode();_187 } else if (verifyPin === pin.value) {_187 modalController.dismiss(pin.value);_187 } else {_187 errorMessage.value = 'PINs do not match';_187 initSetPasscodeMode();_187 }_187}_187_187const initSetPasscodeMode = () => {_187 title.value = 'Create PIN';_187 prompt.value = 'Create Session PIN';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initUnlockMode = () => {_187 title.value = 'Unlock';_187 prompt.value = 'Enter PIN to Unlock';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initVerifyMode = () => {_187 prompt.value = 'Verify PIN';_187 verifyPin = pin.value;_187 pin.value = '';_187};_187_187const cancel = () => {_187 modalController.dismiss(undefined, 'cancel');_187};_187_187const submit = () => {_187 if (props.setPasscodeMode) {_187 handleSetPasscodeFlow();_187 } else {_187 handleGetPasscodeFlow();_187 }_187};_187_187if (props.setPasscodeMode) {_187 initSetPasscodeMode();_187} else {_187 initUnlockMode();_187}_187</script>_187_187<style scoped>_187.prompt {_187 font-size: 2rem;_187 font-weight: bold;_187}_187_187.pin {_187 font-size: 3rem;_187 font-weight: bold;_187}_187_187.error {_187 font-size: 1.5rem;_187 font-weight: bold;_187}_187_187ion-grid {_187 padding-bottom: 32px;_187}_187</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.
_187<template>_187 <ion-header>_187 <ion-toolbar>_187 <ion-title>{{ title }}</ion-title>_187 <ion-buttons v-if="!setPasscodeMode" slot="start">_187 <ion-button @click="cancel" data-testid="cancel-button">_187 Cancel_187 </ion-button>_187 </ion-buttons>_187 <ion-buttons slot="end">_187 <ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter_187 </ion-button>_187 </ion-buttons>_187 </ion-toolbar>_187 </ion-header>_187_187 <ion-content class="ion-padding ion-text-center">_187 <ion-label data-testid="prompt">_187 <div class="prompt">{{ prompt }}</div>_187 </ion-label>_187 <ion-label data-testid="display-pin">_187 <div class="pin">{{ displayPin }}</div>_187 </ion-label>_187 <ion-label color="danger" data-testid="error-message">_187 <div class="error">{{ errorMessage }}</div>_187 </ion-label>_187 </ion-content>_187_187 <ion-footer>_187 <ion-grid>_187 <ion-row>_187 <ion-col v-for="n of [1, 2, 3]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [4, 5, 6]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [7, 8, 9]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col>_187 </ion-col>_187 <ion-col>_187 <ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"_187 data-testclass="number-button">0</ion-button>_187 </ion-col>_187 <ion-col>_187 <ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"_187 data-testid="delete-button">_187 <ion-icon :icon="backspace"></ion-icon>_187 </ion-button>_187 </ion-col>_187 </ion-row>_187 </ion-grid>_187 </ion-footer>_187</template>_187_187<script setup lang="ts">_187import {_187 IonButton,_187 IonButtons,_187 IonCol,_187 IonContent,_187 IonFooter,_187 IonGrid,_187 IonHeader,_187 IonLabel,_187 IonRow,_187 IonTitle,_187 IonToolbar,_187 modalController,_187} from '@ionic/vue';_187import { computed, ref } from 'vue';_187import { backspace } from 'ionicons/icons';_187_187// eslint-disable-next-line no-undef_187const props = defineProps({_187 setPasscodeMode: Boolean,_187});_187_187let verifyPin = '';_187_187const disableDelete = computed(() => !pin.value.length);_187const disableEnter = computed(() => !(pin.value.length > 3));_187const disableInput = computed(() => pin.value.length > 8);_187_187const errorMessage = ref('');_187const pin = ref('');_187const prompt = ref('');_187const title = ref('');_187_187const displayPin = computed(() => '*********'.slice(0, pin.value.length));_187_187const append = (n: number) => {_187 errorMessage.value = '';_187 pin.value = pin.value.concat(n.toString());_187};_187_187const remove = () => {_187 if (pin.value) {_187 pin.value = pin.value.slice(0, pin.value.length - 1);_187 }_187};_187_187const handleGetPasscodeFlow = () => {_187 modalController.dismiss(pin.value);_187}_187_187const handleSetPasscodeFlow = () => {_187 if (!verifyPin) {_187 initVerifyMode();_187 } else if (verifyPin === pin.value) {_187 modalController.dismiss(pin.value);_187 } else {_187 errorMessage.value = 'PINs do not match';_187 initSetPasscodeMode();_187 }_187}_187_187const initSetPasscodeMode = () => {_187 title.value = 'Create PIN';_187 prompt.value = 'Create Session PIN';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initUnlockMode = () => {_187 title.value = 'Unlock';_187 prompt.value = 'Enter PIN to Unlock';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initVerifyMode = () => {_187 prompt.value = 'Verify PIN';_187 verifyPin = pin.value;_187 pin.value = '';_187};_187_187const cancel = () => {_187 modalController.dismiss(undefined, 'cancel');_187};_187_187const submit = () => {_187 if (props.setPasscodeMode) {_187 handleSetPasscodeFlow();_187 } else {_187 handleGetPasscodeFlow();_187 }_187};_187_187if (props.setPasscodeMode) {_187 initSetPasscodeMode();_187} else {_187 initUnlockMode();_187}_187</script>_187_187<style scoped>_187.prompt {_187 font-size: 2rem;_187 font-weight: bold;_187}_187_187.pin {_187 font-size: 3rem;_187 font-weight: bold;_187}_187_187.error {_187 font-size: 1.5rem;_187 font-weight: bold;_187}_187_187ion-grid {_187 padding-bottom: 32px;_187}_187</style>
When the modal opens, the user is either creating a PIN or entering a PIN to unlock the vault.
_187<template>_187 <ion-header>_187 <ion-toolbar>_187 <ion-title>{{ title }}</ion-title>_187 <ion-buttons v-if="!setPasscodeMode" slot="start">_187 <ion-button @click="cancel" data-testid="cancel-button">_187 Cancel_187 </ion-button>_187 </ion-buttons>_187 <ion-buttons slot="end">_187 <ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter_187 </ion-button>_187 </ion-buttons>_187 </ion-toolbar>_187 </ion-header>_187_187 <ion-content class="ion-padding ion-text-center">_187 <ion-label data-testid="prompt">_187 <div class="prompt">{{ prompt }}</div>_187 </ion-label>_187 <ion-label data-testid="display-pin">_187 <div class="pin">{{ displayPin }}</div>_187 </ion-label>_187 <ion-label color="danger" data-testid="error-message">_187 <div class="error">{{ errorMessage }}</div>_187 </ion-label>_187 </ion-content>_187_187 <ion-footer>_187 <ion-grid>_187 <ion-row>_187 <ion-col v-for="n of [1, 2, 3]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [4, 5, 6]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [7, 8, 9]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col>_187 </ion-col>_187 <ion-col>_187 <ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"_187 data-testclass="number-button">0</ion-button>_187 </ion-col>_187 <ion-col>_187 <ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"_187 data-testid="delete-button">_187 <ion-icon :icon="backspace"></ion-icon>_187 </ion-button>_187 </ion-col>_187 </ion-row>_187 </ion-grid>_187 </ion-footer>_187</template>_187_187<script setup lang="ts">_187import {_187 IonButton,_187 IonButtons,_187 IonCol,_187 IonContent,_187 IonFooter,_187 IonGrid,_187 IonHeader,_187 IonLabel,_187 IonRow,_187 IonTitle,_187 IonToolbar,_187 modalController,_187} from '@ionic/vue';_187import { computed, ref } from 'vue';_187import { backspace } from 'ionicons/icons';_187_187// eslint-disable-next-line no-undef_187const props = defineProps({_187 setPasscodeMode: Boolean,_187});_187_187let verifyPin = '';_187_187const disableDelete = computed(() => !pin.value.length);_187const disableEnter = computed(() => !(pin.value.length > 3));_187const disableInput = computed(() => pin.value.length > 8);_187_187const errorMessage = ref('');_187const pin = ref('');_187const prompt = ref('');_187const title = ref('');_187_187const displayPin = computed(() => '*********'.slice(0, pin.value.length));_187_187const append = (n: number) => {_187 errorMessage.value = '';_187 pin.value = pin.value.concat(n.toString());_187};_187_187const remove = () => {_187 if (pin.value) {_187 pin.value = pin.value.slice(0, pin.value.length - 1);_187 }_187};_187_187const handleGetPasscodeFlow = () => {_187 modalController.dismiss(pin.value);_187}_187_187const handleSetPasscodeFlow = () => {_187 if (!verifyPin) {_187 initVerifyMode();_187 } else if (verifyPin === pin.value) {_187 modalController.dismiss(pin.value);_187 } else {_187 errorMessage.value = 'PINs do not match';_187 initSetPasscodeMode();_187 }_187}_187_187const initSetPasscodeMode = () => {_187 title.value = 'Create PIN';_187 prompt.value = 'Create Session PIN';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initUnlockMode = () => {_187 title.value = 'Unlock';_187 prompt.value = 'Enter PIN to Unlock';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initVerifyMode = () => {_187 prompt.value = 'Verify PIN';_187 verifyPin = pin.value;_187 pin.value = '';_187};_187_187const cancel = () => {_187 modalController.dismiss(undefined, 'cancel');_187};_187_187const submit = () => {_187 if (props.setPasscodeMode) {_187 handleSetPasscodeFlow();_187 } else {_187 handleGetPasscodeFlow();_187 }_187};_187_187if (props.setPasscodeMode) {_187 initSetPasscodeMode();_187} else {_187 initUnlockMode();_187}_187</script>_187_187<style scoped>_187.prompt {_187 font-size: 2rem;_187 font-weight: bold;_187}_187_187.pin {_187 font-size: 3rem;_187 font-weight: bold;_187}_187_187.error {_187 font-size: 1.5rem;_187 font-weight: bold;_187}_187_187ion-grid {_187 padding-bottom: 32px;_187}_187</style>
When the user taps the Enter button, we need to handle the proper flow.
_187<template>_187 <ion-header>_187 <ion-toolbar>_187 <ion-title>{{ title }}</ion-title>_187 <ion-buttons v-if="!setPasscodeMode" slot="start">_187 <ion-button @click="cancel" data-testid="cancel-button">_187 Cancel_187 </ion-button>_187 </ion-buttons>_187 <ion-buttons slot="end">_187 <ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter_187 </ion-button>_187 </ion-buttons>_187 </ion-toolbar>_187 </ion-header>_187_187 <ion-content class="ion-padding ion-text-center">_187 <ion-label data-testid="prompt">_187 <div class="prompt">{{ prompt }}</div>_187 </ion-label>_187 <ion-label data-testid="display-pin">_187 <div class="pin">{{ displayPin }}</div>_187 </ion-label>_187 <ion-label color="danger" data-testid="error-message">_187 <div class="error">{{ errorMessage }}</div>_187 </ion-label>_187 </ion-content>_187_187 <ion-footer>_187 <ion-grid>_187 <ion-row>_187 <ion-col v-for="n of [1, 2, 3]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [4, 5, 6]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [7, 8, 9]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col>_187 </ion-col>_187 <ion-col>_187 <ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"_187 data-testclass="number-button">0</ion-button>_187 </ion-col>_187 <ion-col>_187 <ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"_187 data-testid="delete-button">_187 <ion-icon :icon="backspace"></ion-icon>_187 </ion-button>_187 </ion-col>_187 </ion-row>_187 </ion-grid>_187 </ion-footer>_187</template>_187_187<script setup lang="ts">_187import {_187 IonButton,_187 IonButtons,_187 IonCol,_187 IonContent,_187 IonFooter,_187 IonGrid,_187 IonHeader,_187 IonLabel,_187 IonRow,_187 IonTitle,_187 IonToolbar,_187 modalController,_187} from '@ionic/vue';_187import { computed, ref } from 'vue';_187import { backspace } from 'ionicons/icons';_187_187// eslint-disable-next-line no-undef_187const props = defineProps({_187 setPasscodeMode: Boolean,_187});_187_187let verifyPin = '';_187_187const disableDelete = computed(() => !pin.value.length);_187const disableEnter = computed(() => !(pin.value.length > 3));_187const disableInput = computed(() => pin.value.length > 8);_187_187const errorMessage = ref('');_187const pin = ref('');_187const prompt = ref('');_187const title = ref('');_187_187const displayPin = computed(() => '*********'.slice(0, pin.value.length));_187_187const append = (n: number) => {_187 errorMessage.value = '';_187 pin.value = pin.value.concat(n.toString());_187};_187_187const remove = () => {_187 if (pin.value) {_187 pin.value = pin.value.slice(0, pin.value.length - 1);_187 }_187};_187_187const handleGetPasscodeFlow = () => {_187 modalController.dismiss(pin.value);_187}_187_187const handleSetPasscodeFlow = () => {_187 if (!verifyPin) {_187 initVerifyMode();_187 } else if (verifyPin === pin.value) {_187 modalController.dismiss(pin.value);_187 } else {_187 errorMessage.value = 'PINs do not match';_187 initSetPasscodeMode();_187 }_187}_187_187const initSetPasscodeMode = () => {_187 title.value = 'Create PIN';_187 prompt.value = 'Create Session PIN';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initUnlockMode = () => {_187 title.value = 'Unlock';_187 prompt.value = 'Enter PIN to Unlock';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initVerifyMode = () => {_187 prompt.value = 'Verify PIN';_187 verifyPin = pin.value;_187 pin.value = '';_187};_187_187const cancel = () => {_187 modalController.dismiss(undefined, 'cancel');_187};_187_187const submit = () => {_187 if (props.setPasscodeMode) {_187 handleSetPasscodeFlow();_187 } else {_187 handleGetPasscodeFlow();_187 }_187};_187_187if (props.setPasscodeMode) {_187 initSetPasscodeMode();_187} else {_187 initUnlockMode();_187}_187</script>_187_187<style scoped>_187.prompt {_187 font-size: 2rem;_187 font-weight: bold;_187}_187_187.pin {_187 font-size: 3rem;_187 font-weight: bold;_187}_187_187.error {_187 font-size: 1.5rem;_187 font-weight: bold;_187}_187_187ion-grid {_187 padding-bottom: 32px;_187}_187</style>
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.
_187<template>_187 <ion-header>_187 <ion-toolbar>_187 <ion-title>{{ title }}</ion-title>_187 <ion-buttons v-if="!setPasscodeMode" slot="start">_187 <ion-button @click="cancel" data-testid="cancel-button">_187 Cancel_187 </ion-button>_187 </ion-buttons>_187 <ion-buttons slot="end">_187 <ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter_187 </ion-button>_187 </ion-buttons>_187 </ion-toolbar>_187 </ion-header>_187_187 <ion-content class="ion-padding ion-text-center">_187 <ion-label data-testid="prompt">_187 <div class="prompt">{{ prompt }}</div>_187 </ion-label>_187 <ion-label data-testid="display-pin">_187 <div class="pin">{{ displayPin }}</div>_187 </ion-label>_187 <ion-label color="danger" data-testid="error-message">_187 <div class="error">{{ errorMessage }}</div>_187 </ion-label>_187 </ion-content>_187_187 <ion-footer>_187 <ion-grid>_187 <ion-row>_187 <ion-col v-for="n of [1, 2, 3]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [4, 5, 6]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [7, 8, 9]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col>_187 </ion-col>_187 <ion-col>_187 <ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"_187 data-testclass="number-button">0</ion-button>_187 </ion-col>_187 <ion-col>_187 <ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"_187 data-testid="delete-button">_187 <ion-icon :icon="backspace"></ion-icon>_187 </ion-button>_187 </ion-col>_187 </ion-row>_187 </ion-grid>_187 </ion-footer>_187</template>_187_187<script setup lang="ts">_187import {_187 IonButton,_187 IonButtons,_187 IonCol,_187 IonContent,_187 IonFooter,_187 IonGrid,_187 IonHeader,_187 IonLabel,_187 IonRow,_187 IonTitle,_187 IonToolbar,_187 modalController,_187} from '@ionic/vue';_187import { computed, ref } from 'vue';_187import { backspace } from 'ionicons/icons';_187_187// eslint-disable-next-line no-undef_187const props = defineProps({_187 setPasscodeMode: Boolean,_187});_187_187let verifyPin = '';_187_187const disableDelete = computed(() => !pin.value.length);_187const disableEnter = computed(() => !(pin.value.length > 3));_187const disableInput = computed(() => pin.value.length > 8);_187_187const errorMessage = ref('');_187const pin = ref('');_187const prompt = ref('');_187const title = ref('');_187_187const displayPin = computed(() => '*********'.slice(0, pin.value.length));_187_187const append = (n: number) => {_187 errorMessage.value = '';_187 pin.value = pin.value.concat(n.toString());_187};_187_187const remove = () => {_187 if (pin.value) {_187 pin.value = pin.value.slice(0, pin.value.length - 1);_187 }_187};_187_187const handleGetPasscodeFlow = () => {_187 modalController.dismiss(pin.value);_187}_187_187const handleSetPasscodeFlow = () => {_187 if (!verifyPin) {_187 initVerifyMode();_187 } else if (verifyPin === pin.value) {_187 modalController.dismiss(pin.value);_187 } else {_187 errorMessage.value = 'PINs do not match';_187 initSetPasscodeMode();_187 }_187}_187_187const initSetPasscodeMode = () => {_187 title.value = 'Create PIN';_187 prompt.value = 'Create Session PIN';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initUnlockMode = () => {_187 title.value = 'Unlock';_187 prompt.value = 'Enter PIN to Unlock';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initVerifyMode = () => {_187 prompt.value = 'Verify PIN';_187 verifyPin = pin.value;_187 pin.value = '';_187};_187_187const cancel = () => {_187 modalController.dismiss(undefined, 'cancel');_187};_187_187const submit = () => {_187 if (props.setPasscodeMode) {_187 handleSetPasscodeFlow();_187 } else {_187 handleGetPasscodeFlow();_187 }_187};_187_187if (props.setPasscodeMode) {_187 initSetPasscodeMode();_187} else {_187 initUnlockMode();_187}_187</script>_187_187<style scoped>_187.prompt {_187 font-size: 2rem;_187 font-weight: bold;_187}_187_187.pin {_187 font-size: 3rem;_187 font-weight: bold;_187}_187_187.error {_187 font-size: 1.5rem;_187 font-weight: bold;_187}_187_187ion-grid {_187 padding-bottom: 32px;_187}_187</style>
When entering a PIN to unlock the vault, close the modal returning the entered PIN.
Cancel and Submit buttons.
Do not allow the user to cancel if they are creating a passcode.
A user feedback area so they know what is going on.
The keypad.
We do not want to display the PIN, so we calculate a string of asterisks instead.
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 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.
_187<template>_187 <ion-header>_187 <ion-toolbar>_187 <ion-title>{{ title }}</ion-title>_187 <ion-buttons v-if="!setPasscodeMode" slot="start">_187 <ion-button @click="cancel" data-testid="cancel-button">_187 Cancel_187 </ion-button>_187 </ion-buttons>_187 <ion-buttons slot="end">_187 <ion-button :strong="true" data-testid="submit-button" @click="submit" :disabled="disableEnter">Enter_187 </ion-button>_187 </ion-buttons>_187 </ion-toolbar>_187 </ion-header>_187_187 <ion-content class="ion-padding ion-text-center">_187 <ion-label data-testid="prompt">_187 <div class="prompt">{{ prompt }}</div>_187 </ion-label>_187 <ion-label data-testid="display-pin">_187 <div class="pin">{{ displayPin }}</div>_187 </ion-label>_187 <ion-label color="danger" data-testid="error-message">_187 <div class="error">{{ errorMessage }}</div>_187 </ion-label>_187 </ion-content>_187_187 <ion-footer>_187 <ion-grid>_187 <ion-row>_187 <ion-col v-for="n of [1, 2, 3]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [4, 5, 6]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col v-for="n of [7, 8, 9]" :key="n">_187 <ion-button expand="block" fill="outline" @click="append(n)" :disabled="disableInput"_187 data-testclass="number-button">{{ n }}</ion-button>_187 </ion-col>_187 </ion-row>_187 <ion-row>_187 <ion-col>_187 </ion-col>_187 <ion-col>_187 <ion-button expand="block" fill="outline" @click="append(0)" :disabled="disableInput"_187 data-testclass="number-button">0</ion-button>_187 </ion-col>_187 <ion-col>_187 <ion-button icon-only color="tertiary" expand="block" @click="remove()" :disabled="disableDelete"_187 data-testid="delete-button">_187 <ion-icon :icon="backspace"></ion-icon>_187 </ion-button>_187 </ion-col>_187 </ion-row>_187 </ion-grid>_187 </ion-footer>_187</template>_187_187<script setup lang="ts">_187import {_187 IonButton,_187 IonButtons,_187 IonCol,_187 IonContent,_187 IonFooter,_187 IonGrid,_187 IonHeader,_187 IonLabel,_187 IonRow,_187 IonTitle,_187 IonToolbar,_187 modalController,_187} from '@ionic/vue';_187import { computed, ref } from 'vue';_187import { backspace } from 'ionicons/icons';_187_187// eslint-disable-next-line no-undef_187const props = defineProps({_187 setPasscodeMode: Boolean,_187});_187_187let verifyPin = '';_187_187const disableDelete = computed(() => !pin.value.length);_187const disableEnter = computed(() => !(pin.value.length > 3));_187const disableInput = computed(() => pin.value.length > 8);_187_187const errorMessage = ref('');_187const pin = ref('');_187const prompt = ref('');_187const title = ref('');_187_187const displayPin = computed(() => '*********'.slice(0, pin.value.length));_187_187const append = (n: number) => {_187 errorMessage.value = '';_187 pin.value = pin.value.concat(n.toString());_187};_187_187const remove = () => {_187 if (pin.value) {_187 pin.value = pin.value.slice(0, pin.value.length - 1);_187 }_187};_187_187const handleGetPasscodeFlow = () => {_187 modalController.dismiss(pin.value);_187}_187_187const handleSetPasscodeFlow = () => {_187 if (!verifyPin) {_187 initVerifyMode();_187 } else if (verifyPin === pin.value) {_187 modalController.dismiss(pin.value);_187 } else {_187 errorMessage.value = 'PINs do not match';_187 initSetPasscodeMode();_187 }_187}_187 _187const initSetPasscodeMode = () => {_187 title.value = 'Create PIN';_187 prompt.value = 'Create Session PIN';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initUnlockMode = () => {_187 title.value = 'Unlock';_187 prompt.value = 'Enter PIN to Unlock';_187 verifyPin = '';_187 pin.value = '';_187};_187_187const initVerifyMode = () => {_187 prompt.value = 'Verify PIN';_187 verifyPin = pin.value;_187 pin.value = '';_187};_187_187const cancel = () => {_187 modalController.dismiss(undefined, 'cancel');_187};_187_187const submit = () => {_187 if (props.setPasscodeMode) {_187 handleSetPasscodeFlow();_187 } else {_187 handleGetPasscodeFlow();_187 }_187};_187_187if (props.setPasscodeMode) {_187 initSetPasscodeMode();_187} else {_187 initUnlockMode();_187}_187</script>_187_187<style scoped>_187.prompt {_187 font-size: 2rem;_187 font-weight: bold;_187}_187_187.pin {_187 font-size: 3rem;_187 font-weight: bold;_187}_187_187.error {_187 font-size: 1.5rem;_187 font-weight: bold;_187}_187_187ion-grid {_187 padding-bottom: 32px;_187}_187</style>
Hook Up the Modal
When the vault needs a PIN it will call onPasscodeRequested()
with a callback function that passes a Boolean flag
indicating if the passcode is being used to set a new passcode (true
) or unlock the vault (false
). 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.
_104import { useVaultFactory } from '@/composables/vault-factory';_104import { Session } from '@/models/session';_104import {_104 BrowserVault,_104 DeviceSecurityType,_104 IdentityVaultConfig,_104 Vault,_104 VaultType,_104} from '@ionic-enterprise/identity-vault';_104import { modalController } from '@ionic/vue';_104import { ref } from 'vue';_104import AppPinDialog from '@/components/AppPinDialog.vue';_104_104export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';_104_104const { createVault } = useVaultFactory();_104const vault: Vault | BrowserVault = createVault();_104const session = ref<Session | null>(null);_104_104const 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_104const storeSession = async (s: Session): Promise<void> => {_104 vault.setValue('session', s);_104 session.value = s;_104};_104_104const 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_104const clearSession = async (): Promise<void> => {_104 await vault.clear();_104 session.value = null;_104};_104_104const lockSession = async (): Promise<void> => {_104 await vault.lock();_104 session.value = null;_104};_104_104const unlockSession = async (): Promise<void> => {_104 await vault.unlock();_104 session.value = await vault.getValue<Session>('session');_104};_104_104const 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_104const 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_104export 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.
_112import { useVaultFactory } from '@/composables/vault-factory';_112import { Session } from '@/models/session';_112import {_112 BrowserVault,_112 DeviceSecurityType,_112 IdentityVaultConfig,_112 Vault,_112 VaultType,_112} from '@ionic-enterprise/identity-vault';_112import { modalController } from '@ionic/vue';_112import { ref } from 'vue';_112import AppPinDialog from '@/components/AppPinDialog.vue';_112_112export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';_112_112const { createVault } = useVaultFactory();_112const vault: Vault | BrowserVault = createVault();_112const session = ref<Session | null>(null);_112_112const 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_112const storeSession = async (s: Session): Promise<void> => {_112 vault.setValue('session', s);_112 session.value = s;_112};_112_112const 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_112const clearSession = async (): Promise<void> => {_112 await vault.clear();_112 session.value = null;_112};_112_112const lockSession = async (): Promise<void> => {_112 await vault.lock();_112 session.value = null;_112};_112_112const unlockSession = async (): Promise<void> => {_112 await vault.unlock();_112 session.value = await vault.getValue<Session>('session');_112};_112_112const 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_112const 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_112export 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.
_113import { useVaultFactory } from '@/composables/vault-factory';_113import { Session } from '@/models/session';_113import {_113 BrowserVault,_113 DeviceSecurityType,_113 IdentityVaultConfig,_113 Vault,_113 VaultType,_113} from '@ionic-enterprise/identity-vault';_113import { modalController } from '@ionic/vue';_113import { ref } from 'vue';_113import AppPinDialog from '@/components/AppPinDialog.vue';_113_113export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';_113_113const { createVault } = useVaultFactory();_113const vault: Vault | BrowserVault = createVault();_113const session = ref<Session | null>(null);_113_113const 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_113const storeSession = async (s: Session): Promise<void> => {_113 vault.setValue('session', s);_113 session.value = s;_113};_113_113const 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_113const clearSession = async (): Promise<void> => {_113 await vault.clear();_113 session.value = null;_113};_113_113const lockSession = async (): Promise<void> => {_113 await vault.lock();_113 session.value = null;_113};_113_113const unlockSession = async (): Promise<void> => {_113 await vault.unlock();_113 session.value = await vault.getValue<Session>('session');_113};_113_113const 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_113const 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_113export 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.
_104import { useVaultFactory } from '@/composables/vault-factory';_104import { Session } from '@/models/session';_104import {_104 BrowserVault,_104 DeviceSecurityType,_104 IdentityVaultConfig,_104 Vault,_104 VaultType,_104} from '@ionic-enterprise/identity-vault';_104import { modalController } from '@ionic/vue';_104import { ref } from 'vue';_104import AppPinDialog from '@/components/AppPinDialog.vue';_104_104export type UnlockMode = 'BiometricsWithPasscode' | 'CustomPasscode' | 'InMemory' | 'SecureStorage';_104_104const { createVault } = useVaultFactory();_104const vault: Vault | BrowserVault = createVault();_104const session = ref<Session | null>(null);_104_104const 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_104const storeSession = async (s: Session): Promise<void> => {_104 vault.setValue('session', s);_104 session.value = s;_104};_104_104const 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_104const clearSession = async (): Promise<void> => {_104 await vault.clear();_104 session.value = null;_104};_104_104const lockSession = async (): Promise<void> => {_104 await vault.lock();_104 session.value = null;_104};_104_104const unlockSession = async (): Promise<void> => {_104 await vault.unlock();_104 session.value = await vault.getValue<Session>('session');_104};_104_104const 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_104const 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_104export 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:
- User uses Biometrics to unlock the
Device
vault. - The Passcode is obtained from the unlocked
Device
vault. - The stored Passcode is used to unlock the
CustomPasscode
vault to obtain the session information.
In cases where the user fails to unlock the Device
, the application can use the CustomPasscode
vault directly, asking
the user to enter the passcode in order to unlock the vault.
Best Practices
Favor Device
Vaults
While CustomPasscode
vaults are secure, a Device
vault is able to take full advantage of the device's hardware-based
encryption and secure storage mechanisms. For this reason, Device
vaults should be favored on devices that support them.
For the most secure applications, the following types of vaults are suggested:
- Use a
Device
type vault withDeviceSecurityType.Both
. If the user does not have Biometrics configured, this configuration will fall back to using the System Passcode. - Use an
InMemory
vault if a user has neither Biometrics nor a System Passcode configured on their device.
With this configuration, users who have secured their devices properly will be able to utilize their device security to remain logged in to your application. For users who have not properly secured their devices, your application will still be secure since users will need to provide their application credentials every time the application is locked or launched.
Favor System Passcode Backup
Using a Custom Passcode as backup for Biometrics has the following disadvantages:
- It is non-standard. The standard fallback for Biometrics failing when unlocking the device itself, for example, is to use the System Passcode. As such, that is what the user will expect.
- Using the System Passcode is built in to Identity Vault. As such, it requires far less code in your application.
- Using the System Passcode takes full advantage of the device's encryption hardware area.
As such, while using a Custom Passcode is secure, using a System Passcode as the fallback mechanism for failing Biometrics maximizes both security and usability while minimizing complexity within your application.