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

Your React Native AppiOS BridgeAndroid BridgeApple Health(HealthKit)Health Connect /Samsung HealthSonar BackendSonarSDK(Swift)sonar-android(Kotlin) iOSAndroidreadreadsyncsync Your React Native AppiOS BridgeAndroid BridgeApple Health(HealthKit)Health Connect /Samsung HealthSonar BackendSonarSDK(Swift)sonar-android(Kotlin) iOSAndroidreadreadsyncsync

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:

bash
# npm
npm install @sonar-health/react-native-apple-health
npm install @sonar-health/react-native-android
bash
# yarn
yarn add @sonar-health/react-native-apple-health
yarn add @sonar-health/react-native-android

Then install native dependencies for iOS:

bash
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):

xml
<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:

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:

xml
<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

  1. Contact Sonar support to submit a Samsung Health access request
  2. No additional manifest changes are needed
  3. 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:

json
{
  "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.

typescript
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.

typescript
// 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);
  }
}
typescript
// 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);
  }
}
typescript
// 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:

typescript
// 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:

typescript
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:

typescript
// 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:

typescript
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:

typescript
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:

typescript
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:

typescript
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