This is a Remote Medical Care (RMC) telemedicine application built with bare React Native CLI (not Expo). The app integrates with multiple medical devices via Bluetooth and provides features like video consultations, health data tracking, and Apple HealthKit integration.
Key Characteristics:
iOS-only application
React Native 0.73.4 with TypeScript
Heavy native module integration for medical devices
Apple HealthKit integration
Real-time video calls via Agora SDK
ios Folder
Unlike standard React Native projects where the
ios folder can be regenerated using
npx react-native init,
this project contains extensive custom native code that cannot be
regenerated. Deleting the ios folder will result in:
Loss of all HealthKit integration code
Loss of all medical device (Mintti) SDK integrations
Loss of custom camera modules
Loss of real-time waveform visualization components
Loss of Bluetooth device communication bridges
The native code took significant effort to develop and is critical to the app’s functionality.
The native bridges (VisionController.m,
SmarthoController.m, HealthKitBridge.swift)
communicate with React Native via specific event names and data
structures. Changing these without updating the corresponding
TypeScript hooks will break device functionality.
The following native SDKs are tightly integrated and should not be updated without thorough testing:
MinttiSmarthoSDK.framework
MinttiVisionSDK.framework
WYCameraLib.framework
BKCameraLib
pod install After Modifying Podfile Without
Backup
Always backup the ios/Pods directory before making
Podfile changes since some SDK configurations are sensitive.
macOS (required for iOS development)
Xcode 15+ (latest stable version recommended)
Node.js 18+
CocoaPods (sudo gem install cocoapods)
Watchman (brew install watchman)
Ruby (for CocoaPods, comes with macOS)
# 1. Clone the repository
git clone <repository-url>
cd RMC_RN_IOS
# 2. Install JavaScript dependencies
npm install
# or
yarn install
# 3. Install iOS dependencies
cd ios
pod install
cd ..
# 4. Start Metro bundler
npm start
# or
yarn start
# 5. Run the app (in a separate terminal)
npm run ios
# or
yarn ios
Check /src/utils/config.ts for API endpoints and
environment-specific configurations.
| Issue | Solution |
|-------|----------|
| Pod install fails | Run pod repo update then
pod install |
| Build fails with signing error | Open Xcode, select your team in Signing & Capabilities |
| Metro bundler port conflict | Kill process on port 8081:
lsof -i :8081 then kill -9 <PID> |
| Native SDK not found | Ensure Frameworks are properly linked in Xcode |
RMC_RN_IOS/
├── ios/ # iOS native code (DO NOT DELETE)
│ ├── *.swift # Swift native modules
│ ├── *.m # Objective-C native modules
│ ├── CustomCamera/ # Custom camera module
│ ├── Frameworks/ # Native SDKs (Mintti, Camera libs)
│ └── Podfile # iOS dependencies
│
├── src/ # Main application source
│ ├── api/ # API layer
│ │ ├── action/ # Mutation hooks (POST, PUT, DELETE)
│ │ ├── query/ # Query hooks (GET requests)
│ │ └── schema/ # Zod validation schemas
│ │
│ ├── assets/ # Static assets
│ │ ├── fonts/ # Inter font family
│ │ ├── icons/ # SVG and image icons
│ │ └── images/ # App images
│ │
│ ├── components/ # React components
│ │ ├── ui/ # Reusable UI components
│ │ └── [Feature]/ # Feature-specific components
│ │
│ ├── constant/ # App constants
│ │ ├── Colors.ts # Color definitions
│ │ ├── TestMapping.ts # Medical test mappings
│ │ └── AgoraConfig.ts # Video call configuration
│ │
│ ├── i18n/ # Internationalization
│ │ ├── i18n.config.ts # i18next configuration
│ │ └── translations/ # Language files (en, de, es, ar)
│ │
│ ├── nativemodules/ # TypeScript bridges for native code
│ │ ├── SmarthoStethoScope/ # Stethoscope integration
│ │ ├── HealthKit/ # HealthKit bridge
│ │ ├── useMinttiVision.ts # Vision device hook
│ │ ├── EcgChart.tsx # ECG visualization component
│ │ └── BoGraph.tsx # Blood oxygen graph component
│ │
│ ├── screens/ # Screen components (~60 screens)
│ │
│ ├── styles/ # Global styles
│ │ └── style.ts # StyleSheet definitions
│ │
│ └── utils/ # Utilities and helpers
│ ├── hook/ # Custom React hooks
│ ├── store/ # Zustand stores (21 stores)
│ ├── AppNavigation.tsx # Navigation configuration
│ ├── mmkv.ts # MMKV storage setup
│ └── config.ts # App configuration
│
├── App.tsx # Root component
├── index.js # App entry point
├── package.json # Dependencies
├── tsconfig.json # TypeScript config
├── babel.config.js # Babel config (NativeWind, Reanimated)
├── tailwind.config.js # Tailwind CSS config
└── metro.config.js # Metro bundler config
| Folder | Purpose |
|--------|---------|
| src/api/action/ | Contains 47 mutation hooks for form
submissions, updates, and deletions |
| src/api/query/ | Contains 35 query hooks for fetching
data (appointments, tests, health data) |
| src/api/schema/ | Zod schemas for request/response
validation |
| src/components/ui/ | 18 reusable components: Button,
FormInput, DropDown, Loader, etc. |
| src/nativemodules/ | TypeScript wrappers for native iOS
modules |
| src/utils/store/ | 21 Zustand stores for state
management |
| src/utils/hook/ | 11 custom hooks for permissions,
Bluetooth, camera, etc. |
| ios/ | Native iOS code, frameworks, and Xcode project |
This section details the custom native modules that bridge iOS functionality to React Native.
React Native (TypeScript)
│
▼
NativeModules API
│
▼
Native Bridge (Objective-C/Swift)
│
▼
Native SDK / Framework
Native File: ios/HealthKitBridge.swift
Purpose: Reads health data from Apple Health (50+ metrics)
Supported Metrics:
Steps, distance, calories
Heart rate, blood pressure
Blood glucose, blood oxygen
Sleep analysis
Body measurements (weight, height, BMI)
TypeScript Usage:
// src/nativemodules/HealthKit/
import { NativeModules } from 'react-native';
const { HealthKitBridge } = NativeModules;
// Request authorization
await HealthKitBridge.requestAuthorization();
// Fetch health data
const steps = await HealthKitBridge.getSteps(startDate, endDate);
Native File: ios/VisionController.m
Purpose: Multi-parameter medical device integration
Capabilities:
ECG (Electrocardiogram)
SpO2 (Blood oxygen saturation)
Blood pressure
Body temperature
Blood glucose
Heart rate
Event Types Emitted (16 events):
onBleState - Bluetooth state changes
onDeviceFound - Device discovery
onConnected / onDisconnected -
Connection status
onEcgData - ECG waveform data
onSpO2Data - Blood oxygen data
onBpData - Blood pressure readings
onTempData - Temperature readings
onBatteryInfo - Device battery level
TypeScript Hook:
src/nativemodules/useMinttiVision.ts
import { useMinttiVision } from '@/nativemodules/useMinttiVision';
const {
startScan,
stopScan,
connect,
disconnect,
startEcgMeasurement,
// ... other methods
} = useMinttiVision();
Native File: ios/SmarthoController.m
Purpose: Digital stethoscope for heart and lung sounds
Capabilities:
Heart sound recording
Lung sound recording
Real-time audio streaming
Heart rate detection
TypeScript Hook:
src/nativemodules/SmarthoStethoScope/useStethoScope.tsx
import { useStethoScope } from '@/nativemodules/SmarthoStethoScope/useStethoScope';
const {
scanDevices,
connectDevice,
startRecording,
stopRecording,
} = useStethoScope();
Native Files:
ios/CustomCamera/RCTCameraViewManager.m
ios/CustomCamera/FYB800CameraModule.m
Purpose: Integration with dental/dermatoscope cameras (FYB800 device)
Native Views (Objective-C):
| File | Purpose | React Component |
|------|---------|-----------------|
| EcgChartViewManager.m | ECG waveform rendering |
<EcgChart /> |
| BoGraphViewManager.m | Blood oxygen graph |
<BoGraph /> |
| HeartLive.m | Real-time heart rate | Used internally |
| OxyWaveView.m | SpO2 waveform | Used internally |
| WaveView.m | General waveform | Used internally |
Usage in React Native:
import EcgChart from '@/nativemodules/EcgChart';
import BoGraph from '@/nativemodules/BoGraph';
// In your component
<EcgChart data={ecgData} />
<BoGraph data={spO2Data} />
Native File: ios/PCMDataPlayer.m
Purpose: Plays PCM audio data from stethoscope recordings
// Pattern used throughout the app
import { NativeModules, NativeEventEmitter } from 'react-native';
const { VisionController } = NativeModules;
const eventEmitter = new NativeEventEmitter(VisionController);
// Subscribe to events
useEffect(() => {
const subscription = eventEmitter.addListener('onEcgData', (data) => {
// Handle ECG data
});
return () => subscription.remove();
}, []);
// Call native methods
VisionController.startEcgMeasurement();
The app uses a multi-layered styling system:
Configuration: tailwind.config.js
module.exports = {
content: ['./App.{js,jsx,ts,tsx}', './src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
primary: '#46b98d', // Main brand color (green)
accent: '#fc5e53', // Accent color (red)
secondary: '#60A5FA', // Secondary color (blue)
},
},
},
};
Usage:
<View className="flex-1 bg-white p-4">
<Text className="text-primary text-lg font-bold">Hello</Text>
<TouchableOpacity className="bg-primary rounded-lg p-3">
<Text className="text-white text-center">Button</Text>
</TouchableOpacity>
</View>
Location: src/styles/style.ts
import { StyleSheet } from 'react-native';
export const globalStyles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
shadow: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
});
Font Family: Inter (Regular, Medium, SemiBold, Bold, Thin)
Linked via: react-native.config.js
// Usage
<Text style={{ fontFamily: 'Inter-Bold' }}>Bold Text</Text>
// or with Tailwind
<Text className="font-bold">Bold Text</Text>
Prefer Tailwind classes for common styles
Use StyleSheet for complex or reusable styles
Avoid inline style objects when possible (performance)
Use the defined color palette from tailwind.config.js
Why Zustand:
Minimal boilerplate
TypeScript support
Easy persistence with MMKV
No providers needed
Located in src/utils/store/ (21 stores):
| Store | Purpose |
|-------|---------|
| useSignInStore | User authentication and profile |
| useMinttiVisionStore | BLE device state, scanning,
battery |
| useSmarthoInitalization | Stethoscope initialization |
| useMeetingStore | Video call state |
| useLanguageStore | App language preference |
| useBookAppointmentStore | Appointment booking form |
| useUnitStore | Measurement unit preferences |
| useChatStatus | Chat connection status |
| useConsentStore | User consent tracking |
// src/utils/store/useExampleStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { mmkvStorage } from '../mmkv';
interface ExampleState {
value: string;
setValue: (value: string) => void;
reset: () => void;
}
export const useExampleStore = create<ExampleState>()(
persist(
(set) => ({
value: '',
setValue: (value) => set({ value }),
reset: () => set({ value: '' }),
}),
{
name: 'example-storage',
storage: createJSONStorage(() => mmkvStorage),
}
)
);
Location: src/utils/mmkv.ts
MMKV is used instead of AsyncStorage for:
10x faster read/write
Synchronous operations
Encryption support
src/api/
├── action/ # 47 mutation hooks (useMutation)
├── query/ # 35 query hooks (useQuery)
└── schema/ # Zod validation schemas
Query Hook Pattern:
// src/api/query/useGetAllAppointments.tsx
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
export const useGetAllAppointments = (userId: string) => {
return useQuery({
queryKey: ['appointments', userId],
queryFn: async () => {
const response = await axios.get(`/appointments/${userId}`);
return response.data;
},
enabled: !!userId,
});
};
Mutation Hook Pattern:
// src/api/action/useCreateAppointment.tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
export const useCreateAppointment = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: AppointmentData) => {
const response = await axios.post('/appointments', data);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['appointments'] });
},
});
};
Location: src/api/schema/
// src/api/schema/loginSchema.ts
import { z } from 'zod';
export const loginSchema = z.object({
username: z.string().min(3, 'Username must be at least 3 characters'),
password: z.string().min(1, 'Password is required'),
});
export type LoginFormData = z.infer<typeof loginSchema>;
With React Hook Form:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { loginSchema, LoginFormData } from '@/api/schema/loginSchema';
const { control, handleSubmit } = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
Configuration:
src/utils/AppNavigation.tsx
Native Stack - Main screen navigation
Drawer - Side menu navigation
Material Top Tabs - Tab-based screens
// Simplified type definition
type HomeStackNavigatorParamList = {
// Auth
Login: undefined;
Register: undefined;
// Main
Home: undefined;
Dashboard: undefined;
// Appointments
BookAppointment: { doctorId: string };
AppointmentDetails: { appointmentId: string };
// Health Tests
ECGTest: { appointmentId: string };
BloodPressureTest: { appointmentId: string };
BloodGlucoseTest: { appointmentId: string };
// Devices
InitMinttiVision: undefined;
InitSmartho: undefined;
// ... 50+ more routes
};
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
type NavigationProp = NativeStackNavigationProp<HomeStackNavigatorParamList>;
const navigation = useNavigation<NavigationProp>();
// Navigate
navigation.navigate('AppointmentDetails', { appointmentId: '123' });
// Go back
navigation.goBack();
// Reset stack
navigation.reset({
index: 0,
routes: [{ name: 'Home' }],
});
React Query for server state (API data)
Zustand for client state (UI state, forms, preferences)
hooks/
├── useBluetoothPermission.ts # Permission management
├── useDentalCamera.ts # Camera device logic
├── useNetworkStatus.ts # Connectivity status
└── ...
React Hook Form for form state
Zod for validation
Custom FormInput component for consistency
Languages Supported: English, German, Spanish, Arabic
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
<Text>{t('common.submit')}</Text>
components/
├── ui/ # Shared UI components
├── BookAppointment/ # Appointment booking components
├── HealthData/ # Health data display components
├── Devices/ # Device management components
└── ...
| Category | Technology | Version |
|----------|------------|---------|
| Framework | React Native | 0.73.4 |
| Language | TypeScript | 5.0.4 |
| UI Library | React | 18.2.0 |
| State Management | Zustand | 4.5.0 |
| Data Fetching | TanStack React Query | 5.20.5 |
| HTTP Client | Axios | 1.6.7 |
| Forms | React Hook Form | 7.50.1 |
| Validation | Zod | 3.22.4 |
| Styling | NativeWind + Tailwind | 2.0.11 / 3.2.2 |
| Navigation | React Navigation | 6.x |
| Animations | React Native Reanimated | 3.7.0 |
| Storage | MMKV | Latest |
| i18n | i18next | 23.14.0 |
| Video Calls | Agora RTC SDK | Latest |
| Messaging | Agora RTM SDK | Latest |
| Testing | Jest | 29.6.3 |
The app uses Agora RTC SDK for real-time video consultations and Agora RTM SDK for in-app messaging between patients and doctors.
The Agora configuration file is gitignored to protect credentials. You must create it manually.
Step 1: Rename or create the config file:
# If an example file exists:
cp src/constant/agoraConfig.example.ts src/constant/agoraConfig.ts
# Or create it manually
touch src/constant/agoraConfig.ts
Step 2: Add your Agora credentials to
src/constant/agoraConfig.ts:
import {VideoContentHint} from 'react-native-agora';
function generateRandomLargeNumber(
type: 'screen-sharing' | 'dental-cam-android' | 'dental-cam-ios',
): number {
let min: number;
let max: number;
switch (type) {
case 'screen-sharing':
min = 10000000;
max = 10000099;
break;
case 'dental-cam-android':
min = 10000100;
max = 10000199;
break;
case 'dental-cam-ios':
min = 10000200;
max = 10000299;
break;
default:
throw new Error('Invalid type passed');
}
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Replace with your Agora credentials
const agoraConfig = {
appId: 'YOUR_AGORA_APP_ID',
screenSharingUID: generateRandomLargeNumber('screen-sharing'),
dentalCamUID: generateRandomLargeNumber('dental-cam-ios'),
product: null as any,
};
const agoraChatConfig = {
APP_KEY: 'YOUR_AGORA_CHAT_APP_KEY',
WS_ADDRESS: 'YOUR_WEBSOCKET_ADDRESS',
REST_API: 'YOUR_REST_API_ADDRESS',
};
const screenSharingConfig = {
sampleRate: 16000,
channels: 2,
captureSignalVolume: 0,
width: 640,
height: 360,
frameRate: 10,
bitrate: 500,
contentHint: VideoContentHint.ContentHintNone,
captureAudio: true,
captureVideo: true,
};
export {agoraChatConfig, agoraConfig, screenSharingConfig};
Go to Agora Console
Create a new project or select existing one
Copy the App ID from project settings
Enable Agora Chat if using messaging features
Get Chat credentials (APP_KEY, WS_ADDRESS, REST_API) from Chat settings
| File | Purpose |
|------|---------|
| src/constant/agoraConfig.ts | Agora credentials and
config (gitignored) |
| src/screens/AppointmentMeeting.tsx | Video call screen
implementation |
| src/utils/hook/useRtm.tsx | RTM (Real-time Messaging)
hook |
| src/components/RemoteUserContainer.tsx | Remote
participant video view |
| src/utils/store/useMeetingStore.ts | Video call state
management |
Video Consultations: Real-time video calls between patients and doctors
Screen Sharing: Share device screen during consultations
Dental Camera Streaming: Stream from dental camera devices
In-app Chat: Text messaging via Agora RTM
Never commit agoraConfig.ts to version control
The file is listed in .gitignore
The app is deployed manually using Xcode’s archive and upload workflow:
1. Open ios/RMC.xcworkspace in Xcode
2. Select "Any iOS Device (arm64)" as the build target
3. Go to Product > Archive
4. Wait for the archive to complete
1. Once archived, the Organizer window opens automatically
2. Select the archive and click "Distribute App"
3. Choose "App Store Connect" > "Upload"
4. Follow the prompts to upload the build
5. Go to App Store Connect to submit for review
There are three apps in the App Store Connect account:
| App Name | Purpose | Server |
|----------|---------|--------|
| Telecare Connect | Testing app with latest code | Development/Test server |
| Remote Medical Care | Production app on App Store | Clinic server (production) |
| Third App | Legacy/Other | - |
Important:
Telecare Connect is used for internal testing and QA
Remote Medical Care is the main production app available to end users
Telecare Connect and Remote Medical Care are two separate apps with different:
Bundle Identifiers
App Names
App Icons
Splash Screens
The assets (icons) for both apps are provided in the
docs/ folder.
To deploy to a different app (e.g., from Telecare Connect to Remote Medical Care), follow these steps:
Open ios/RMC/Info.plist in Xcode or a text editor:
Change Bundle Identifier:
For Telecare Connect: Use the test app bundle ID
For Remote Medical Care: Use the production app bundle ID
Change App Display Name:
<key>CFBundleDisplayName</key>
<string>Remote Medical Care</string> <!-- or "Telecare Connect" -->
<key>CFBundleShortVersionString</key>
<string>1.0.0</string> <!-- App version shown to users -->
<key>CFBundleVersion</key>
<string>1</string> <!-- Build number, increment for each upload -->
Navigate to
ios/RMC/Images.xcassets/AppIcon.appiconset/
Replace the icon images with the appropriate set from
docs/ folder:
Use Telecare Connect icons for testing app
Use Remote Medical Care icons for production app
Navigate to ios/RMC/Images.xcassets/ (or the
LaunchScreen assets)
Replace the splash screen images with the appropriate set from
docs/ folder:
Use Telecare Connect splash for testing app
Use Remote Medical Care splash for production app
ios/RMC/LaunchScreen.storyboard
Check src/utils/config.ts to ensure the correct API
endpoints are configured
Remote Medical Care must point to clinic/production server credentials
Before deploying to production (Remote Medical Care):
Update Bundle Identifier in Info.plist
Increment version number (CFBundleShortVersionString) if needed
Increment build number (CFBundleVersion)
Verify production server endpoints in config.ts
Test the archive on a physical device
Ensure all required app icons and screenshots are up to date
Check signing certificates and provisioning profiles are valid
# Start Metro bundler
npm start
# Run iOS app
npm run ios
# Run iOS on specific simulator
npm run ios -- --simulator="iPhone 15 Pro"
# Install new dependency
npm install <package-name>
# Install iOS pods after adding native dependency
cd ios && pod install && cd ..
# Clear Metro cache
npm start -- --reset-cache
# Clean iOS build
cd ios && xcodebuild clean && cd ..
# Open Xcode project
open ios/RMC.xcworkspace
# View Metro logs
npm start
# View native logs
# Open Xcode > Window > Devices and Simulators > View Device Logs
# React Native Debugger
# Shake device or Cmd+D > Debug with Chrome
Create screen in src/screens/NewScreen.tsx
Add route to src/utils/AppNavigation.tsx
Add type to HomeStackNavigatorParamList
Navigate using navigation.navigate('NewScreen')
Query: Create file in
src/api/query/useGetSomething.tsx
Mutation: Create file in
src/api/action/useDoSomething.tsx
Schema: If needed, add to
src/api/schema/
Last updated: January 2026