Skip to main content

Mobile Package

The Mobile package is an offline-first client application for ID PASS DataCollect that serves as a mobile data collection interface. It operates as a client instance that synchronizes with the DataCollect sync server when connectivity is available, enabling reliable data collection in environments with intermittent or no internet connectivity.

The Mobile package provides a Vue.js-based mobile application for ID PASS DataCollect, built with Capacitor for cross-platform mobile development. It supports both traditional form-based data collection and dynamic app configurations.

Built with Vue 3, Vuetify 3, TypeScript, XState v5, and Capacitor, the mobile application offers a flexible data collection solution for:

  • Dynamic app configuration loading via QR codes or URLs
  • Traditional form-based data collection
  • Offline-first data storage via the @idpass/data-collect-core library (IndexedDB)
  • Cross-platform mobile deployment (iOS/Android)

Key Features

  • Cross-Platform: Native mobile apps for iOS and Android using Capacitor
  • Dynamic App Loading: Load app configurations via QR code scanning or URL input
  • Form Builder Integration: Full FormIO integration for dynamic form creation
  • Offline Storage: IndexedDB-backed local storage via @idpass/data-collect-core
  • Secure Authentication: JWT-based authentication with OAuth provider support (Auth0, Keycloak), managed by XState v5 state machines
  • Biometric App Lock: Device biometric authentication (fingerprint/face) via XState-driven lock machine and @aparajita/capacitor-biometric-auth
  • Secure Storage: Sensitive credentials stored with @aparajita/capacitor-secure-storage
  • Multi-Provider Auth: Support for multiple authentication providers per app configuration
  • QR Code Scanning: Built-in barcode scanning for app configuration loading
  • Material Design 3 UI: Vuetify 3 components for a consistent, mobile-optimized interface
  • Error Overlay: Global error handling with user-facing snackbar notifications

Technology Stack

LayerTechnology
UI FrameworkVue 3 + Vuetify 3 (Material Design 3)
State ManagementXState v5 (authMachine, lockMachine) + Pinia
StorageIndexedDB via @idpass/data-collect-core
Platform BridgeCapacitor 6
Biometrics@aparajita/capacitor-biometric-auth
Secure Storage@aparajita/capacitor-secure-storage
FormsFormIO (@formio/vue, formiojs)
AuthJWT + OAuth (Auth0 / Keycloak)

Architecture

Core Features

Dynamic App Configuration

The mobile app supports loading app configurations dynamically:

  • QR Code Scanning: Scan QR codes to load app configurations
  • URL Input: Manually enter app configuration URLs
  • Local Storage: Store multiple app configurations locally using IndexedDB via @idpass/data-collect-core
  • App Switching: Switch between different app configurations

Form-Based Data Collection

Traditional form-based data collection:

  • FormIO Integration: Full FormIO form builder support
  • Dynamic Forms: Render forms based on configuration
  • Data Validation: Client-side and server-side validation
  • File Upload: Support for image and document uploads

Offline Capabilities

Offline-first data collection:

  • IndexedDB Storage: Local data persistence via the @idpass/data-collect-core library's IndexedDbStorageAdapter
  • Sync Management: Background synchronization when online
  • Conflict Resolution: Handle data conflicts during sync via the core library's event-sourcing model

Biometric App Lock (v2.0.0)

The mobile app protects sensitive beneficiary data with a biometric app lock that activates automatically after a period of inactivity. The lock is managed by a dedicated lockMachine (XState v5) that persists state across app restarts.

How it works

When the app starts on a native device (iOS or Android), AppLockService.init() loads the last persisted lock state from secure storage. If the app was previously locked (or has no persisted state — the safe default for cold starts), the LockScreen overlay is shown immediately and the user must authenticate before accessing any data.

While unlocked, the lockMachine runs an inactivity timer. Any user interaction (pointer or touch event) resets the timer via AppLockService.resetInactivityTimer(). After 5 minutes of inactivity the machine transitions to the locked state automatically.

Authentication is handled by the @aparajita/capacitor-biometric-auth plugin. The plugin uses whatever the device has available — fingerprint, face recognition, or the device screen lock PIN/pattern/passcode — controlled by the allowDeviceCredential: true flag. On devices with no screen lock configured the lock remains engaged and cannot be bypassed, protecting data on unsecured devices.

The feature is native-platform only. On web builds Capacitor.isNativePlatform() returns false, so the lock machine transitions directly to unlocked on init and all biometric calls are no-ops.

XState lock machine states

idle → initializing → locked ←→ authenticating
↘ unlocked (5 min timer → locked)
StateDescription
idleMachine created but not yet started
initializingLoading persisted lock state from secure storage
lockedApp locked; persists isLocked=true; waits for AUTHENTICATE event
authenticatingBiometric prompt shown; transitions to unlocked on success or back to locked on failure/cancel
unlockedNormal operation; inactivity timer running; transitions to locked after 5 minutes or on explicit LOCK event

AppLockService API

The AppLockService singleton wraps the XState actor and exposes a Vue-reactive API:

import { AppLockService } from '@/services/AppLockService'

// Initialise on app mount (loads persisted lock state)
await AppLockService.init()

// Reactive boolean — use in Vue templates
AppLockService.locked.value // true when locked

// Trigger biometric prompt programmatically
const success = await AppLockService.authenticate()

// Lock immediately (e.g. on a "lock now" button)
await AppLockService.lock()

// Reset the inactivity timer on user activity
AppLockService.resetInactivityTimer()

// Check whether biometrics are available on this device
const available = await AppLockService.isAvailable()

Root component integration

App.vue shows the LockScreen overlay when the machine is in the locked state and calls resetInactivityTimer on all user interactions:

<template>
<v-app @pointerdown="AppLockService.resetInactivityTimer()">
<LockScreen v-if="AppLockService.locked.value" />
<!-- rest of app -->
</v-app>
</template>

<script setup lang="ts">
import { onMounted } from 'vue'
import { AppLockService } from '@/services/AppLockService'

onMounted(async () => {
await AppLockService.init()
})
</script>

Inactivity timeout

The timeout is defined as a constant in lockMachine.ts:

const INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000  // 5 minutes

To change the timeout, modify this constant. Future versions may expose it as a configurable setting.

XState Auth/Lock Flows (v2.0.0)

Authentication and lock state are managed by two XState v5 state machines:

  • authMachine: Handles the full auth lifecycle — initialization, username/password login, OAuth callback handling, token refresh, and logout
  • lockMachine: Handles inactivity-based locking, biometric unlock attempts, and persisted lock state

Authentication

Authentication Setup

The mobile app authentication is driven by the authMachine XState state machine and exposed via the useAuthStore Pinia store.

Username/Password Authentication

import { useAuthStore } from '@/store/auth';

const authStore = useAuthStore();

// Login with credentials
await authStore.loginSyncServer(syncServerUrl, {
email: 'user@example.com',
password: 'password123'
});

OAuth Provider Authentication

// Auth0 login via authMachine
const auth0Config = {
type: 'auth0',
fields: {
domain: config.domain,
client_id: config.client_id,
scope: 'openid profile email'
}
};

// The authMachine actor handles the OAuth flow and callback
authStore.send({ type: 'LOGIN', provider: 'auth0' });

Authentication Components

LoginView.vue

The login view uses Vuetify 3 components for a Material Design layout:

<template>
<v-container class="fill-height">
<v-row justify="center" align="center" class="fill-height">
<v-col cols="12" sm="8" md="5" lg="4">
<v-form @submit.prevent="onLogin">
<v-text-field
v-model="form.email"
label="Email"
type="email"
required
/>
<v-text-field
v-model="form.password"
label="Password"
type="password"
required
/>
<v-btn type="submit" color="secondary" variant="flat" size="large">
Login
</v-btn>
</v-form>
</v-col>
</v-row>
</v-container>
</template>

Route Guards

Authentication state from the authMachine is used to protect routes:

// Route guard for authentication
export const authGuard = async (to: any, from: any, next: any) => {
const authStore = useAuthStore();

if (authStore.isAuthenticated) {
next();
} else {
next('/login');
}
};

Quick Start

Installation

cd ./packages/mobile
pnpm install

Development Setup

Create a .env file:

# Database Configuration
VITE_DB_ENCRYPTION_PASSWORD=password
VITE_FEATURE_DYNAMIC=true
VITE_DEVELOP=true

Development

pnpm run dev

The mobile app will be available at http://localhost:8081

Mobile Development

# Build for iOS
pnpm run build:ios

# Build for Android
pnpm run build:android

Application Structure

Root Component

App.vue

The root component initializes the AppLockService and renders the Vuetify application shell with bottom navigation, offline indicator, and global overlays:

<template>
<v-app>
<LockScreen v-if="AppLockService.locked" />
<AppSnackbar />
<!-- ... bottom navigation, router view ... -->
</v-app>
</template>

<script setup lang="ts">
import { onMounted } from 'vue';
import { AppLockService } from '@/services/AppLockService';

onMounted(async () => {
await AppLockService.init();
});
</script>

Views

ViewRouteDescription
HomeView.vue/App list and QR scanner entry point
AppView.vue/app/:idDynamic form rendering for a loaded app config
LoginView.vue/login/:idSync server credential login
DetailView.vue/app/:id/entity/:eidRead-only entity detail
EditView.vue/app/:id/entity/:eid/editEntity edit form
SettingsView.vue/settingsApp settings and biometric lock toggle
ToolsView.vue/toolsDeveloper/diagnostic tools

Key Components

LockScreen.vue

Full-screen overlay rendered by the lockMachine when the app is locked. Uses Vuetify 3 cards and buttons with Material Design icons:

<template>
<div class="lock-screen">
<v-card elevation="0" width="320" class="text-center pa-6" rounded="lg">
<v-icon size="64" color="primary" class="mb-4">mdi-lock-outline</v-icon>
<v-btn color="primary" variant="flat" @click="unlock">Unlock</v-btn>
</v-card>
</div>
</template>

AppSnackbar.vue

Global error and notification overlay driven by the useSnackbar composable. Surfaces errors from auth failures, sync issues, and form validation via v-snackbar.

QrScanner

QR code scanning for app configuration loading via @capacitor-mlkit/barcode-scanning:

<template>
<QrScanner
@scan="handleScan"
@error="handleError"
/>
</template>

SaveDialog

Modal dialog for saving data using Vuetify 3's v-dialog:

<template>
<v-dialog v-model="open">
<v-card>
<v-card-title>{{ title }}</v-card-title>
<v-card-actions>
<v-btn @click="onSave">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>

Environment Variables

# Feature Flags
VITE_DB_ENCRYPTION_PASSWORD=password
VITE_FEATURE_DYNAMIC=true
VITE_DEVELOP=true

Capacitor Configuration

const config: CapacitorConfig = {
appId: "org.idpass.selfreg",
appName: "ID PASS DataCollect",
webDir: "dist",
server: {
cleartext: true,
},
android: {
allowMixedContent: true,
},
plugins: {
CapacitorHttp: {
enabled: true
}
}
};

Data Schemas

Tenant App Schema

interface TenantAppData {
id: string;
name: string;
description?: string;
configuration: any;
authConfigs?: AuthConfig[];
createdAt: Date;
updatedAt: Date;
}

Auth Config Schema

interface AuthConfig {
type: 'auth0' | 'keycloak' | 'otp' | 'id';
fields: {
domain?: string; // Auth0 domain
clientId: string; // Client ID
audience?: string; // API audience
scope?: string; // OAuth scope
url?: string; // Keycloak URL
realm?: string; // Keycloak realm
};
}

User Session Schema

interface UserSession {
id: string;
userId: string;
email: string;
role: string;
token: string;
expiresAt: Date;
provider?: string;
createdAt: Date;
}

Mobile Features

Capacitor Plugins

  • @capacitor-mlkit/barcode-scanning: QR/barcode scanning for app config loading
  • @aparajita/capacitor-biometric-auth: Biometric authentication for app lock
  • @aparajita/capacitor-secure-storage: Secure storage for lock state and sensitive data
  • Camera: Fallback camera access
  • Haptics: Tactile feedback
  • Keyboard: Mobile keyboard handling
  • Status Bar: Status bar customization

Platform-Specific Features

  • Android: Native Android integration, APK build scripts available (build-apk.sh)
  • iOS: Native iOS integration via Xcode

Testing

Unit Tests

pnpm run test

Component Testing

pnpm run test:ui

End-to-End Tests

pnpm run test:e2e

Mobile Testing

# iOS Simulator
pnpm run build:ios

# Android Emulator
pnpm run build:android

Deployment

Mobile App Stores

  • iOS App Store: Submit to Apple App Store
  • Google Play Store: Submit to Google Play Store

Web Deployment

pnpm run build
pnpm run preview

Capacitor Build

# iOS
pnpm run build:ios

# Android
pnpm run build:android

# Android APK (debug)
pnpm run build:android:apk

# Android APK (release)
pnpm run build:android:apk:release

Browser Support

  • Chrome 88+
  • Firefox 85+
  • Safari 14+
  • Edge 88+

Mobile Platform Support

  • Android: API level 21+
  • iOS: iOS 13+

Performance

  • Lazy loading of app configurations
  • Optimized form rendering
  • Efficient local database operations via IndexedDB
  • Background sync capabilities
  • Image optimization for mobile

Next Steps