The Sonar React Native packages are thin TypeScript bridges over the native iOS and Android SDKs. Both packages share the same interface — initialize, connect, getUserId, disconnect — so your application logic stays consistent across platforms. No Swift or Kotlin code required.
Architecture
Packages
| Package | Platform | Health Stores |
|---|---|---|
@sonar-health/react-native-apple-health |
iOS 14+ | Apple Health |
@sonar-health/react-native-android |
Android API 28+ | Health Connect, Samsung Health |
Install the package(s) for your target platforms:
# npm
npm install @sonar-health/react-native-apple-health
npm install @sonar-health/react-native-android
# yarn
yarn add @sonar-health/react-native-apple-health
yarn add @sonar-health/react-native-android
Then install native dependencies for iOS:
cd ios && pod install
Native Configuration
The TypeScript API is unified, but each platform requires native setup for permissions and capabilities.
iOS Setup
In Xcode, open your .xcworkspace and configure:
1. Enable capabilities under Signing & Capabilities:
- HealthKit — required for Apple Health access
- Background Modes — enable Background fetch and Background processing
2. Update Info.plist (ios/<AppName>/Info.plist):
<key>NSHealthShareUsageDescription</key>
<string>This app reads your health data to provide personalized insights.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>This app saves workout data to Apple Health.</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>co.sonar.health.background</string>
</array>
Android Setup — Health Connect
1. Declare permissions in android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.health.READ_STEPS" />
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />
<uses-permission android:name="android.permission.health.READ_SLEEP" />
<uses-permission android:name="android.permission.health.READ_EXERCISE" />
<uses-permission android:name="android.permission.health.READ_DISTANCE" />
2. Add privacy policy activity:
<activity
android:name="co.sonar.reactnative.android.PrivacyPolicyActivity"
android:exported="true">
<intent-filter>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
</activity>
The package ships with a default PrivacyPolicyActivity that opens a URL you configure during initialization. Replace it with your own activity if needed.
3. Apply for production access through the Google Play Console before releasing.
Android Setup — Samsung Health
- Contact Sonar support to submit a Samsung Health access request
- No additional manifest changes are needed
- Samsung Health app must be installed on the device
Expo Configuration
If you're using Expo with a development build, add the config plugins to app.json instead of editing native files manually:
{
"expo": {
"plugins": [
[
"@sonar-health/react-native-apple-health",
{
"healthShareUsageDescription": "This app reads your health data to provide personalized insights.",
"healthUpdateUsageDescription": "This app saves workout data to Apple Health.",
"backgroundDelivery": true
}
],
[
"@sonar-health/react-native-android",
{
"healthConnectPermissions": [
"READ_STEPS",
"READ_HEART_RATE",
"READ_SLEEP",
"READ_EXERCISE",
"READ_DISTANCE"
],
"privacyPolicyUrl": "https://yourapp.com/privacy"
}
]
]
}
}
Integration Lifecycle
Step 1 — Initialize
Initialize once at app startup — typically in your root component or a context provider. Re-initialize when the app returns to foreground.
import { useEffect, useRef } from 'react';
import { AppState } from 'react-native';
import { SonarHealth } from '@sonar-health/react-native-apple-health';
// or: import { SonarHealth } from '@sonar-health/react-native-android';
function App() {
const sonarRef = useRef<SonarHealth | null>(null);
useEffect(() => {
async function init() {
const token = await fetchTokenFromBackend();
const sonar = await SonarHealth.initialize({ token });
sonarRef.current = sonar;
}
init();
// Re-initialize on foreground
const subscription = AppState.addEventListener('change', (state) => {
if (state === 'active') init();
});
return () => subscription.remove();
}, []);
// ...
}
Step 2 — Connect
Once initialized, connect the user to a health data source. This triggers the native permission dialog.
// Apple Health (iOS)
import { SonarHealth, Source } from '@sonar-health/react-native-apple-health';
async function connectAppleHealth(sonar: SonarHealth) {
try {
await sonar.connect({
source: Source.APPLE_HEALTH,
backgroundSync: true,
});
console.log('Connected to Apple Health');
} catch (error) {
console.error('Connection failed:', error);
}
}
// Health Connect (Android)
import { SonarHealth, Source } from '@sonar-health/react-native-android';
async function connectHealthConnect(sonar: SonarHealth) {
try {
await sonar.connect({
source: Source.HEALTH_CONNECT,
backgroundSync: true,
});
console.log('Connected to Health Connect');
} catch (error) {
console.error('Connection failed:', error);
}
}
// Samsung Health (Android)
import { SonarHealth, Source } from '@sonar-health/react-native-android';
async function connectSamsungHealth(sonar: SonarHealth) {
try {
await sonar.connect({
source: Source.SAMSUNG_HEALTH,
backgroundSync: true,
});
console.log('Connected to Samsung Health');
} catch (error) {
console.error('Connection failed:', error);
}
}
Step 3 — Check Connection
Verify the connection before taking actions that depend on it:
// iOS
const appleUserId = sonar.getUserId(Source.APPLE_HEALTH);
// Android
const hcUserId = sonar.getUserId(Source.HEALTH_CONNECT);
const shUserId = sonar.getUserId(Source.SAMSUNG_HEALTH);
Step 4 — Background Delivery (iOS)
On iOS, call setupBackgroundDelivery at module level to enable background sync:
import { SonarHealth } from '@sonar-health/react-native-apple-health';
// Call this outside of any component, at module level
SonarHealth.setupBackgroundDelivery();
Step 5 — Disconnect
To stop syncing and remove the connection:
// iOS
await sonar.disconnect(Source.APPLE_HEALTH);
// Android
await sonar.disconnect(Source.HEALTH_CONNECT);
await sonar.disconnect(Source.SAMSUNG_HEALTH);
This stops background sync and removes local connection state. Historical data remains available in the Sonar platform through the REST API.
Cross-Platform Patterns
Platform-Aware Module Selection
Use Platform.OS to select the correct package at runtime:
import { Platform } from 'react-native';
const SonarModule = Platform.select({
ios: () => require('@sonar-health/react-native-apple-health'),
android: () => require('@sonar-health/react-native-android'),
})!();
const { SonarHealth, Source } = SonarModule;
Unified Health Hook
A reusable hook that handles initialization and platform differences:
import { useState, useEffect, useRef, useCallback } from 'react';
import { Platform, AppState } from 'react-native';
const SonarModule = Platform.select({
ios: () => require('@sonar-health/react-native-apple-health'),
android: () => require('@sonar-health/react-native-android'),
})!();
const { SonarHealth, Source } = SonarModule;
// Set up background delivery on iOS (module level)
if (Platform.OS === 'ios') {
SonarHealth.setupBackgroundDelivery();
}
export function useSonarHealth() {
const sonarRef = useRef<typeof SonarHealth | null>(null);
const [connected, setConnected] = useState(false);
const initialize = useCallback(async () => {
const token = await fetchTokenFromBackend();
const sonar = await SonarHealth.initialize({ token });
sonarRef.current = sonar;
// Check for existing connection
const source = Platform.OS === 'ios' ? Source.APPLE_HEALTH : Source.HEALTH_CONNECT;
setConnected(!!sonar.getUserId(source));
}, []);
useEffect(() => {
initialize();
const subscription = AppState.addEventListener('change', (state) => {
if (state === 'active') initialize();
});
return () => subscription.remove();
}, [initialize]);
const connect = useCallback(async (source?: typeof Source[keyof typeof Source]) => {
const sonar = sonarRef.current;
if (!sonar) return;
const defaultSource = Platform.OS === 'ios' ? Source.APPLE_HEALTH : Source.HEALTH_CONNECT;
await sonar.connect({
source: source ?? defaultSource,
backgroundSync: true,
});
setConnected(true);
}, []);
return { sonar: sonarRef.current, connected, connect, Source };
}
Multi-Source Android
On Android, you can let the user choose between Health Connect and Samsung Health:
import React from 'react';
import { View, Button, Text } from 'react-native';
import { useSonarHealth } from './useSonarHealth';
export function AndroidSourcePicker() {
const { sonar, connected, connect, Source } = useSonarHealth();
if (connected) {
return <Text>Connected to health data source</Text>;
}
return (
<View style={{ gap: 12 }}>
<Text>Choose a health data source:</Text>
<Button
title="Health Connect"
onPress={() => connect(Source.HEALTH_CONNECT)}
/>
<Button
title="Samsung Health"
onPress={() => connect(Source.SAMSUNG_HEALTH)}
/>
</View>
);
}
Full Example
A complete component showing the end-to-end flow with platform detection:
import React, { useState, useEffect, useRef } from 'react';
import { View, Button, Text, AppState, Platform, StyleSheet } from 'react-native';
const SonarModule = Platform.select({
ios: () => require('@sonar-health/react-native-apple-health'),
android: () => require('@sonar-health/react-native-android'),
})!();
const { SonarHealth, Source } = SonarModule;
// Enable background delivery on iOS
if (Platform.OS === 'ios') {
SonarHealth.setupBackgroundDelivery();
}
export default function HealthScreen() {
const sonarRef = useRef<typeof SonarHealth | null>(null);
const [connected, setConnected] = useState(false);
const [sourceName, setSourceName] = useState('');
useEffect(() => {
initialize();
const subscription = AppState.addEventListener('change', (state) => {
if (state === 'active') initialize();
});
return () => subscription.remove();
}, []);
async function initialize() {
const token = await fetchTokenFromBackend();
const sonar = await SonarHealth.initialize({ token });
sonarRef.current = sonar;
// Check for existing connections
if (Platform.OS === 'ios') {
if (sonar.getUserId(Source.APPLE_HEALTH)) {
setConnected(true);
setSourceName('Apple Health');
}
} else {
if (sonar.getUserId(Source.HEALTH_CONNECT)) {
setConnected(true);
setSourceName('Health Connect');
} else if (sonar.getUserId(Source.SAMSUNG_HEALTH)) {
setConnected(true);
setSourceName('Samsung Health');
}
}
}
async function handleConnect(source: typeof Source[keyof typeof Source], name: string) {
const sonar = sonarRef.current;
if (!sonar) return;
await sonar.connect({ source, backgroundSync: true });
setConnected(true);
setSourceName(name);
}
if (connected) {
return (
<View style={styles.container}>
<Text style={styles.status}>Connected to {sourceName}</Text>
<Text style={styles.hint}>
Data syncs automatically in the background.
</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Connect a health data source</Text>
{Platform.OS === 'ios' ? (
<Button
title="Connect Apple Health"
onPress={() => handleConnect(Source.APPLE_HEALTH, 'Apple Health')}
/>
) : (
<View style={{ gap: 12 }}>
<Button
title="Connect Health Connect"
onPress={() => handleConnect(Source.HEALTH_CONNECT, 'Health Connect')}
/>
<Button
title="Connect Samsung Health"
onPress={() => handleConnect(Source.SAMSUNG_HEALTH, 'Samsung Health')}
/>
</View>
)}
</View>
);
}
async function fetchTokenFromBackend(): Promise<string> {
// Call your backend to generate a mobile token
// See the Authentication Flow section in the SDK overview
return 'token_from_your_backend';
}
const styles = StyleSheet.create({
container: { padding: 20, gap: 16 },
title: { fontSize: 18, fontWeight: '600' },
status: { fontSize: 16, fontWeight: '500' },
hint: { fontSize: 14, color: '#666' },
});
Troubleshooting
| Issue | Platform | Solution |
|---|---|---|
| Permission dialog doesn't appear | iOS | Check HealthKit entitlement is enabled and Info.plist keys are set |
| Permission dialog doesn't appear | Android | Verify Health Connect app is installed and permissions are declared in manifest |
| Background sync not working | iOS | Verify Background Modes capability and BGTaskSchedulerPermittedIdentifiers |
getUserId() returns null |
Both | Re-initialize the SDK with a fresh token and call connect() |
| Data appears incomplete | Both | User may have denied some permissions — check system settings |
| Samsung Health not available | Android | Verify Samsung Health app is installed and Sonar support has approved access |
pod install fails |
iOS | Run pod repo update and ensure deployment target is iOS 14+ |
| Build fails with dependency error | Android | Verify minSdkVersion is 28+ in android/app/build.gradle |
| Expo build fails | Both | Ensure you're using a development build, not Expo Go |
Go Deeper
- Mobile SDK Overview — Architecture, auth flow, and choosing the right SDK
- iOS (Swift) Integration — Full native iOS reference for understanding underlying behavior
- Android (Kotlin) Integration — Full native Android reference
- Data Delivery — Get notified when synced data is ready
Sonar