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​