Skip to main content
Version: 5.0

Handling Application Startup

Overview

When we created the application for the getting started tutorial we made sure Identity Vault was properly initialized before we used it. However, we just jumped right into the first tab of the main part of the app. This is not a realistic experience for our users. Let's implement something more realistic.

The Login Page

Our application currently just starts right up in the application itself and we have a button that the user can press to store the authentication information in the vault. This is not realistic. Our application should have a page where the user logs in.

In our case, this will still just be a button that the user presses to fake a log in, but we are getting a step closer to an actual flow by having the login page.

The Startup Flow

When our application starts, the session can be in one of the following states:

  1. Locked:
    1. With valid authentication tokens.
    2. With invalid authentication tokens.
  2. Not logged in.

If the application is locked, the application shall give the user the opportunity to unlock the vault. If the unlock fails, the user shall be given the option to either try again or to clear the session data and log in again.

If the user unlocks the vault and the resulting authentication information is valid, the first tab shall be loaded. For our tutorial application, if we have session data the session is, by definition, valid.

If the user unlocks the vault and the resulting authentication information is expired, the login page shall be loaded. Having expired or otherwise invalid authentication information is not technically possible in our tutorial application, but we will code for it none the less.

If the user is not logged in, the login page shall be loaded.

The Unlock on Resume Flow

It is also possible for the vault to lock while the application is in the background. In reality, Identity Vault detects that the application has resumed and will lock the vault if more than lockAfterBackgrounded milliseconds have passed.

One flow we may want to implement in this case is to give the user the opportunity to unlock the vault immediately upon receiving an onLock event for the vault. In this scenario, the user stays on whichever page they were on when the app locked.

If the user cancels the unlock, however, we should navigate to an "Unlock" page where we give the user various options as to what they would like to do next (these are usually to attempt to unlock again, or to log out and back in to the application).

important

If you are using lockAfterBackgrounded do not interact with the vault in a resume event handler. Doing so will cause a race condition resulting in indeterminate behavior. Always manage the state of the vault through the onLock and onUnlock event handlers, never the resume event.

Let's Code

We will build upon the application we created in the getting started tutorial in order to implement the basic application startup workflow along with the unlocking upon resume flow.

If you have the code from when you performed the getting started tutorial, then you are good to go. If you need the code you can make a copy from our GitHub repository.

Create the New Pages

In order to implement our startup and authentication strategies, we need to have a Login page. We will also replace the "default" page (currently the Tab1 page) with a Start page that will contain our startup logic.

Create basic shells for these pages.

src/pages/Login.tsx
src/pages/Start.tsx
src/pages/Unlock.tsx

_23
import {
_23
IonContent,
_23
IonHeader,
_23
IonPage,
_23
IonTitle,
_23
IonToolbar,
_23
} from '@ionic/react';
_23
_23
const Login = () => {
_23
return (
_23
<IonPage>
_23
<IonHeader>
_23
<IonToolbar>
_23
<IonTitle>Login</IonTitle>
_23
</IonToolbar>
_23
</IonHeader>
_23
<IonContent>
_23
</IonContent>
_23
</IonPage>
_23
);
_23
};
_23
_23
export default Login;

Be sure to check the code for all three pages. All of the pages, especially the Start page, are very minimal and that is intentional.

Update Routes

With the new pages in place, the routing needs to be fixed. The current routes are fairly flat with the tabs being defined in src/App.tsx and all of the routes existing directly under the root route: /tab1, /tab2, /tab3. The root (/) currently redirects to /tab1. As we walk through this step, we will move the following code to its own component and modify it for our needs.

src/App.tsx

_78
import { Redirect, Route } from 'react-router-dom';
_78
import {
_78
IonApp,
_78
IonIcon,
_78
IonLabel,
_78
IonRouterOutlet,
_78
IonTabBar,
_78
IonTabButton,
_78
IonTabs,
_78
setupIonicReact,
_78
} from '@ionic/react';
_78
import { IonReactRouter } from '@ionic/react-router';
_78
import { ellipse, square, triangle } from 'ionicons/icons';
_78
import Tab1 from './pages/Tab1';
_78
import Tab2 from './pages/Tab2';
_78
import Tab3 from './pages/Tab3';
_78
_78
/* Core CSS required for Ionic components to work properly */
_78
import '@ionic/react/css/core.css';
_78
_78
/* Basic CSS for apps built with Ionic */
_78
import '@ionic/react/css/normalize.css';
_78
import '@ionic/react/css/structure.css';
_78
import '@ionic/react/css/typography.css';
_78
_78
/* Optional CSS utils that can be commented out */
_78
import '@ionic/react/css/padding.css';
_78
import '@ionic/react/css/float-elements.css';
_78
import '@ionic/react/css/text-alignment.css';
_78
import '@ionic/react/css/text-transformation.css';
_78
import '@ionic/react/css/flex-utils.css';
_78
import '@ionic/react/css/display.css';
_78
_78
import '@ionic/react/css/palettes/dark.system.css';
_78
<IonIcon aria-hidden="true" icon={triangle} />

We want to have /, /login, and /unlock with / loading the Start page. We also want to have a /tabs route to collect the tabbed pages: /tabs/tab1, /tabs/tab2, /tabs/tab3.

src/components/tabs.tsx
src/App.tsx

_43
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
_43
import { ellipse, square, triangle } from 'ionicons/icons';
_43
import { Redirect, Route } from 'react-router-dom';
_43
import Tab1 from '../pages/Tab1';
_43
import Tab2 from '../pages/Tab2';
_43
import Tab3 from '../pages/Tab3';
_43
_43
const Tabs = () => {
_43
return (
_43
<IonTabs>
_43
<IonRouterOutlet>
_43
<Route exact path="/tab1">
_43
<Tab1 />
_43
</Route>
_43
<Route exact path="/tab2">
_43
<Tab2 />
_43
</Route>
_43
<Route exact path="/tab3">
_43
<Tab3 />
_43
</Route>
_43
<Route exact path="/">
_43
<Redirect to="/tab1" />
_43
</Route>
_43
</IonRouterOutlet>
_43
<IonTabBar slot="bottom">
_43
<IonTabButton tab="tab1" href="/tab1">
_43
<IonIcon aria-hidden="true" icon={triangle} />
_43
<IonLabel>Tab 1</IonLabel>
_43
</IonTabButton>
_43
<IonTabButton tab="tab2" href="/tab2">
_43
<IonIcon aria-hidden="true" icon={ellipse} />
_43
<IonLabel>Tab 2</IonLabel>
_43
</IonTabButton>
_43
<IonTabButton tab="tab3" href="/tab3">
_43
<IonIcon aria-hidden="true" icon={square} />
_43
<IonLabel>Tab 3</IonLabel>
_43
</IonTabButton>
_43
</IonTabBar>
_43
</IonTabs>
_43
);
_43
}
_43
_43
export default Tabs;

Move the tabs from src/App.tsx into their own component in src/components/Tabs.tsx

src/components/tabs.tsx
src/App.tsx

_45
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
_45
import { ellipse, square, triangle } from 'ionicons/icons';
_45
import { Redirect, Route, useRouteMatch } from 'react-router-dom';
_45
import Tab1 from '../pages/Tab1';
_45
import Tab2 from '../pages/Tab2';
_45
import Tab3 from '../pages/Tab3';
_45
_45
const Tabs = () => {
_45
const { url } = useRouteMatch();
_45
_45
return (
_45
<IonTabs>
_45
<IonRouterOutlet>
_45
<Route exact path={`${url}/tab1`}>
_45
<Tab1 />
_45
</Route>
_45
<Route exact path={`${url}/tab2`}>
_45
<Tab2 />
_45
</Route>
_45
<Route exact path={`${url}/tab3`}>
_45
<Tab3 />
_45
</Route>
_45
<Route exact path="/">
_45
<Redirect to={`${url}/tab1`} />
_45
</Route>
_45
</IonRouterOutlet>
_45
<IonTabBar slot="bottom">
_45
<IonTabButton tab="tab1" href={`${url}/tab1`}>
_45
<IonIcon aria-hidden="true" icon={triangle} />
_45
<IonLabel>Tab 1</IonLabel>
_45
</IonTabButton>
_45
<IonTabButton tab="tab2" href={`${url}/tab2`}>
_45
<IonIcon aria-hidden="true" icon={ellipse} />
_45
<IonLabel>Tab 2</IonLabel>
_45
</IonTabButton>
_45
<IonTabButton tab="tab3" href={`${url}/tab3`}>
_45
<IonIcon aria-hidden="true" icon={square} />
_45
<IonLabel>Tab 3</IonLabel>
_45
</IonTabButton>
_45
</IonTabBar>
_45
</IonTabs>
_45
);
_45
}
_45
_45
export default Tabs;

Adjust the paths to now be child paths.

src/components/tabs.tsx
src/App.tsx

_57
import {
_57
IonApp,
_57
IonRouterOutlet,
_57
setupIonicReact,
_57
} from '@ionic/react';
_57
import { IonReactRouter } from '@ionic/react-router';
_57
import { Route } from 'react-router-dom';
_57
import Tabs from './components/Tabs';
_57
import Login from './pages/Login';
_57
import Start from './pages/Start';
_57
import Unlock from './pages/Unlock';
_57
_57
/* Core CSS required for Ionic components to work properly */
_57
import '@ionic/react/css/core.css';
_57
_57
/* Basic CSS for apps built with Ionic */
_57
import '@ionic/react/css/normalize.css';
_57
import '@ionic/react/css/structure.css';
_57
import '@ionic/react/css/typography.css';
_57
_57
/* Optional CSS utils that can be commented out */
_57
import '@ionic/react/css/padding.css';
_57
import '@ionic/react/css/float-elements.css';
_57
import '@ionic/react/css/text-alignment.css';
_57
import '@ionic/react/css/text-transformation.css';
_57
import '@ionic/react/css/flex-utils.css';
_57
import '@ionic/react/css/display.css';
_57
_57
import '@ionic/react/css/palettes/dark.system.css';
_57
_57
/* Theme variables */
_57
import './theme/variables.css';
_57
_57
setupIonicReact();
_57
_57
const App: React.FC = () => (
_57
<IonApp>
_57
<IonReactRouter>
_57
<IonRouterOutlet>
_57
<Route exact path="/login">
_57
<Login />
_57
</Route>
_57
<Route exact path="/">
_57
<Start />
_57
</Route>
_57
<Route exact path="/unlock">
_57
<Unlock />
_57
</Route>
_57
<Route path="/tabs">
_57
<Tabs />
_57
</Route>
_57
</IonRouterOutlet>
_57
</IonReactRouter>
_57
</IonApp>
_57
);
_57
_57
export default App;

Add the base-level routes to src/App.tsx, including the parent /tabs route.

Move the tabs from src/App.tsx into their own component in src/components/Tabs.tsx

Adjust the paths to now be child paths.

Add the base-level routes to src/App.tsx, including the parent /tabs route.

src/components/tabs.tsx
src/App.tsx

_43
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
_43
import { ellipse, square, triangle } from 'ionicons/icons';
_43
import { Redirect, Route } from 'react-router-dom';
_43
import Tab1 from '../pages/Tab1';
_43
import Tab2 from '../pages/Tab2';
_43
import Tab3 from '../pages/Tab3';
_43
_43
const Tabs = () => {
_43
return (
_43
<IonTabs>
_43
<IonRouterOutlet>
_43
<Route exact path="/tab1">
_43
<Tab1 />
_43
</Route>
_43
<Route exact path="/tab2">
_43
<Tab2 />
_43
</Route>
_43
<Route exact path="/tab3">
_43
<Tab3 />
_43
</Route>
_43
<Route exact path="/">
_43
<Redirect to="/tab1" />
_43
</Route>
_43
</IonRouterOutlet>
_43
<IonTabBar slot="bottom">
_43
<IonTabButton tab="tab1" href="/tab1">
_43
<IonIcon aria-hidden="true" icon={triangle} />
_43
<IonLabel>Tab 1</IonLabel>
_43
</IonTabButton>
_43
<IonTabButton tab="tab2" href="/tab2">
_43
<IonIcon aria-hidden="true" icon={ellipse} />
_43
<IonLabel>Tab 2</IonLabel>
_43
</IonTabButton>
_43
<IonTabButton tab="tab3" href="/tab3">
_43
<IonIcon aria-hidden="true" icon={square} />
_43
<IonLabel>Tab 3</IonLabel>
_43
</IonTabButton>
_43
</IonTabBar>
_43
</IonTabs>
_43
);
_43
}
_43
_43
export default Tabs;

The authentication Utility Functions

Part of our startup strategy involves authentication. We will not really be performing authentication, but we will add the utility functions so that we have the infrastructure in place so we can later add authentication via a solution such as Auth Connect.

src/util/authentication.ts
src/util/session-vault.ts

_10
import { clearSession, restoreSession, storeSession } from './session-vault';

We will need to clear, store, and restore sessions from here.

src/util/authentication.ts
src/util/session-vault.ts

_10
import { clearSession, restoreSession, storeSession } from './session-vault';
_10
_10
export const login = (): Promise<void> =>
_10
storeSession({
_10
email: 'test@ionic.io',
_10
firstName: 'Tessa',
_10
lastName: 'Testsmith',
_10
accessToken: '4abf1d79-143c-4b89-b478-19607eb5ce97',
_10
refreshToken: '565111b6-66c3-4527-9238-6ea2cc017126',
_10
});

For the login(), store some fake session information in the vault.

src/util/authentication.ts
src/util/session-vault.ts

_12
import { clearSession, restoreSession, storeSession } from './session-vault';
_12
_12
export const login = (): Promise<void> =>
_12
storeSession({
_12
email: 'test@ionic.io',
_12
firstName: 'Tessa',
_12
lastName: 'Testsmith',
_12
accessToken: '4abf1d79-143c-4b89-b478-19607eb5ce97',
_12
refreshToken: '565111b6-66c3-4527-9238-6ea2cc017126',
_12
});
_12
_12
export const logout = (): Promise<void> => clearSession();

For the logout(), clear the session data from the vault.

src/util/authentication.ts
src/util/session-vault.ts

_16
import { clearSession, restoreSession, storeSession } from './session-vault';
_16
_16
export const login = (): Promise<void> =>
_16
storeSession({
_16
email: 'test@ionic.io',
_16
firstName: 'Tessa',
_16
lastName: 'Testsmith',
_16
accessToken: '4abf1d79-143c-4b89-b478-19607eb5ce97',
_16
refreshToken: '565111b6-66c3-4527-9238-6ea2cc017126',
_16
});
_16
_16
export const logout = (): Promise<void> => clearSession();
_16
_16
export const isAuthenticated = async (): Promise<boolean> => {
_16
return !!(await restoreSession());
_16
};

To determine if the user is currently authenticated, make sure the session has been restored, and if there is session data resolve true, otherwise resolve false.

src/util/authentication.ts
src/util/session-vault.ts

_82
import {
_82
BrowserVault,
_82
Vault,
_82
VaultType,
_82
DeviceSecurityType,
_82
IdentityVaultConfig,
_82
} from '@ionic-enterprise/identity-vault';
_82
import { createVault } from './vault-factory';
_82
import { Session } from '../models/Session';
_82
import { setState } from './session-store';
_82
_82
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_82
_82
const vault: Vault | BrowserVault = createVault();
_82
_82
export const initializeVault = async (): Promise<void> => {
_82
try {
_82
await vault.initialize({
_82
key: 'io.ionic.gettingstartedivreact',
_82
type: VaultType.SecureStorage,
_82
deviceSecurityType: DeviceSecurityType.None,
_82
lockAfterBackgrounded: 2000,
_82
});
_82
} catch (e: unknown) {
_82
await vault.clear();
_82
await updateUnlockMode('SecureStorage');
_82
}
_82
_82
vault.onLock(() => {
_82
setState({ session: null });
_82
});
_82
_82
await restoreSession();
_82
};
_82
_82
export const storeSession = async (session: Session): Promise<void> => {
_82
vault.setValue('session', session);
_82
setState({ session });
_82
};
_82
_82
export const restoreSession = async (): Promise<Session | null> => {
_82
let session: Session | null = null;
_82
if (!(await vault.isEmpty())) {
_82
session = await vault.getValue<Session>('session');
_82
}
_82
setState({ session });
_82
return session;
_82
};
_82
_82
export const clearSession = async (): Promise<void> => {
_82
await vault.clear();
_82
setState({ session: null });
_82
};
_82
_82
export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_82
const newConfig = { ...(vault.config as IdentityVaultConfig) };
_82
_82
switch (mode) {
_82
case 'BiometricsWithPasscode': {
_82
newConfig.type = VaultType.DeviceSecurity;
_82
newConfig.deviceSecurityType = DeviceSecurityType.Both;
_82
break;
_82
}
_82
case 'InMemory': {
_82
newConfig.type = VaultType.InMemory;
_82
newConfig.deviceSecurityType = DeviceSecurityType.None;
_82
break;
_82
}
_82
default: {
_82
newConfig.type = VaultType.SecureStorage;
_82
newConfig.deviceSecurityType = DeviceSecurityType.None;
_82
break;
_82
}
_82
}
_82
_82
await vault.updateConfig(newConfig);
_82
};
_82
_82
export const lockSession = async (): Promise<void> => {
_82
await vault.lock();
_82
setState({ session: null });
_82
};

Doing so requires a minor change to restoreSession() so it resolves the currently stored session rather than void.

We will need to clear, store, and restore sessions from here.

For the login(), store some fake session information in the vault.

For the logout(), clear the session data from the vault.

To determine if the user is currently authenticated, make sure the session has been restored, and if there is session data resolve true, otherwise resolve false.

Doing so requires a minor change to restoreSession() so it resolves the currently stored session rather than void.

src/util/authentication.ts
src/util/session-vault.ts

_10
import { clearSession, restoreSession, storeSession } from './session-vault';

We now have a set of authentication utility functions that we can use in the rest of our app. This also gives us the stubs for the type of functions we need when we begin using an actual authentication solution such as Auth Connect.

The Login Page

Add a "Login" button to the Login page.

src/pages/Login.tsx

_35
import {
_35
IonButton,
_35
IonContent,
_35
IonHeader,
_35
IonItem,
_35
IonLabel,
_35
IonList,
_35
IonPage,
_35
IonTitle,
_35
IonToolbar,
_35
} from '@ionic/react';
_35
_35
const Login = () => {
_35
return (
_35
<IonPage>
_35
<IonHeader>
_35
<IonToolbar>
_35
<IonTitle>Login</IonTitle>
_35
</IonToolbar>
_35
</IonHeader>
_35
<IonContent>
_35
<IonList>
_35
<IonItem>
_35
<IonLabel>
_35
<IonButton expand="block">
_35
Login
_35
</IonButton>
_35
</IonLabel>
_35
</IonItem>
_35
</IonList>
_35
</IonContent>
_35
</IonPage>
_35
);
_35
};
_35
</script>

When the button is pressed the following tasks are performed:

  • Attempt to log in.
  • If the login succeeds, go to the Tab1Page.
  • If the login fails we will just log it for now. When actual authentication is implemented this may be a good place to display a "Login Failed" message, but that is beyond the scope of this tutorial.
src/pages/Login.tsx

_49
import {
_49
IonButton,
_49
IonContent,
_49
IonHeader,
_49
IonItem,
_49
IonLabel,
_49
IonList,
_49
IonPage,
_49
IonTitle,
_49
IonToolbar,
_49
} from '@ionic/react';
_49
import { useHistory } from 'react-router-dom';
_49
import { login } from '../util/authentication';
_49
_49
const Login = () => {
_49
const history = useHistory();
_49
_49
const loginClicked = async (): Promise<void> => {
_49
try {
_49
await login();
_49
history.replace('/tabs/tab1');
_49
} catch (error: unknown) {
_49
console.error('Failed to log in', error);
_49
}
_49
};
_49
_49
return (
_49
<IonPage>
_49
<IonHeader>
_49
<IonToolbar>
_49
<IonTitle>Login</IonTitle>
_49
<IonButton expand="block" onClick={loginClicked}>

Update the session-vault Utility Functions

Remove the call to restoreSession() at the end of the initializeVault() function. We will now be restoring the session via various flows within our application and should not do it during initialization any more.

src/utils/session-vault.ts

_91
import {
_91
BrowserVault,
_91
Vault,
_91
VaultType,
_91
DeviceSecurityType,
_91
IdentityVaultConfig,
_91
} from '@ionic-enterprise/identity-vault';
_91
import { createVault } from './vault-factory';
_91
import { Session } from '../models/Session';
_91
import { setState } from './session-store';
_91
_91
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_91
_91
const vault: Vault | BrowserVault = createVault();
_91
_91
export const initializeVault = async (): Promise<void> => {
_91
try {
_91
await vault.initialize({
_91
key: 'io.ionic.gettingstartedivreact',
_91
type: VaultType.SecureStorage,
_91
deviceSecurityType: DeviceSecurityType.None,
_91
lockAfterBackgrounded: 2000,
_91
});
_91
} catch (e: unknown) {
_91
await vault.clear();
_91
await updateUnlockMode('SecureStorage');
_91
}
_91
_91
vault.onLock(() => {
_91
setState({ session: null });
_91
});
_91
_91
await restoreSession(); // Remove this line
_91
};

The startup logic needs to determine if the vault is currently locked and provide a mechanism to unlock the vault if it is locked. Add a sessionIsLocked() function for use in the startup logic.

src/utils/session-vault.ts

_91
import {
_91
BrowserVault,
_91
Vault,
_91
VaultType,
_91
DeviceSecurityType,
_91
IdentityVaultConfig,
_91
} from '@ionic-enterprise/identity-vault';
_91
import { createVault } from './vault-factory';
_91
import { Session } from '../models/Session';
_91
import { setState } from './session-store';
_91
_91
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_91
_91
const vault: Vault | BrowserVault = createVault();
_91
_91
export const initializeVault = async (): Promise<void> => {
_91
try {
_91
export const sessionIsLocked = async (): Promise<boolean> => {

In the sessionIsLocked() method, we do not get the reported state for vaults of type SecureStorage or InMemory because Identity Vault may report them as "locked" even though they logically cannot lock. This is a long standing quirk with Identity Vault that would be a breaking change to fix.

The StartPage

When the application starts, it will be in one of two states:

  • Logged out: the user needs to be redirected to the login page.
  • Logged in and locked: the user should be given the opportunity to unlock the app.

As such, the StartPage does not contain any real user interaction so the UI can be minimal. You could show a spinner, or your company logo, or something like that. For our simple app we will just show an empty component.

src/pages/Start.tsx

_10
const Start = () => {
_10
return <></>;
_10
};
_10
_10
export default Start;

We start with a minimal, empty component for a page.

src/pages/Start.tsx

_18
import { useHistory } from 'react-router-dom';
_18
import { isAuthenticated } from '../util/authentication';
_18
_18
const Start = () => {
_18
const history = useHistory();
_18
_18
const performNavigation = async () => {
_18
if (await isAuthenticated()) {
_18
history.replace('/tabs/tab1');
_18
} else {
_18
history.replace('/login');
_18
}
_18
};
_18
_18
return <></>;
_18
};
_18
_18
export default Start;

The Start page navigates based on the current authentication status. If the user is authenticated, go to the main page of the app, otherwise to go to the login page.

Note that determining if the user is authenticated will try to unlock the vault if it is currently locked.

src/pages/Start.tsx

_20
import { useHistory } from 'react-router-dom';
_20
import { isAuthenticated } from '../util/authentication';
_20
_20
const Start = () => {
_20
const history = useHistory();
_20
_20
const performNavigation = async () => {
_20
if (await isAuthenticated()) {
_20
history.replace('/tabs/tab1');
_20
} else {
_20
history.replace('/login');
_20
}
_20
};
_20
_20
performNavigation().catch(() => history.replace('/unlock'));
_20
_20
return <></>;
_20
};
_20
_20
export default Start;

Perform the previously defined navigation. If it fails it is likely because the user failed to unlock the vault, so navigate to the unlock page, giving the user an opportunity to try again.

We start with a minimal, empty component for a page.

The Start page navigates based on the current authentication status. If the user is authenticated, go to the main page of the app, otherwise to go to the login page.

Note that determining if the user is authenticated will try to unlock the vault if it is currently locked.

Perform the previously defined navigation. If it fails it is likely because the user failed to unlock the vault, so navigate to the unlock page, giving the user an opportunity to try again.

src/pages/Start.tsx

_10
const Start = () => {
_10
return <></>;
_10
};
_10
_10
export default Start;

The UnlockPage

The Unlock page is visited after the user chooses to cancel the unlocking of the vault elsewhere in the application. The purpose of this page is to give the user reasonable options for what they can try next: either re-try the unlock or completely redo their login.

src/pages/Unlock.tsx

_23
import {
_23
IonContent,
_23
IonHeader,
_23
IonPage,
_23
IonTitle,
_23
IonToolbar,
_23
} from '@ionic/react';
_23
_23
const Unlock = () => {
_23
return (
_23
<IonPage>
_23
<IonHeader>
_23
<IonToolbar>
_23
<IonTitle>Unlock</IonTitle>
_23
</IonToolbar>
_23
</IonHeader>
_23
<IonContent class="ion-padding">
_23
</IonContent>
_23
</IonPage>
_23
);
_23
};
_23
_23
export default Unlock;

We start with a minimal page.

src/pages/Unlock.tsx

_47
import {
_47
IonButton,
_47
IonContent,
_47
IonHeader,
_47
IonItem,
_47
IonLabel,
_47
IonList,
_47
IonPage,
_47
IonTitle,
_47
IonToolbar,
_47
} from '@ionic/react';
_47
_47
const Unlock = () => {
_47
const unlock = async () => { };
_47
_47
const redoLogin = async () => { };
_47
_47
return (
_47
<IonPage>
_47
<IonHeader>
_47
<IonToolbar>
_47
<IonTitle>Unlock</IonTitle>
_47
</IonToolbar>
_47
</IonHeader>
_47
<IonContent class="ion-padding">
_47
<IonList>
_47
<IonItem>
_47
<IonLabel>
_47
<IonButton expand="block" onClick={unlock}>
_47
Unlock
_47
</IonButton>
_47
</IonLabel>
_47
</IonItem>
_47
<IonItem>
_47
<IonLabel>
_47
<IonButton expand="block" onClick={redoLogin}>
_47
Login
_47
</IonButton>
_47
</IonLabel>
_47
</IonItem>
_47
</IonList>
_47
</IonContent>
_47
</IonPage>
_47
);
_47
};
_47
_47
export default Unlock;

At this point, the user has two options: try to unlock the vault again or redo the login.

src/pages/Unlock.tsx

_52
import {
_52
IonButton,
_52
IonContent,
_52
IonHeader,
_52
IonItem,
_52
IonLabel,
_52
IonList,
_52
IonPage,
_52
IonTitle,
_52
IonToolbar,
_52
} from '@ionic/react';
_52
import { logout } from '../util/authentication';
_52
import { useHistory } from 'react-router';
_52
_52
const Unlock = () => {
_52
const history = useHistory();
_52
_52
const unlock = async () => { };
_52
_52
const redoLogin = async () => {
_52
await logout();
_52
history.replace('/login');
_52
};
_52
_52
return (
_52
<IonPage>
_52
<IonHeader>
_52
<IonToolbar>
_52
<IonTitle>Unlock</IonTitle>
_52
</IonToolbar>
_52
</IonHeader>
_52
<IonContent class="ion-padding">
_52
<IonList>
_52
<IonItem>
_52
<IonLabel>
_52
<IonButton expand="block" onClick={unlock}>
_52
Unlock
_52
</IonButton>
_52
</IonLabel>
_52
</IonItem>
_52
<IonItem>
_52
<IonLabel>
_52
<IonButton expand="block" onClick={redoLogin}>
_52
Login
_52
</IonButton>
_52
</IonLabel>
_52
</IonItem>
_52
</IonList>
_52
</IonContent>
_52
</IonPage>
_52
);
_52
};

To redo the login, logout and then navigate to the Login Page.

src/pages/Unlock.tsx

_60
import {
_60
IonButton,
_60
IonContent,
_60
IonHeader,
_60
IonItem,
_60
IonLabel,
_60
IonList,
_60
IonPage,
_60
IonTitle,
_60
IonToolbar,
_60
} from '@ionic/react';
_60
import { logout } from '../util/authentication';
_60
import { useHistory } from 'react-router';
_60
import { restoreSession } from '../util/session-vault';
_60
_60
const Unlock = () => {
_60
const history = useHistory();
_60
_60
const unlock = async () => {
_60
try {
_60
await restoreSession();
_60
history.replace('/tabs/tab1');
_60
} catch (err: unknown) {
_60
null;
_60
}
_60
};
_60
_60
const redoLogin = async () => {
_60
await logout();
_60
history.replace('/login');
_60
};
_60
_60
return (
_60
<IonPage>
_60
<IonHeader>
_60
<IonToolbar>
_60
<IonTitle>Unlock</IonTitle>
_60
</IonToolbar>
_60
</IonHeader>
_60
<IonContent class="ion-padding">
_60
<IonList>
_60
<IonItem>
_60
<IonLabel>
_60
<IonButton expand="block" onClick={unlock}>
_60
Unlock
_60
</IonButton>
_60
</IonLabel>
_60
</IonItem>
_60
<IonItem>
_60
<IonLabel>
_60
<IonButton expand="block" onClick={redoLogin}>
_60
Login
_60
</IonButton>
_60
</IonLabel>
_60
</IonItem>
_60
</IonList>
_60
</IonContent>
_60
</IonPage>
_60
);
_60
};

Try to unlock the vault and navigate to the first page of the app if successful.

We start with a minimal page.

At this point, the user has two options: try to unlock the vault again or redo the login.

To redo the login, logout and then navigate to the Login Page.

Try to unlock the vault and navigate to the first page of the app if successful.

src/pages/Unlock.tsx

_23
import {
_23
IonContent,
_23
IonHeader,
_23
IonPage,
_23
IonTitle,
_23
IonToolbar,
_23
} from '@ionic/react';
_23
_23
const Unlock = () => {
_23
return (
_23
<IonPage>
_23
<IonHeader>
_23
<IonToolbar>
_23
<IonTitle>Unlock</IonTitle>
_23
</IonToolbar>
_23
</IonHeader>
_23
<IonContent class="ion-padding">
_23
</IonContent>
_23
</IonPage>
_23
);
_23
};
_23
_23
export default Unlock;

Handle a Vault Locking In-App

The vault will lock upon resume if the application has been in the background more than lockAfterBackgrounded milliseconds. In our case, that is 2000 milliseconds. When this happens, it would be nice to initiate an initial unlock attempt without navigating the user away from wherever they are in the app. The Tabs component encapsulates all of the routes that the user is locked out of when the vault is locked, so this is a natural location for this logic.

Upon locking, the session will be set to null. Update the Tabs component to attempt an unlock any time the vault locks. If the unlocking of the vault is canceled by the user, navigate to the Unlock page. This process represents our component synchronizing with an external system, so use a useEffect hook.

src/components/Tabs.tsx

_59
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
_59
import { ellipse, square, triangle } from 'ionicons/icons';
_59
import { useEffect } from 'react';
_59
import { Redirect, Route, useHistory, useRouteMatch } from 'react-router-dom';
_59
import Tab1 from '../pages/Tab1';
_59
import Tab2 from '../pages/Tab2';
_59
import Tab3 from '../pages/Tab3';
_59
import { useSession } from '../util/session-store';
_59
import { restoreSession, sessionIsLocked } from '../util/session-vault';
_59
_59
const Tabs = () => {
_59
const { url } = useRouteMatch();
_59
_59
const history = useHistory();
_59
const { session } = useSession();
_59
_59
useEffect(() => {
_59
if (!session) {
_59
sessionIsLocked()
_59
.then((x) => (x ? restoreSession() : undefined))
_59
.catch(() => history.replace('/unlock'));
_59
}
_59
}, [session, history]);
_59
_59
return (
_59
<IonTabs>
_59
<IonRouterOutlet>
_59
<Route exact path={`${url}/tab1`}>
_59
<Tab1 />
_59
</Route>

Cleanup the Tab1 Page

There are several items in the Tab1 page that no longer make sense. Now is a good time to clean those up. Here is a synopsis of what needs to be cleaned up:

  • Remove the "Store" button and all code associated with it.
  • Change the "Clear" button to a "Logout" button and update the click handler accordingly.
  • Remove the "Lock" and "Unlock" buttons and all code associated with them.

Cleaning this all up is left as an exercise to the reader but we provide the completed code here for you to compare against.

src/pages/Tab1.tsx

_90
import {
_90
IonButton,
_90
IonContent,
_90
IonHeader,
_90
IonItem,
_90
IonLabel,
_90
IonList,
_90
IonPage,
_90
IonTitle,
_90
IonToolbar,
_90
} from '@ionic/react';
_90
import { useHistory } from 'react-router';
_90
import { logout } from '../util/authentication';
_90
import { useSession } from '../util/session-store';
_90
import { updateUnlockMode } from '../util/session-vault';
_90
import './Tab1.css';
_90
_90
const Tab1: React.FC = () => {
_90
const history = useHistory();
_90
const { session } = useSession();
_90
_90
const logoutClicked = async () => {
_90
await logout();
_90
history.replace('/login');
_90
};
_90
_90
return (
_90
<IonPage>
_90
<IonHeader>
_90
<IonToolbar>
_90
<IonTitle>Tab 1</IonTitle>
_90
</IonToolbar>
_90
</IonHeader>
_90
<IonContent fullscreen>
_90
<IonHeader collapse="condense">
_90
<IonToolbar>
_90
<IonTitle size="large">Tab 1</IonTitle>
_90
</IonToolbar>
_90
</IonHeader>
_90
_90
<IonList>
_90
<IonItem>
_90
<IonLabel>
_90
<IonButton expand="block" color="secondary" onClick={() => updateUnlockMode('BiometricsWithPasscode')}>
_90
Use Biometrics
_90
</IonButton>
_90
</IonLabel>
_90
</IonItem>
_90
_90
<IonItem>
_90
<IonLabel>
_90
<IonButton expand="block" color="secondary" onClick={() => updateUnlockMode('InMemory')}>
_90
Use In Memory
_90
</IonButton>
_90
</IonLabel>
_90
</IonItem>
_90
_90
<IonItem>
_90
<IonLabel>
_90
<IonButton expand="block" color="secondary" onClick={() => updateUnlockMode('SecureStorage')}>
_90
Use Secure Storage
_90
</IonButton>
_90
</IonLabel>
_90
</IonItem>
_90
_90
<IonItem>
_90
<IonLabel>
_90
<IonButton expand="block" color="danger" onClick={logoutClicked}>
_90
Logout
_90
</IonButton>
_90
</IonLabel>
_90
</IonItem>
_90
_90
<IonItem>
_90
<div>
_90
<div>{session?.email}</div>
_90
<div>
_90
{session?.firstName} {session?.lastName}
_90
</div>
_90
<div>{session?.accessToken}</div>
_90
<div>{session?.refreshToken}</div>
_90
</div>
_90
</IonItem>
_90
</IonList>
_90
</IonContent>
_90
</IonPage>
_90
);
_90
};
_90
_90
export default Tab1;

Next Steps

In this tutorial, we created a basic application startup workflow. This is an example of a good workflow, but it is not the only potential flow. Tailor it for your application.