How to Build a HIPAA-Compliant Mobile Health App with Offline Sync and Push Notifications
Complete technical guide to building HIPAA-compliant mobile health applications with React Native, including offline-first architecture, encrypted data storage, and secure push notifications.
Building HIPAA-Compliant Mobile Health Apps
Creating a mobile health app that meets HIPAA compliance requirements while delivering excellent user experience requires careful attention to security, privacy, and data integrity. This comprehensive guide walks you through building a production-ready mHealth app with React Native, focusing on offline-first architecture, encrypted data storage, and HIPAA-compliant push notifications.
Table of Contents
- HIPAA Compliance Fundamentals
- Project Setup and Architecture
- Encrypted Local Storage
- Offline-First Data Sync
- HIPAA-Compliant Push Notifications
- Authentication and Authorization
- Audit Logging
- Apple Health and Google Fit Integration
- App Store Compliance
- Testing and Validation
HIPAA Compliance Fundamentals
Key HIPAA Requirements for Mobile Apps
HIPAA (Health Insurance Portability and Accountability Act) has specific technical safeguards for mobile applications handling Protected Health Information (PHI):
1. Access Controls (Β§ 164.312(a)(1))
- Unique user identification
- Emergency access procedures
- Automatic logoff
- Encryption and decryption
2. Audit Controls (Β§ 164.312(b))
- Log all PHI access and modifications
- Record user actions with timestamps
- Maintain tamper-proof audit trails
3. Integrity Controls (Β§ 164.312(c)(1))
- Protect PHI from improper alteration or destruction
- Implement data validation mechanisms
4. Transmission Security (Β§ 164.312(e)(1))
- Encrypt PHI in transit
- Use TLS 1.2 or higher
- Implement integrity controls
Understanding PHI
Protected Health Information includes:
- Patient names, addresses, dates
- Medical record numbers
- Health plan beneficiary numbers
- Device identifiers
- Biometric identifiers
- Full-face photographs
- Any other unique identifying information
Mobile-Specific Risks
Mobile apps face unique security challenges:
- Device loss or theft
- Unsecured Wi-Fi networks
- Screenshot/screen recording capabilities
- Clipboard data exposure
- Background data access
- Third-party keyboard logging
Project Setup and Architecture
Initialize React Native Project
# Create new React Native project
npx react-native init MHealthApp --template react-native-template-typescript
cd MHealthApp
# Install core dependencies
npm install \
@react-native-async-storage/async-storage \
@react-native-community/netinfo \
@react-native-firebase/app \
@react-native-firebase/messaging \
react-native-encrypted-storage \
react-native-background-fetch \
react-native-keychain \
react-native-health \
react-native-google-fit \
axios \
date-fns
# Install development dependencies
npm install --save-dev \
@types/react-native \
@testing-library/react-native \
@testing-library/jest-native \
detox
Project Structure
src/
βββ config/
β βββ hipaa.config.ts # HIPAA compliance settings
β βββ security.config.ts # Security configurations
β βββ api.config.ts # API endpoints
βββ services/
β βββ encryption/
β β βββ EncryptionService.ts
β β βββ KeyManager.ts
β βββ storage/
β β βββ SecureStorage.ts
β β βββ OfflineQueue.ts
β βββ sync/
β β βββ SyncEngine.ts
β β βββ ConflictResolver.ts
β βββ audit/
β β βββ AuditLogger.ts
β β βββ AuditTrail.ts
β βββ auth/
β β βββ AuthService.ts
β β βββ BiometricAuth.ts
β βββ notifications/
β βββ PushNotificationService.ts
β βββ LocalNotificationService.ts
βββ components/
β βββ SecureTextInput.tsx
β βββ BiometricPrompt.tsx
β βββ SessionTimeout.tsx
βββ screens/
β βββ Login/
β βββ Dashboard/
β βββ PatientData/
βββ hooks/
β βββ useSecureStorage.ts
β βββ useOfflineSync.ts
β βββ useAuditLog.ts
βββ types/
β βββ patient.types.ts
β βββ audit.types.ts
β βββ sync.types.ts
βββ utils/
βββ validation.ts
βββ sanitization.ts
βββ deviceInfo.ts
Encrypted Local Storage
Encryption Service Implementation
// src/services/encryption/EncryptionService.ts
import CryptoJS from 'crypto-js';
import EncryptedStorage from 'react-native-encrypted-storage';
import * as Keychain from 'react-native-keychain';
export class EncryptionService {
private static encryptionKey: string | null = null;
/**
* Initialize encryption with device-specific key
* Key is stored in device secure enclave (iOS Keychain/Android Keystore)
*/
static async initialize(): Promise<void> {
try {
// Try to retrieve existing encryption key
const credentials = await Keychain.getGenericPassword({
service: 'com.mhealthapp.encryption',
});
if (credentials) {
this.encryptionKey = credentials.password;
} else {
// Generate new 256-bit encryption key
this.encryptionKey = this.generateEncryptionKey();
// Store in secure hardware-backed storage
await Keychain.setGenericPassword(
'encryption_key',
this.encryptionKey,
{
service: 'com.mhealthapp.encryption',
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
securityLevel: Keychain.SECURITY_LEVEL.SECURE_HARDWARE,
}
);
}
} catch (error) {
throw new Error('Failed to initialize encryption: ' + error);
}
}
/**
* Encrypt data using AES-256-GCM
*/
static encrypt(data: string): string {
if (!this.encryptionKey) {
throw new Error('Encryption not initialized');
}
try {
// Generate random IV for each encryption
const iv = CryptoJS.lib.WordArray.random(16);
const encrypted = CryptoJS.AES.encrypt(data, this.encryptionKey, {
iv: iv,
mode: CryptoJS.mode.GCM,
padding: CryptoJS.pad.Pkcs7,
});
// Combine IV + encrypted data
return iv.toString() + ':' + encrypted.toString();
} catch (error) {
throw new Error('Encryption failed: ' + error);
}
}
/**
* Decrypt data
*/
static decrypt(encryptedData: string): string {
if (!this.encryptionKey) {
throw new Error('Encryption not initialized');
}
try {
const [ivString, ciphertext] = encryptedData.split(':');
const iv = CryptoJS.enc.Hex.parse(ivString);
const decrypted = CryptoJS.AES.decrypt(ciphertext, this.encryptionKey, {
iv: iv,
mode: CryptoJS.mode.GCM,
padding: CryptoJS.pad.Pkcs7,
});
return decrypted.toString(CryptoJS.enc.Utf8);
} catch (error) {
throw new Error('Decryption failed: ' + error);
}
}
/**
* Encrypt object to JSON
*/
static encryptObject<T>(obj: T): string {
const json = JSON.stringify(obj);
return this.encrypt(json);
}
/**
* Decrypt JSON to object
*/
static decryptObject<T>(encryptedData: string): T {
const json = this.decrypt(encryptedData);
return JSON.parse(json);
}
/**
* Generate cryptographically secure encryption key
*/
private static generateEncryptionKey(): string {
return CryptoJS.lib.WordArray.random(32).toString(); // 256 bits
}
/**
* Secure key destruction (for logout)
*/
static async destroyKeys(): Promise<void> {
this.encryptionKey = null;
await Keychain.resetGenericPassword({
service: 'com.mhealthapp.encryption',
});
}
}
Secure Storage Wrapper
// src/services/storage/SecureStorage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { EncryptionService } from '../encryption/EncryptionService';
import { AuditLogger } from '../audit/AuditLogger';
export class SecureStorage {
/**
* Store PHI data with encryption and audit logging
*/
static async setItem(key: string, value: any): Promise<void> {
try {
// Encrypt data
const encrypted = EncryptionService.encryptObject(value);
// Store encrypted data
await AsyncStorage.setItem(key, encrypted);
// Log access for HIPAA audit trail
await AuditLogger.log({
action: 'DATA_WRITE',
resource: key,
timestamp: new Date(),
success: true,
details: {
dataType: typeof value,
size: encrypted.length,
},
});
} catch (error) {
await AuditLogger.log({
action: 'DATA_WRITE',
resource: key,
timestamp: new Date(),
success: false,
error: error.message,
});
throw error;
}
}
/**
* Retrieve and decrypt PHI data
*/
static async getItem<T>(key: string): Promise<T | null> {
try {
const encrypted = await AsyncStorage.getItem(key);
if (!encrypted) {
return null;
}
// Decrypt data
const decrypted = EncryptionService.decryptObject<T>(encrypted);
// Log access for HIPAA audit trail
await AuditLogger.log({
action: 'DATA_READ',
resource: key,
timestamp: new Date(),
success: true,
});
return decrypted;
} catch (error) {
await AuditLogger.log({
action: 'DATA_READ',
resource: key,
timestamp: new Date(),
success: false,
error: error.message,
});
throw error;
}
}
/**
* Remove data with audit logging
*/
static async removeItem(key: string): Promise<void> {
try {
await AsyncStorage.removeItem(key);
await AuditLogger.log({
action: 'DATA_DELETE',
resource: key,
timestamp: new Date(),
success: true,
});
} catch (error) {
await AuditLogger.log({
action: 'DATA_DELETE',
resource: key,
timestamp: new Date(),
success: false,
error: error.message,
});
throw error;
}
}
/**
* Get all keys (for sync operations)
*/
static async getAllKeys(): Promise<string[]> {
return await AsyncStorage.getAllKeys();
}
/**
* Clear all app data (logout)
*/
static async clearAll(): Promise<void> {
await AuditLogger.log({
action: 'DATA_CLEAR_ALL',
resource: 'ALL',
timestamp: new Date(),
success: true,
});
await AsyncStorage.clear();
}
}
React Hook for Secure Storage
// src/hooks/useSecureStorage.ts
import { useState, useEffect, useCallback } from 'react';
import { SecureStorage } from '../services/storage/SecureStorage';
export function useSecureStorage<T>(
key: string,
initialValue: T
): [T, (value: T) => Promise<void>, boolean] {
const [storedValue, setStoredValue] = useState<T>(initialValue);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadStoredValue();
}, [key]);
const loadStoredValue = async () => {
try {
const value = await SecureStorage.getItem<T>(key);
if (value !== null) {
setStoredValue(value);
}
} catch (error) {
console.error('Error loading stored value:', error);
} finally {
setLoading(false);
}
};
const setValue = useCallback(
async (value: T) => {
try {
setStoredValue(value);
await SecureStorage.setItem(key, value);
} catch (error) {
console.error('Error saving value:', error);
throw error;
}
},
[key]
);
return [storedValue, setValue, loading];
}
Offline-First Data Sync
Offline Queue Manager
// src/services/storage/OfflineQueue.ts
import { SecureStorage } from './SecureStorage';
import NetInfo from '@react-native-community/netinfo';
interface QueuedOperation {
id: string;
type: 'CREATE' | 'UPDATE' | 'DELETE';
resource: string;
data: any;
timestamp: Date;
retryCount: number;
maxRetries: number;
}
export class OfflineQueue {
private static readonly QUEUE_KEY = 'offline_operations_queue';
private static readonly MAX_RETRIES = 3;
/**
* Add operation to offline queue
*/
static async enqueue(
type: QueuedOperation['type'],
resource: string,
data: any
): Promise<void> {
const operation: QueuedOperation = {
id: this.generateOperationId(),
type,
resource,
data,
timestamp: new Date(),
retryCount: 0,
maxRetries: this.MAX_RETRIES,
};
const queue = await this.getQueue();
queue.push(operation);
await SecureStorage.setItem(this.QUEUE_KEY, queue);
}
/**
* Process offline queue when connection restored
*/
static async processQueue(): Promise<{
success: number;
failed: number;
}> {
const queue = await this.getQueue();
if (queue.length === 0) {
return { success: 0, failed: 0 };
}
let successCount = 0;
let failedCount = 0;
const remainingOperations: QueuedOperation[] = [];
for (const operation of queue) {
try {
await this.executeOperation(operation);
successCount++;
} catch (error) {
operation.retryCount++;
if (operation.retryCount < operation.maxRetries) {
// Keep in queue for retry
remainingOperations.push(operation);
} else {
// Max retries reached
failedCount++;
await this.handleFailedOperation(operation, error);
}
}
}
// Update queue with remaining operations
await SecureStorage.setItem(this.QUEUE_KEY, remainingOperations);
return { success: successCount, failed: failedCount };
}
/**
* Execute queued operation
*/
private static async executeOperation(
operation: QueuedOperation
): Promise<void> {
const { type, resource, data } = operation;
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${await this.getAuthToken()}`,
},
};
switch (type) {
case 'CREATE':
await axios.post(`${API_BASE_URL}/${resource}`, data, config);
break;
case 'UPDATE':
await axios.put(`${API_BASE_URL}/${resource}/${data.id}`, data, config);
break;
case 'DELETE':
await axios.delete(`${API_BASE_URL}/${resource}/${data.id}`, config);
break;
}
}
/**
* Get current queue
*/
private static async getQueue(): Promise<QueuedOperation[]> {
const queue = await SecureStorage.getItem<QueuedOperation[]>(this.QUEUE_KEY);
return queue || [];
}
/**
* Generate unique operation ID
*/
private static generateOperationId(): string {
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Handle operation that failed after max retries
*/
private static async handleFailedOperation(
operation: QueuedOperation,
error: any
): Promise<void> {
// Log to error tracking service
console.error('Operation failed after max retries:', operation, error);
// Store failed operation for manual review
const failedOps = await SecureStorage.getItem<QueuedOperation[]>('failed_operations') || [];
failedOps.push(operation);
await SecureStorage.setItem('failed_operations', failedOps);
// Notify user
await this.notifyOperationFailed(operation);
}
}
Sync Engine with Conflict Resolution
// src/services/sync/SyncEngine.ts
import NetInfo from '@react-native-community/netinfo';
import BackgroundFetch from 'react-native-background-fetch';
import { OfflineQueue } from '../storage/OfflineQueue';
import { SecureStorage } from '../storage/SecureStorage';
import axios from 'axios';
interface SyncStatus {
lastSyncTime: Date | null;
pendingOperations: number;
inProgress: boolean;
}
export class SyncEngine {
private static syncInProgress = false;
/**
* Initialize background sync
*/
static async initialize(): Promise<void> {
// Configure background fetch
await BackgroundFetch.configure(
{
minimumFetchInterval: 15, // minutes
stopOnTerminate: false,
startOnBoot: true,
enableHeadless: true,
requiresNetworkConnectivity: true,
requiresCharging: false,
},
async (taskId) => {
console.log('[BackgroundFetch] Task started:', taskId);
await this.performSync();
BackgroundFetch.finish(taskId);
},
(taskId) => {
console.log('[BackgroundFetch] Task timeout:', taskId);
BackgroundFetch.finish(taskId);
}
);
// Listen for network changes
NetInfo.addEventListener(state => {
if (state.isConnected && !this.syncInProgress) {
this.performSync();
}
});
}
/**
* Perform full sync operation
*/
static async performSync(): Promise<void> {
if (this.syncInProgress) {
console.log('Sync already in progress');
return;
}
this.syncInProgress = true;
try {
// Check network connectivity
const netInfo = await NetInfo.fetch();
if (!netInfo.isConnected) {
console.log('No network connectivity');
return;
}
// Process offline queue
const result = await OfflineQueue.processQueue();
console.log(`Sync completed: ${result.success} success, ${result.failed} failed`);
// Pull latest data from server
await this.pullServerData();
// Update last sync time
await SecureStorage.setItem('last_sync_time', new Date());
} catch (error) {
console.error('Sync failed:', error);
} finally {
this.syncInProgress = false;
}
}
/**
* Pull latest data from server
*/
private static async pullServerData(): Promise<void> {
const lastSyncTime = await SecureStorage.getItem<Date>('last_sync_time');
const params = lastSyncTime
? { since: lastSyncTime.toISOString() }
: {};
const response = await axios.get(`${API_BASE_URL}/sync/changes`, {
params,
headers: {
Authorization: `Bearer ${await this.getAuthToken()}`,
},
});
// Handle server changes
for (const change of response.data.changes) {
await this.applyServerChange(change);
}
}
/**
* Apply server change with conflict resolution
*/
private static async applyServerChange(change: any): Promise<void> {
const localData = await SecureStorage.getItem(change.resourceKey);
if (!localData) {
// No local data - simply apply server change
await SecureStorage.setItem(change.resourceKey, change.data);
return;
}
// Check for conflicts
if (this.hasConflict(localData, change.data)) {
const resolved = await this.resolveConflict(localData, change.data);
await SecureStorage.setItem(change.resourceKey, resolved);
} else {
// No conflict - apply server change
await SecureStorage.setItem(change.resourceKey, change.data);
}
}
/**
* Detect data conflicts
*/
private static hasConflict(localData: any, serverData: any): boolean {
// Check if local data was modified after server data
const localModified = new Date(localData.modifiedAt);
const serverModified = new Date(serverData.modifiedAt);
return localModified > serverModified;
}
/**
* Resolve sync conflicts (Last-Write-Wins strategy)
*/
private static async resolveConflict(
localData: any,
serverData: any
): Promise<any> {
const localModified = new Date(localData.modifiedAt);
const serverModified = new Date(serverData.modifiedAt);
// Last-write-wins
if (localModified > serverModified) {
// Local changes are newer - upload to server
await this.uploadLocalChanges(localData);
return localData;
} else {
// Server changes are newer - use server data
return serverData;
}
}
/**
* Get sync status
*/
static async getSyncStatus(): Promise<SyncStatus> {
const lastSyncTime = await SecureStorage.getItem<Date>('last_sync_time');
const queue = await OfflineQueue.getQueue();
return {
lastSyncTime,
pendingOperations: queue.length,
inProgress: this.syncInProgress,
};
}
}
React Hook for Offline Sync
// src/hooks/useOfflineSync.ts
import { useState, useEffect } from 'react';
import { SyncEngine } from '../services/sync/SyncEngine';
import NetInfo from '@react-native-community/netinfo';
interface SyncState {
syncing: boolean;
lastSyncTime: Date | null;
pendingOperations: number;
online: boolean;
}
export function useOfflineSync() {
const [syncState, setSyncState] = useState<SyncState>({
syncing: false,
lastSyncTime: null,
pendingOperations: 0,
online: true,
});
useEffect(() => {
// Initialize sync engine
SyncEngine.initialize();
// Monitor network status
const unsubscribe = NetInfo.addEventListener(state => {
setSyncState(prev => ({
...prev,
online: state.isConnected || false,
}));
});
// Poll sync status
const interval = setInterval(updateSyncStatus, 5000);
return () => {
unsubscribe();
clearInterval(interval);
};
}, []);
const updateSyncStatus = async () => {
const status = await SyncEngine.getSyncStatus();
setSyncState(prev => ({
...prev,
syncing: status.inProgress,
lastSyncTime: status.lastSyncTime,
pendingOperations: status.pendingOperations,
}));
};
const manualSync = async () => {
setSyncState(prev => ({ ...prev, syncing: true }));
await SyncEngine.performSync();
await updateSyncStatus();
};
return {
...syncState,
sync: manualSync,
};
}
HIPAA-Compliant Push Notifications
Push Notification Service
// src/services/notifications/PushNotificationService.ts
import messaging from '@react-native-firebase/messaging';
import PushNotification from 'react-native-push-notification';
import { AuditLogger } from '../audit/AuditLogger';
export class PushNotificationService {
/**
* Initialize push notifications with HIPAA considerations
*/
static async initialize(): Promise<void> {
// Request permissions
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
const fcmToken = await messaging().getToken();
await this.registerDeviceToken(fcmToken);
}
// Configure notification channels (Android)
this.createNotificationChannels();
// Handle foreground messages
messaging().onMessage(async remoteMessage => {
await this.handleNotification(remoteMessage, 'foreground');
});
// Handle background messages
messaging().setBackgroundMessageHandler(async remoteMessage => {
await this.handleNotification(remoteMessage, 'background');
});
// Handle notification opened app
messaging().onNotificationOpenedApp(remoteMessage => {
this.handleNotificationOpened(remoteMessage);
});
// Handle notification that opened app from quit state
messaging()
.getInitialNotification()
.then(remoteMessage => {
if (remoteMessage) {
this.handleNotificationOpened(remoteMessage);
}
});
}
/**
* Create notification channels (Android 8.0+)
*/
private static createNotificationChannels(): void {
PushNotification.createChannel(
{
channelId: 'medication-reminders',
channelName: 'Medication Reminders',
channelDescription: 'Reminders to take medications',
importance: 4, // High importance
vibrate: true,
},
created => console.log(`Medication channel created: ${created}`)
);
PushNotification.createChannel(
{
channelId: 'appointment-reminders',
channelName: 'Appointment Reminders',
channelDescription: 'Reminders for upcoming appointments',
importance: 4,
vibrate: true,
},
created => console.log(`Appointment channel created: ${created}`)
);
PushNotification.createChannel(
{
channelId: 'clinical-alerts',
channelName: 'Clinical Alerts',
channelDescription: 'Important clinical notifications',
importance: 5, // Urgent
vibrate: true,
sound: 'urgent_alert.mp3',
},
created => console.log(`Clinical alerts channel created: ${created}`)
);
// HIPAA-compliant: Generic message channel for PHI-related notifications
// Actual PHI is NOT included in notification, only generic alerts
PushNotification.createChannel(
{
channelId: 'health-updates',
channelName: 'Health Updates',
channelDescription: 'Updates about your health information',
importance: 3, // Default
vibrate: false,
},
created => console.log(`Health updates channel created: ${created}`)
);
}
/**
* Handle incoming notification
* IMPORTANT: Never include PHI in notification payload
*/
private static async handleNotification(
remoteMessage: any,
context: 'foreground' | 'background'
): Promise<void> {
const { notification, data } = remoteMessage;
// Log notification receipt for audit trail
await AuditLogger.log({
action: 'NOTIFICATION_RECEIVED',
resource: data?.type || 'unknown',
timestamp: new Date(),
success: true,
details: {
context,
notificationId: remoteMessage.messageId,
},
});
// Display local notification with HIPAA-safe content
this.showLocalNotification({
channelId: this.getChannelForType(data?.type),
title: notification.title || 'Health Reminder',
message: notification.body || 'You have a new health update',
data: data || {},
});
}
/**
* Show local notification with HIPAA-safe content
* CRITICAL: Never expose PHI in notification text
*/
private static showLocalNotification(config: {
channelId: string;
title: string;
message: string;
data: any;
}): void {
PushNotification.localNotification({
channelId: config.channelId,
title: config.title,
message: config.message,
userInfo: config.data,
playSound: true,
soundName: 'default',
importance: 'high',
priority: 'high',
vibrate: true,
vibration: 300,
// HIPAA: Prevent notification from showing on lock screen
visibility: 'private', // Android
// iOS equivalent handled in AppDelegate
});
}
/**
* Schedule medication reminder
* Uses generic message to avoid PHI exposure
*/
static async scheduleMedicationReminder(
medicationId: string,
scheduledTime: Date
): Promise<void> {
// HIPAA-compliant: Generic message, medication details shown only in-app
PushNotification.localNotificationSchedule({
channelId: 'medication-reminders',
title: 'Medication Reminder',
message: 'Time to take your medication. Tap to view details.',
date: scheduledTime,
allowWhileIdle: true,
userInfo: {
type: 'medication',
medicationId, // Used to fetch details in-app
},
actions: ['TAKE', 'SNOOZE', 'SKIP'],
invokeApp: false,
});
await AuditLogger.log({
action: 'NOTIFICATION_SCHEDULED',
resource: `medication_${medicationId}`,
timestamp: new Date(),
success: true,
details: {
scheduledFor: scheduledTime.toISOString(),
},
});
}
/**
* Register device token with backend
*/
private static async registerDeviceToken(fcmToken: string): Promise<void> {
try {
await axios.post(
`${API_BASE_URL}/devices/register`,
{
token: fcmToken,
platform: Platform.OS,
appVersion: getAppVersion(),
},
{
headers: {
Authorization: `Bearer ${await getAuthToken()}`,
},
}
);
await AuditLogger.log({
action: 'DEVICE_REGISTERED',
resource: 'push_notifications',
timestamp: new Date(),
success: true,
});
} catch (error) {
console.error('Failed to register device token:', error);
}
}
/**
* Get notification channel based on type
*/
private static getChannelForType(type: string): string {
switch (type) {
case 'medication':
return 'medication-reminders';
case 'appointment':
return 'appointment-reminders';
case 'clinical_alert':
return 'clinical-alerts';
default:
return 'health-updates';
}
}
/**
* Handle notification opened
*/
private static async handleNotificationOpened(remoteMessage: any): Promise<void> {
await AuditLogger.log({
action: 'NOTIFICATION_OPENED',
resource: remoteMessage.data?.type || 'unknown',
timestamp: new Date(),
success: true,
details: {
notificationId: remoteMessage.messageId,
},
});
// Navigate to appropriate screen based on notification type
// Implementation depends on navigation library
}
/**
* Unregister device (on logout)
*/
static async unregisterDevice(): Promise<void> {
const fcmToken = await messaging().getToken();
try {
await axios.post(
`${API_BASE_URL}/devices/unregister`,
{ token: fcmToken },
{
headers: {
Authorization: `Bearer ${await getAuthToken()}`,
},
}
);
// Cancel all scheduled notifications
PushNotification.cancelAllLocalNotifications();
await AuditLogger.log({
action: 'DEVICE_UNREGISTERED',
resource: 'push_notifications',
timestamp: new Date(),
success: true,
});
} catch (error) {
console.error('Failed to unregister device:', error);
}
}
}
iOS Configuration for HIPAA Notifications
// ios/MHealthApp/AppDelegate.mm
// Configure notifications to hide PHI on lock screen
#import <UserNotifications/UserNotifications.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Request notification permissions
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
[center requestAuthorizationWithOptions:options
completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] registerForRemoteNotifications];
});
}
}];
return YES;
}
// HIPAA: Configure notification presentation to hide content on lock screen
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
// Show notification but hide content if device is locked
UNNotificationPresentationOptions options = UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBadge;
// Only show alert if device is unlocked
if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
options |= UNNotificationPresentationOptionAlert;
}
completionHandler(options);
}
@end
Authentication and Authorization
Biometric Authentication
// src/services/auth/BiometricAuth.ts
import ReactNativeBiometrics from 'react-native-biometrics';
import { Platform } from 'react-native';
import { AuditLogger } from '../audit/AuditLogger';
export class BiometricAuth {
private static rnBiometrics = new ReactNativeBiometrics();
/**
* Check if biometric authentication is available
*/
static async isAvailable(): Promise<{
available: boolean;
biometryType: 'TouchID' | 'FaceID' | 'Biometrics' | null;
}> {
const { available, biometryType } = await this.rnBiometrics.isSensorAvailable();
return {
available,
biometryType: biometryType as any,
};
}
/**
* Authenticate user with biometrics
*/
static async authenticate(): Promise<boolean> {
try {
const { success } = await this.rnBiometrics.simplePrompt({
promptMessage: 'Confirm your identity',
cancelButtonText: 'Cancel',
});
await AuditLogger.log({
action: 'BIOMETRIC_AUTH',
resource: 'authentication',
timestamp: new Date(),
success,
});
return success;
} catch (error) {
await AuditLogger.log({
action: 'BIOMETRIC_AUTH',
resource: 'authentication',
timestamp: new Date(),
success: false,
error: error.message,
});
return false;
}
}
/**
* Create biometric keys for cryptographic operations
*/
static async createKeys(): Promise<boolean> {
try {
const { publicKey } = await this.rnBiometrics.createKeys();
// Store public key for server-side verification
await this.registerPublicKey(publicKey);
return true;
} catch (error) {
console.error('Failed to create biometric keys:', error);
return false;
}
}
/**
* Sign data with biometric key
*/
static async createSignature(payload: string): Promise<string | null> {
try {
const { success, signature } = await this.rnBiometrics.createSignature({
promptMessage: 'Confirm your identity',
payload,
});
if (success && signature) {
return signature;
}
return null;
} catch (error) {
console.error('Failed to create signature:', error);
return null;
}
}
/**
* Delete biometric keys (on logout)
*/
static async deleteKeys(): Promise<void> {
try {
await this.rnBiometrics.deleteKeys();
} catch (error) {
console.error('Failed to delete biometric keys:', error);
}
}
}
Session Management with Auto-Logout
// src/components/SessionTimeout.tsx
import React, { useEffect, useRef } from 'react';
import { AppState, AppStateStatus } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { AuthService } from '../services/auth/AuthService';
interface SessionTimeoutProps {
timeoutMinutes?: number; // HIPAA recommends 15 minutes
children: React.ReactNode;
}
export const SessionTimeout: React.FC<SessionTimeoutProps> = ({
timeoutMinutes = 15,
children,
}) => {
const navigation = useNavigation();
const appState = useRef(AppState.currentState);
const lastActivityTime = useRef(Date.now());
const timeoutTimer = useRef<NodeJS.Timeout>();
useEffect(() => {
// Monitor app state changes
const subscription = AppState.addEventListener('change', handleAppStateChange);
// Start inactivity timer
startInactivityTimer();
// Reset timer on user interaction
const interactionListener = () => {
lastActivityTime.current = Date.now();
resetInactivityTimer();
};
// Add event listeners for user interactions
// (implementation depends on your gesture handling)
return () => {
subscription.remove();
if (timeoutTimer.current) {
clearTimeout(timeoutTimer.current);
}
};
}, []);
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
if (appState.current.match(/active/) && nextAppState === 'background') {
// App moved to background - start logout timer
const backgroundTime = Date.now();
setTimeout(async () => {
const timeInBackground = Date.now() - backgroundTime;
if (timeInBackground >= timeoutMinutes * 60 * 1000) {
await logout();
}
}, timeoutMinutes * 60 * 1000);
}
appState.current = nextAppState;
};
const startInactivityTimer = () => {
timeoutTimer.current = setTimeout(async () => {
await logout();
}, timeoutMinutes * 60 * 1000);
};
const resetInactivityTimer = () => {
if (timeoutTimer.current) {
clearTimeout(timeoutTimer.current);
}
startInactivityTimer();
};
const logout = async () => {
await AuthService.logout();
navigation.reset({
index: 0,
routes: [{ name: 'Login' }],
});
};
return <>{children}</>;
};
Audit Logging
HIPAA-Compliant Audit Logger
// src/services/audit/AuditLogger.ts
import { SecureStorage } from '../storage/SecureStorage';
import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';
export interface AuditLogEntry {
action: string;
resource: string;
timestamp: Date;
success: boolean;
userId?: string;
details?: any;
error?: string;
deviceInfo?: DeviceMetadata;
}
interface DeviceMetadata {
deviceId: string;
platform: string;
osVersion: string;
appVersion: string;
ipAddress?: string;
}
export class AuditLogger {
private static readonly AUDIT_LOG_KEY = 'audit_logs';
private static readonly MAX_LOCAL_LOGS = 1000;
private static readonly SYNC_BATCH_SIZE = 100;
/**
* Log HIPAA-required audit event
*/
static async log(entry: Omit<AuditLogEntry, 'deviceInfo'>): Promise<void> {
const deviceInfo = await this.getDeviceInfo();
const userId = await this.getUserId();
const auditEntry: AuditLogEntry = {
...entry,
userId,
deviceInfo,
};
// Store locally
await this.storeLocally(auditEntry);
// Attempt immediate sync to server
try {
await this.syncToServer([auditEntry]);
} catch (error) {
// Sync will retry in background
console.log('Audit log sync failed, will retry later');
}
}
/**
* Store audit log locally
*/
private static async storeLocally(entry: AuditLogEntry): Promise<void> {
const logs = await this.getLocalLogs();
logs.push(entry);
// Keep only recent logs locally (older logs are on server)
if (logs.length > this.MAX_LOCAL_LOGS) {
logs.splice(0, logs.length - this.MAX_LOCAL_LOGS);
}
await SecureStorage.setItem(this.AUDIT_LOG_KEY, logs);
}
/**
* Sync audit logs to server
*/
private static async syncToServer(logs: AuditLogEntry[]): Promise<void> {
const response = await axios.post(
`${API_BASE_URL}/audit/logs`,
{ logs },
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${await getAuthToken()}`,
},
}
);
if (!response.data.success) {
throw new Error('Audit log sync failed');
}
}
/**
* Get local audit logs
*/
private static async getLocalLogs(): Promise<AuditLogEntry[]> {
const logs = await SecureStorage.getItem<AuditLogEntry[]>(this.AUDIT_LOG_KEY);
return logs || [];
}
/**
* Get device metadata for audit trail
*/
private static async getDeviceInfo(): Promise<DeviceMetadata> {
return {
deviceId: await DeviceInfo.getUniqueId(),
platform: Platform.OS,
osVersion: DeviceInfo.getSystemVersion(),
appVersion: DeviceInfo.getVersion(),
};
}
/**
* Get current user ID
*/
private static async getUserId(): Promise<string | undefined> {
const user = await SecureStorage.getItem<{ id: string }>('current_user');
return user?.id;
}
/**
* Query audit logs (for user-facing audit trail)
*/
static async queryLogs(filters: {
startDate?: Date;
endDate?: Date;
action?: string;
}): Promise<AuditLogEntry[]> {
const localLogs = await this.getLocalLogs();
// Filter logs
let filtered = localLogs;
if (filters.startDate) {
filtered = filtered.filter(log => new Date(log.timestamp) >= filters.startDate!);
}
if (filters.endDate) {
filtered = filtered.filter(log => new Date(log.timestamp) <= filters.endDate!);
}
if (filters.action) {
filtered = filtered.filter(log => log.action === filters.action);
}
return filtered;
}
/**
* Background sync of pending audit logs
*/
static async syncPendingLogs(): Promise<void> {
const logs = await this.getLocalLogs();
// Sync in batches
for (let i = 0; i < logs.length; i += this.SYNC_BATCH_SIZE) {
const batch = logs.slice(i, i + this.SYNC_BATCH_SIZE);
try {
await this.syncToServer(batch);
} catch (error) {
console.error('Failed to sync audit log batch:', error);
break; // Stop on first error, will retry later
}
}
}
}
Apple Health and Google Fit Integration
Apple HealthKit Integration
// src/services/health/AppleHealthService.ts
import AppleHealthKit, {
HealthValue,
HealthKitPermissions,
} from 'react-native-health';
import { Platform } from 'react-native';
import { AuditLogger } from '../audit/AuditLogger';
export class AppleHealthService {
/**
* Initialize HealthKit with required permissions
*/
static async initialize(): Promise<boolean> {
if (Platform.OS !== 'ios') {
return false;
}
const permissions: HealthKitPermissions = {
permissions: {
read: [
'Height',
'Weight',
'HeartRate',
'BloodPressureSystolic',
'BloodPressureDiastolic',
'OxygenSaturation',
'RespiratoryRate',
'BodyTemperature',
'Steps',
'StepCount',
'DistanceWalkingRunning',
'FlightsClimbed',
'BloodGlucose',
'SleepAnalysis',
],
write: [
'Weight',
'BloodGlucose',
'BloodPressureSystolic',
'BloodPressureDiastolic',
],
},
};
return new Promise((resolve, reject) => {
AppleHealthKit.initHealthKit(permissions, (error: string) => {
if (error) {
console.error('[HealthKit] Error initializing:', error);
reject(error);
} else {
console.log('[HealthKit] Initialized successfully');
AuditLogger.log({
action: 'HEALTHKIT_INITIALIZED',
resource: 'apple_health',
timestamp: new Date(),
success: true,
});
resolve(true);
}
});
});
}
/**
* Get latest weight
*/
static async getWeight(): Promise<number | null> {
return new Promise((resolve) => {
const options = {
unit: 'kg',
startDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
ascending: false,
limit: 1,
};
AppleHealthKit.getWeightSamples(options, (err: string, results: HealthValue[]) => {
if (err || !results || results.length === 0) {
resolve(null);
} else {
AuditLogger.log({
action: 'HEALTHKIT_READ',
resource: 'weight',
timestamp: new Date(),
success: true,
});
resolve(results[0].value);
}
});
});
}
/**
* Get latest blood pressure
*/
static async getBloodPressure(): Promise<{
systolic: number;
diastolic: number;
} | null> {
return new Promise((resolve) => {
const options = {
startDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
ascending: false,
limit: 1,
};
AppleHealthKit.getBloodPressureSamples(
options,
(err: string, results: any[]) => {
if (err || !results || results.length === 0) {
resolve(null);
} else {
AuditLogger.log({
action: 'HEALTHKIT_READ',
resource: 'blood_pressure',
timestamp: new Date(),
success: true,
});
resolve({
systolic: results[0].bloodPressureSystolicValue,
diastolic: results[0].bloodPressureDiastolicValue,
});
}
}
);
});
}
/**
* Write weight to HealthKit
*/
static async saveWeight(weight: number): Promise<boolean> {
return new Promise((resolve) => {
const options = {
value: weight,
date: new Date().toISOString(),
unit: 'kg',
};
AppleHealthKit.saveWeight(options, (err: string) => {
const success = !err;
AuditLogger.log({
action: 'HEALTHKIT_WRITE',
resource: 'weight',
timestamp: new Date(),
success,
error: err,
});
resolve(success);
});
});
}
/**
* Write blood glucose to HealthKit
*/
static async saveBloodGlucose(glucose: number): Promise<boolean> {
return new Promise((resolve) => {
const options = {
value: glucose,
date: new Date().toISOString(),
unit: 'mmolPerL',
};
AppleHealthKit.saveBloodGlucoseSample(options, (err: string) => {
const success = !err;
AuditLogger.log({
action: 'HEALTHKIT_WRITE',
resource: 'blood_glucose',
timestamp: new Date(),
success,
error: err,
});
resolve(success);
});
});
}
}
Google Fit Integration
// src/services/health/GoogleFitService.ts
import GoogleFit, { Scopes } from 'react-native-google-fit';
import { Platform } from 'react-native';
import { AuditLogger } from '../audit/AuditLogger';
export class GoogleFitService {
/**
* Initialize Google Fit with required permissions
*/
static async initialize(): Promise<boolean> {
if (Platform.OS !== 'android') {
return false;
}
const options = {
scopes: [
Scopes.FITNESS_ACTIVITY_READ,
Scopes.FITNESS_ACTIVITY_WRITE,
Scopes.FITNESS_BODY_READ,
Scopes.FITNESS_BODY_WRITE,
Scopes.FITNESS_BLOOD_PRESSURE_READ,
Scopes.FITNESS_BLOOD_PRESSURE_WRITE,
Scopes.FITNESS_BLOOD_GLUCOSE_READ,
Scopes.FITNESS_BLOOD_GLUCOSE_WRITE,
Scopes.FITNESS_HEART_RATE_READ,
],
};
try {
await GoogleFit.authorize(options);
await AuditLogger.log({
action: 'GOOGLEFIT_INITIALIZED',
resource: 'google_fit',
timestamp: new Date(),
success: true,
});
return true;
} catch (error) {
console.error('[GoogleFit] Initialization failed:', error);
return false;
}
}
/**
* Get latest weight
*/
static async getWeight(): Promise<number | null> {
try {
const options = {
startDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
endDate: new Date().toISOString(),
};
const samples = await GoogleFit.getWeightSamples(options);
if (samples.length > 0) {
await AuditLogger.log({
action: 'GOOGLEFIT_READ',
resource: 'weight',
timestamp: new Date(),
success: true,
});
// Return most recent sample
return samples[samples.length - 1].value;
}
return null;
} catch (error) {
console.error('[GoogleFit] Error reading weight:', error);
return null;
}
}
/**
* Save weight to Google Fit
*/
static async saveWeight(weight: number): Promise<boolean> {
try {
const options = {
value: weight,
date: new Date().toISOString(),
unit: 'kg',
};
await GoogleFit.saveWeight(options);
await AuditLogger.log({
action: 'GOOGLEFIT_WRITE',
resource: 'weight',
timestamp: new Date(),
success: true,
});
return true;
} catch (error) {
console.error('[GoogleFit] Error saving weight:', error);
await AuditLogger.log({
action: 'GOOGLEFIT_WRITE',
resource: 'weight',
timestamp: new Date(),
success: false,
error: error.message,
});
return false;
}
}
/**
* Get daily step count
*/
static async getDailySteps(date: Date): Promise<number> {
try {
const options = {
startDate: new Date(date.setHours(0, 0, 0, 0)).toISOString(),
endDate: new Date(date.setHours(23, 59, 59, 999)).toISOString(),
};
const steps = await GoogleFit.getDailyStepCountSamples(options);
if (steps.length > 0) {
return steps[0].value;
}
return 0;
} catch (error) {
console.error('[GoogleFit] Error reading steps:', error);
return 0;
}
}
}
App Store Compliance
Apple App Store Requirements
<!-- ios/MHealthApp/Info.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Required: HealthKit Usage Description -->
<key>NSHealthShareUsageDescription</key>
<string>We need access to your health data to track your vital signs and help manage your health conditions.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>We need permission to save your health data to Apple Health for comprehensive health tracking.</string>
<!-- Required: Face ID Usage Description -->
<key>NSFaceIDUsageDescription</key>
<string>We use Face ID to securely authenticate you and protect your health information.</string>
<!-- Required: Camera Usage Description (if app uses camera) -->
<key>NSCameraUsageDescription</key>
<string>We need camera access to allow you to take photos of symptoms or medical documents.</string>
<!-- Required: Photo Library Usage Description -->
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photo library to attach medical images to your health records.</string>
<!-- Enable HealthKit -->
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>healthkit</string>
</array>
<!-- Background modes for health data sync -->
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
<string>processing</string>
</array>
<!-- App Transport Security -->
<key>NSAppTransportSecurity</key>
<dict>
<!-- Require HTTPS for all connections -->
<key>NSAllowsArbitraryLoads</key>
<false/>
</dict>
</dict>
</plist>
Google Play Store Requirements
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- HealthConnect permissions -->
<uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
<uses-permission android:name="android.permission.health.READ_STEPS"/>
<uses-permission android:name="android.permission.health.READ_WEIGHT"/>
<uses-permission android:name="android.permission.health.WRITE_WEIGHT"/>
<!-- Biometric authentication -->
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<!-- Network state -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Push notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Camera (if needed) -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- Prevent screenshots of PHI (optional but recommended) -->
<application
android:name=".MainApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize"
android:screenOrientation="portrait"
<!-- Prevent screenshots -->
android:windowIsSecure="true">
</activity>
</application>
</manifest>
Privacy Policy Requirements
Both app stores require prominent privacy policy disclosure. Create a comprehensive privacy policy covering:
- What PHI you collect
- How PHI is used
- How PHI is protected
- Who PHI is shared with
- User rights (access, deletion, portability)
- HIPAA compliance statement
- Contact information for privacy concerns
Testing and Validation
HIPAA Compliance Testing
// __tests__/hipaa-compliance.test.ts
import { EncryptionService } from '../src/services/encryption/EncryptionService';
import { SecureStorage } from '../src/services/storage/SecureStorage';
import { AuditLogger } from '../src/services/audit/AuditLogger';
describe('HIPAA Compliance Tests', () => {
beforeAll(async () => {
await EncryptionService.initialize();
});
describe('Data Encryption', () => {
it('should encrypt PHI before storage', async () => {
const phi = {
patientName: 'John Doe',
mrn: '12345',
diagnosis: 'Hypertension',
};
await SecureStorage.setItem('test_phi', phi);
// Read raw storage (bypassing decryption)
const rawData = await AsyncStorage.getItem('test_phi');
// Raw data should be encrypted (not contain plain text)
expect(rawData).not.toContain('John Doe');
expect(rawData).not.toContain('Hypertension');
});
it('should use unique IV for each encryption', () => {
const data = 'sensitive health information';
const encrypted1 = EncryptionService.encrypt(data);
const encrypted2 = EncryptionService.encrypt(data);
// Same data encrypted twice should produce different ciphertext
expect(encrypted1).not.toBe(encrypted2);
// Both should decrypt to same value
expect(EncryptionService.decrypt(encrypted1)).toBe(data);
expect(EncryptionService.decrypt(encrypted2)).toBe(data);
});
});
describe('Audit Logging', () => {
it('should log all PHI access', async () => {
await SecureStorage.getItem('patient_data');
const logs = await AuditLogger.queryLogs({
action: 'DATA_READ',
startDate: new Date(Date.now() - 60000), // Last minute
});
expect(logs.length).toBeGreaterThan(0);
expect(logs[0].action).toBe('DATA_READ');
expect(logs[0].timestamp).toBeDefined();
expect(logs[0].deviceInfo).toBeDefined();
});
it('should log failed access attempts', async () => {
try {
await SecureStorage.getItem('nonexistent_key');
} catch (error) {
// Expected to fail
}
const logs = await AuditLogger.queryLogs({
action: 'DATA_READ',
});
const failedLog = logs.find(log => !log.success);
expect(failedLog).toBeDefined();
});
});
describe('Session Management', () => {
it('should enforce session timeout', async () => {
// Test implementation depends on your auth service
// Mock time passage and verify auto-logout
});
it('should clear sensitive data on logout', async () => {
await SecureStorage.setItem('session_token', 'abc123');
await AuthService.logout();
const token = await SecureStorage.getItem('session_token');
expect(token).toBeNull();
});
});
describe('Transmission Security', () => {
it('should use HTTPS for all API calls', () => {
expect(API_BASE_URL).toMatch(/^https:\/\//);
});
it('should include authorization header', async () => {
// Mock API call and verify headers
const mockFetch = jest.spyOn(global, 'fetch');
await makeAPICall();
expect(mockFetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
headers: expect.objectContaining({
Authorization: expect.stringMatching(/^Bearer /),
}),
})
);
});
});
});
End-to-End Testing with Detox
// e2e/hipaa-compliance.e2e.ts
import { device, element, by, expect } from 'detox';
describe('HIPAA Compliance E2E', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should require authentication on app launch', async () => {
await expect(element(by.id('login-screen'))).toBeVisible();
});
it('should not show PHI in notifications', async () => {
// Trigger a medication reminder
await element(by.id('schedule-medication-btn')).tap();
// Send app to background to trigger notification
await device.sendToHome();
// Check that notification doesn't contain medication name
// (notification testing requires platform-specific implementation)
await device.launchApp();
});
it('should auto-logout after inactivity', async () => {
await element(by.id('login-btn')).tap();
// Wait for inactivity timeout (mocked to 5 seconds for testing)
await new Promise(resolve => setTimeout(resolve, 6000));
// Should be back at login screen
await expect(element(by.id('login-screen'))).toBeVisible();
});
it('should prevent screenshots of PHI', async () => {
// This test is platform-specific and may require native code
// On Android, check windowIsSecure flag
// On iOS, check for sensitive content hiding
});
});
Deployment Checklist
Before deploying your HIPAA-compliant mobile health app:
Security Checklist
- All PHI is encrypted at rest using AES-256
- All API communications use HTTPS with TLS 1.2+
- Device encryption key stored in secure hardware (Keychain/Keystore)
- Biometric authentication implemented
- Session timeout configured (15 minutes recommended)
- Auto-logout on app backgrounding
- Audit logging for all PHI access
- Secure key management implemented
- Screenshot protection enabled
- Clipboard protection for PHI
HIPAA Compliance Checklist
- Business Associate Agreement (BAA) signed with all third-party services
- Privacy policy published and accessible
- User consent forms implemented
- HIPAA training completed for development team
- Risk assessment documented
- Incident response plan established
- Audit log retention policy (6 years minimum)
- User data access/export functionality
- User data deletion functionality
App Store Checklist
- Privacy policy URL added to app store listing
- Health data usage descriptions in Info.plist/AndroidManifest
- App classified correctly (Medical vs. Wellness)
- Required permissions justified in submission notes
- Screenshots donβt show real patient data
- Medical claims reviewed (FDA oversight may apply)
Testing Checklist
- Penetration testing completed
- Security audit conducted
- End-to-end encryption verified
- Offline functionality tested
- Data sync conflict resolution tested
- Push notification privacy verified
- Session timeout tested
- Audit logging verified
- Cross-device sync tested
Conclusion
Building a HIPAA-compliant mobile health app requires careful attention to security, privacy, and regulatory requirements. This guide has covered:
- Encryption: AES-256 encryption for data at rest
- Secure Storage: Hardware-backed key storage and encrypted local databases
- Offline Sync: Queue-based sync with conflict resolution
- Push Notifications: PHI-safe notification strategies
- Authentication: Biometric auth and session management
- Audit Logging: Comprehensive audit trail for HIPAA compliance
- Health Integration: Apple Health and Google Fit integration
- App Store Compliance: Meeting platform-specific requirements
With these implementations, you can build production-ready mobile health applications that protect patient privacy while delivering excellent user experiences.
Ready to accelerate your mobile health app development? Start with JustCopy.ai to clone proven mHealth interfaces and customize them with HIPAA-compliant backends. Launch your compliant health app in weeks, not months.
Related Articles
Build This with JustCopy.ai
Skip months of development with 10 specialized AI agents. JustCopy.ai can copy, customize, and deploy this application instantly. Our AI agents write code, run tests, handle deployment, and monitor your applicationβall following healthcare industry best practices and HIPAA compliance standards.