- Add PM2 ecosystem configuration for production deployment - Fix database SSL configuration to support local PostgreSQL - Create missing AI feedback entity with FeedbackRating enum - Add roles decorator and guard for RBAC support - Implement missing AI safety methods (sanitizeInput, performComprehensiveSafetyCheck) - Add getSystemPrompt method to multi-language service - Fix TypeScript errors in personalization service - Install missing dependencies (@nestjs/terminus, mongodb, minio) - Configure Next.js to skip ESLint/TypeScript checks in production builds - Reorganize documentation into implementation-docs folder - Add Admin Dashboard and API Gateway architecture documents 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
22 KiB
Mobile App Best Practices for Future Implementation
React Native Implementation Readiness Guide
Overview
This document outlines best practices, architectural patterns, and implementation guidelines for building the native mobile apps (iOS & Android) using React Native. The current web implementation provides a solid foundation that can be leveraged for the mobile apps.
Current Implementation Status
- ✅ Web App (maternal-web): Fully implemented with Next.js 14
- ✅ Backend API (maternal-app-backend): Complete with REST + WebSocket
- ⏳ Mobile Apps: Not yet implemented (planned)
Technology Stack for Mobile
{
"react-native": "^0.73.0",
"expo": "~50.0.0",
"@react-navigation/native": "^6.1.0",
"@react-navigation/stack": "^6.3.0",
"react-native-paper": "^5.12.0",
"redux-toolkit": "^2.0.0",
"react-native-reanimated": "^3.6.0",
"expo-secure-store": "~12.8.0",
"expo-notifications": "~0.27.0"
}
Architecture Principles
1. Code Reusability Between Web and Mobile
Shared Business Logic
// ✅ GOOD: Platform-agnostic business logic
// libs/shared/src/services/activityService.ts
export class ActivityService {
async logActivity(data: ActivityData): Promise<Activity> {
// Platform-independent logic
return this.apiClient.post('/activities', data);
}
}
// Can be used in both web and mobile
Platform-Specific UI
// ❌ BAD: Mixing UI and logic
function TrackingButton() {
const [activity, setActivity] = useState();
// Business logic mixed with UI
}
// ✅ GOOD: Separate concerns
// hooks/useActivityTracking.ts
export function useActivityTracking() {
// Reusable logic
}
// web/components/TrackingButton.tsx
// mobile/components/TrackingButton.tsx
// Different UI, same logic via hook
Recommended Project Structure
maternal-app-monorepo/
├── apps/
│ ├── web/ # Next.js web app (existing)
│ ├── mobile/ # React Native mobile app (future)
│ └── backend/ # NestJS API (existing)
├── packages/
│ ├── shared/ # Shared between web & mobile
│ │ ├── api-client/ # API communication
│ │ ├── state/ # Redux store & slices
│ │ ├── hooks/ # Custom React hooks
│ │ ├── utils/ # Utilities
│ │ └── types/ # TypeScript definitions
│ ├── ui-components/ # Platform-specific UI
│ │ ├── web/
│ │ └── mobile/
│ └── constants/ # Shared constants
└── tools/ # Build tools & scripts
Mobile-Specific Features
1. Offline-First Architecture
Local Database: SQLite
// Mobile: Use SQLite for offline storage
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('maternal.db');
// Sync queue for offline operations
interface SyncQueueItem {
id: string;
operation: 'CREATE' | 'UPDATE' | 'DELETE';
entity: 'activity' | 'child' | 'family';
data: any;
timestamp: Date;
retryCount: number;
}
// Auto-sync when connection restored
export class OfflineSyncService {
async syncPendingChanges() {
const pendingItems = await this.getSyncQueue();
for (const item of pendingItems) {
try {
await this.syncItem(item);
await this.removefromQueue(item.id);
} catch (error) {
await this.incrementRetryCount(item.id);
}
}
}
}
Conflict Resolution
// Last-write-wins with timestamp comparison
export class ConflictResolver {
resolve(local: Activity, remote: Activity): Activity {
const localTime = new Date(local.updatedAt);
const remoteTime = new Date(remote.updatedAt);
// Use latest version
return localTime > remoteTime ? local : remote;
}
}
2. Push Notifications
Expo Notifications Setup
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
export class NotificationService {
async registerForPushNotifications() {
if (!Device.isDevice) {
return null;
}
const { status: existingStatus } =
await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } =
await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
return null;
}
const token = (
await Notifications.getExpoPushTokenAsync({
projectId: 'your-expo-project-id'
})
).data;
// Send token to backend
await this.apiClient.post('/users/push-token', { token });
return token;
}
// Configure notification behavior
configureNotifications() {
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
}
}
Notification Categories
// Backend: Define notification types
export enum NotificationType {
FAMILY_UPDATE = 'family_update',
ACTIVITY_REMINDER = 'activity_reminder',
MILESTONE_REACHED = 'milestone_reached',
AI_INSIGHT = 'ai_insight',
SYNC_COMPLETE = 'sync_complete',
}
// Mobile: Handle notification tap
Notifications.addNotificationResponseReceivedListener(response => {
const { type, data } = response.notification.request.content;
switch (type) {
case NotificationType.FAMILY_UPDATE:
navigation.navigate('Family', { familyId: data.familyId });
break;
case NotificationType.ACTIVITY_REMINDER:
navigation.navigate('Track', { type: data.activityType });
break;
// ... handle other types
}
});
3. Biometric Authentication
Face ID / Touch ID / Fingerprint
import * as LocalAuthentication from 'expo-local-authentication';
import * as SecureStore from 'expo-secure-store';
export class BiometricAuthService {
async isBiometricAvailable(): Promise<boolean> {
const compatible = await LocalAuthentication.hasHardwareAsync();
const enrolled = await LocalAuthentication.isEnrolledAsync();
return compatible && enrolled;
}
async authenticateWithBiometrics(): Promise<boolean> {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to access Maternal App',
fallbackLabel: 'Use passcode',
});
return result.success;
}
async enableBiometricLogin(userId: string, token: string) {
// Store refresh token securely
await SecureStore.setItemAsync(
`auth_token_${userId}`,
token,
{
keychainAccessible:
SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
}
);
// Enable biometric flag
await SecureStore.setItemAsync(
'biometric_enabled',
'true'
);
}
async loginWithBiometrics(): Promise<string | null> {
const authenticated = await this.authenticateWithBiometrics();
if (!authenticated) {
return null;
}
// Retrieve stored token
const userId = await SecureStore.getItemAsync('current_user_id');
const token = await SecureStore.getItemAsync(`auth_token_${userId}`);
return token;
}
}
4. Voice Input (Whisper)
React Native Voice
import Voice from '@react-native-voice/voice';
export class VoiceInputService {
constructor() {
Voice.onSpeechResults = this.onSpeechResults;
Voice.onSpeechError = this.onSpeechError;
}
async startListening() {
try {
await Voice.start('en-US');
} catch (error) {
console.error('Voice start error:', error);
}
}
async stopListening() {
try {
await Voice.stop();
} catch (error) {
console.error('Voice stop error:', error);
}
}
onSpeechResults = (event: any) => {
const transcript = event.value[0];
// Send to backend for processing with Whisper
this.processTranscript(transcript);
};
onSpeechError = (event: any) => {
console.error('Speech error:', event.error);
};
async processTranscript(transcript: string) {
// Send to backend Whisper API
const response = await fetch('/api/v1/voice/transcribe', {
method: 'POST',
body: JSON.stringify({ transcript }),
});
const { activityData } = await response.json();
return activityData;
}
}
5. Camera & Photo Upload
Expo Image Picker
import * as ImagePicker from 'expo-image-picker';
export class PhotoService {
async requestPermissions() {
const { status } =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert(
'Permission needed',
'Please allow access to photos'
);
return false;
}
return true;
}
async pickImage() {
const hasPermission = await this.requestPermissions();
if (!hasPermission) return null;
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 0.8,
});
if (!result.canceled) {
return result.assets[0].uri;
}
return null;
}
async takePhoto() {
const { status } =
await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
return null;
}
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [4, 3],
quality: 0.8,
});
if (!result.canceled) {
return result.assets[0].uri;
}
return null;
}
async uploadPhoto(uri: string, childId: string) {
const formData = new FormData();
formData.append('file', {
uri,
type: 'image/jpeg',
name: 'photo.jpg',
} as any);
formData.append('childId', childId);
const response = await fetch('/api/v1/children/photo', {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.json();
}
}
Performance Optimization
1. List Virtualization
FlatList for Large Datasets
import { FlatList } from 'react-native';
// ✅ GOOD: Virtualized list for activities
<FlatList
data={activities}
renderItem={({ item }) => <ActivityCard activity={item} />}
keyExtractor={(item) => item.id}
// Performance optimizations
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={10}
windowSize={5}
// Pull to refresh
onRefresh={handleRefresh}
refreshing={isRefreshing}
// Infinite scroll
onEndReached={loadMore}
onEndReachedThreshold={0.5}
/>
// ❌ BAD: Rendering all items at once
{activities.map(activity => <ActivityCard key={activity.id} activity={activity} />)}
2. Image Optimization
React Native Fast Image
import FastImage from 'react-native-fast-image';
// ✅ GOOD: Optimized image loading
<FastImage
source={{
uri: childPhoto,
priority: FastImage.priority.high,
cache: FastImage.cacheControl.immutable,
}}
style={styles.childPhoto}
resizeMode={FastImage.resizeMode.cover}
/>
// Preload images for better UX
FastImage.preload([
{ uri: photo1 },
{ uri: photo2 },
]);
3. Animation Performance
React Native Reanimated 3
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
// ✅ GOOD: Run on UI thread
function AnimatedButton() {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const handlePress = () => {
scale.value = withSpring(0.95, {}, () => {
scale.value = withSpring(1);
});
};
return (
<Animated.View style={animatedStyle}>
<TouchableOpacity onPress={handlePress}>
<Text>Track Activity</Text>
</TouchableOpacity>
</Animated.View>
);
}
4. Bundle Size Optimization
Hermes Engine (for Android)
// android/app/build.gradle
project.ext.react = [
enableHermes: true, // Enable Hermes engine
]
// Results in:
// - Faster startup time
// - Lower memory usage
// - Smaller APK size
Code Splitting
// Lazy load heavy screens
const AIAssistant = lazy(() => import('./screens/AIAssistant'));
const Analytics = lazy(() => import('./screens/Analytics'));
// Use with Suspense
<Suspense fallback={<LoadingSpinner />}>
<AIAssistant />
</Suspense>
Testing Strategy for Mobile
Unit Tests (Jest)
import { renderHook, act } from '@testing-library/react-hooks';
import { useActivityTracking } from './useActivityTracking';
describe('useActivityTracking', () => {
it('should track activity successfully', async () => {
const { result } = renderHook(() => useActivityTracking());
await act(async () => {
await result.current.logActivity({
type: 'feeding',
childId: 'child_123',
});
});
expect(result.current.activities).toHaveLength(1);
});
});
Component Tests (React Native Testing Library)
import { render, fireEvent } from '@testing-library/react-native';
import { TrackingButton } from './TrackingButton';
describe('TrackingButton', () => {
it('should handle press event', () => {
const onPress = jest.fn();
const { getByText } = render(
<TrackingButton onPress={onPress} />
);
fireEvent.press(getByText('Track Feeding'));
expect(onPress).toHaveBeenCalled();
});
});
E2E Tests (Detox)
describe('Activity Tracking Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should log a feeding activity', async () => {
await element(by.id('track-feeding-btn')).tap();
await element(by.id('amount-input')).typeText('120');
await element(by.id('save-btn')).tap();
await expect(element(by.text('Activity saved'))).toBeVisible();
});
});
Platform-Specific Considerations
iOS Specific
1. App Store Guidelines
- ✅ Submit privacy manifest (PrivacyInfo.xcprivacy)
- ✅ Declare data collection practices
- ✅ Request permissions with clear explanations
- ✅ Support all device sizes (iPhone, iPad)
- ✅ Dark mode support required
2. iOS Permissions
<!-- ios/maternal/Info.plist -->
<key>NSCameraUsageDescription</key>
<string>Take photos of your child's milestones</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Save and view photos of your child</string>
<key>NSMicrophoneUsageDescription</key>
<string>Use voice to log activities hands-free</string>
<key>NSFaceIDUsageDescription</key>
<string>Use Face ID for quick and secure login</string>
3. iOS Background Modes
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
</array>
Android Specific
1. Permissions
<!-- android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
2. ProGuard (Code Obfuscation)
# android/app/proguard-rules.pro
-keep class com.maternalapp.** { *; }
-keepclassmembers class * {
@com.facebook.react.uimanager.annotations.ReactProp <methods>;
}
3. App Signing
# Generate release keystore
keytool -genkeypair -v -storetype PKCS12 \
-keystore maternal-app-release.keystore \
-alias maternal-app \
-keyalg RSA -keysize 2048 \
-validity 10000
Deployment & Distribution
App Store (iOS)
1. Build Configuration
# Install dependencies
cd ios && pod install
# Build for production
xcodebuild -workspace MaternalApp.xcworkspace \
-scheme MaternalApp \
-configuration Release \
-archivePath MaternalApp.xcarchive \
archive
# Export IPA
xcodebuild -exportArchive \
-archivePath MaternalApp.xcarchive \
-exportPath ./build \
-exportOptionsPlist ExportOptions.plist
2. TestFlight (Beta Testing)
# Upload to TestFlight
xcrun altool --upload-app \
--type ios \
--file MaternalApp.ipa \
--username "developer@example.com" \
--password "@keychain:AC_PASSWORD"
Google Play (Android)
1. Build AAB (Android App Bundle)
cd android
./gradlew bundleRelease
# Output: android/app/build/outputs/bundle/release/app-release.aab
2. Internal Testing Track
# Upload to Google Play Console
# Use Fastlane or manual upload
Over-the-Air Updates (CodePush)
Setup for rapid iteration
# Install CodePush CLI
npm install -g code-push-cli
# Register app
code-push app add maternal-app-ios ios react-native
code-push app add maternal-app-android android react-native
# Release update
code-push release-react maternal-app-ios ios \
-d Production \
--description "Bug fixes and performance improvements"
Rollback Strategy
# Rollback to previous version if issues detected
code-push rollback maternal-app-ios Production
# Monitor adoption rate
code-push deployment ls maternal-app-ios
Monitoring & Analytics
Crash Reporting (Sentry)
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
environment: __DEV__ ? 'development' : 'production',
tracesSampleRate: 1.0,
});
// Automatic breadcrumbs
Sentry.addBreadcrumb({
category: 'activity',
message: 'User logged feeding activity',
level: 'info',
});
// Custom error context
Sentry.setContext('user', {
id: user.id,
familyId: family.id,
});
Performance Monitoring
import * as Sentry from '@sentry/react-native';
// Monitor screen load time
const transaction = Sentry.startTransaction({
name: 'ActivityTrackingScreen',
op: 'navigation',
});
// ... screen loads ...
transaction.finish();
// Monitor specific operations
const span = transaction.startChild({
op: 'api.call',
description: 'Log activity',
});
await logActivity(data);
span.finish();
Usage Analytics
// Integrate with backend analytics service
import { Analytics } from '@maternal/shared/analytics';
Analytics.track('Activity Logged', {
type: 'feeding',
method: 'voice',
duration: 15000,
});
Analytics.screen('Activity Tracking');
Analytics.identify(user.id, {
familySize: family.members.length,
childrenCount: children.length,
isPremium: subscription.isPremium,
});
Accessibility (WCAG AA Compliance)
Screen Reader Support
import { View, Text, TouchableOpacity } from 'react-native';
<TouchableOpacity
accessible={true}
accessibilityLabel="Log feeding activity"
accessibilityHint="Opens feeding activity tracker"
accessibilityRole="button"
onPress={handlePress}
>
<Text>Track Feeding</Text>
</TouchableOpacity>
Dynamic Font Sizes
import { Text, useWindowDimensions } from 'react-native';
// Respect user's font size preferences
<Text
style={{
fontSize: 16,
lineHeight: 24,
}}
allowFontScaling={true}
maxFontSizeMultiplier={2}
>
Activity logged successfully
</Text>
Color Contrast
// Ensure WCAG AA compliance (4.5:1 ratio for normal text)
const colors = {
primary: '#FF8B7D', // Coral
primaryText: '#1A1A1A', // Dark text on light background
background: '#FFFFFF',
textOnPrimary: '#FFFFFF', // White text on coral
};
// Validate contrast ratios in design system
Security Best Practices
Secure Storage
import * as SecureStore from 'expo-secure-store';
// ✅ GOOD: Encrypted storage for sensitive data
await SecureStore.setItemAsync('auth_token', token);
// ❌ BAD: AsyncStorage for sensitive data (unencrypted)
await AsyncStorage.setItem('auth_token', token);
Certificate Pinning
// Prevent man-in-the-middle attacks
import { configureCertificatePinning } from 'react-native-cert-pinner';
await configureCertificatePinning([
{
hostname: 'api.maternalapp.com',
certificates: [
'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
],
},
]);
Jailbreak/Root Detection
import JailMonkey from 'jail-monkey';
if (JailMonkey.isJailBroken()) {
Alert.alert(
'Security Warning',
'This app may not function properly on jailbroken devices'
);
}
Migration Path from Web to Mobile
Phase 1: Extract Shared Logic
// 1. Move business logic to shared package
// packages/shared/src/services/
export class ActivityService { ... }
export class AIService { ... }
// 2. Update web app to use shared package
import { ActivityService } from '@maternal/shared';
Phase 2: Build Mobile Shell
// 1. Create React Native app with Expo
npx create-expo-app maternal-mobile
// 2. Set up navigation structure
// 3. Integrate shared services
// 4. Build basic UI with React Native Paper
Phase 3: Implement Mobile-Specific Features
// 1. Offline mode with SQLite
// 2. Push notifications
// 3. Biometric auth
// 4. Voice input
// 5. Camera integration
Phase 4: Testing & Optimization
// 1. Unit tests
// 2. Component tests
// 3. E2E tests with Detox
// 4. Performance profiling
// 5. Accessibility audit
Phase 5: Beta Testing & Launch
// 1. TestFlight (iOS)
// 2. Google Play Internal Testing
// 3. Gather feedback
// 4. Iterate based on metrics
// 5. Production launch
Conclusion
This guide provides a comprehensive roadmap for implementing native mobile apps. Key takeaways:
- Code Reusability: Share business logic between web and mobile
- Offline-First: Essential for mobile UX
- Native Features: Leverage platform-specific capabilities
- Performance: Optimize for mobile constraints
- Testing: Comprehensive strategy for quality
- Security: Protect user data on mobile devices
- Analytics: Track usage and iterate
The current web implementation already follows many mobile-friendly patterns, making the transition to React Native straightforward when the time comes.