Add backend with analytics, notifications, and enhanced features
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
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>
This commit is contained in:
975
docs/mobile-app-best-practices.md
Normal file
975
docs/mobile-app-best-practices.md
Normal file
@@ -0,0 +1,975 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user