Some checks failed
Backend CI/CD Pipeline / Lint and Test Backend (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
Backend CI/CD Pipeline / E2E Tests Backend (push) Has been cancelled
Backend CI/CD Pipeline / Build Backend Application (push) Has been cancelled
Backend CI/CD Pipeline / Performance Testing (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
Backend: - Complete NestJS backend implementation with comprehensive features - Analytics: Weekly/monthly reports with PDF/CSV export - Smart notifications: Persistent notifications with milestones and anomaly detection - AI safety: Medical disclaimer triggers and prompt injection protection - COPPA/GDPR compliance: Full audit logging system Frontend: - Updated settings page and analytics components - API integration improvements Docs: - Added implementation gaps tracking - Azure OpenAI integration documentation - Testing and post-launch summaries 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
976 lines
22 KiB
Markdown
976 lines
22 KiB
Markdown
# 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
|
|
|
|
```javascript
|
|
{
|
|
"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**
|
|
```typescript
|
|
// ✅ 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**
|
|
```typescript
|
|
// ❌ 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
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)**
|
|
```javascript
|
|
// 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**
|
|
```typescript
|
|
// 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)
|
|
```typescript
|
|
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)
|
|
```typescript
|
|
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)
|
|
```typescript
|
|
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**
|
|
```markdown
|
|
- ✅ 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**
|
|
```xml
|
|
<!-- 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**
|
|
```xml
|
|
<key>UIBackgroundModes</key>
|
|
<array>
|
|
<string>remote-notification</string>
|
|
<string>fetch</string>
|
|
</array>
|
|
```
|
|
|
|
### Android Specific
|
|
|
|
**1. Permissions**
|
|
```xml
|
|
<!-- 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**
|
|
```bash
|
|
# 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**
|
|
```bash
|
|
# 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)**
|
|
```bash
|
|
# 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)**
|
|
```bash
|
|
cd android
|
|
./gradlew bundleRelease
|
|
|
|
# Output: android/app/build/outputs/bundle/release/app-release.aab
|
|
```
|
|
|
|
**2. Internal Testing Track**
|
|
```bash
|
|
# Upload to Google Play Console
|
|
# Use Fastlane or manual upload
|
|
```
|
|
|
|
### Over-the-Air Updates (CodePush)
|
|
|
|
**Setup for rapid iteration**
|
|
```bash
|
|
# 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**
|
|
```bash
|
|
# 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)
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// 1. Offline mode with SQLite
|
|
// 2. Push notifications
|
|
// 3. Biometric auth
|
|
// 4. Voice input
|
|
// 5. Camera integration
|
|
```
|
|
|
|
### Phase 4: Testing & Optimization
|
|
```typescript
|
|
// 1. Unit tests
|
|
// 2. Component tests
|
|
// 3. E2E tests with Detox
|
|
// 4. Performance profiling
|
|
// 5. Accessibility audit
|
|
```
|
|
|
|
### Phase 5: Beta Testing & Launch
|
|
```typescript
|
|
// 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:
|
|
|
|
1. **Code Reusability**: Share business logic between web and mobile
|
|
2. **Offline-First**: Essential for mobile UX
|
|
3. **Native Features**: Leverage platform-specific capabilities
|
|
4. **Performance**: Optimize for mobile constraints
|
|
5. **Testing**: Comprehensive strategy for quality
|
|
6. **Security**: Protect user data on mobile devices
|
|
7. **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.
|