The official React Native SDK for the Open Wearables project.
The SDK is built with the Expo Module API enabling install the app in Expo Project as well as in React Native CLI projects. It is a wrapper for the native iOS and Android SDKs to allow React Native apps to collect and sync health data.
| Platform | Status |
|---|---|
| iOS | Implemented (via OpenWearablesHealthSDK CocoaPod, requires iOS 15.1+) |
| Android | Implemented (via Maven Local dependency com.openwearables.health:sdk:0.11.2) |
syncNow() has been removed from the JS API, following its removal from the native iOS (0.14.0)
and Android (0.11.2) SDKs. There is no replacement. The native SDKs now resume sync
automatically when the app returns to the foreground, so a manual trigger is no longer needed β
delete any await OpenWearablesHealthSDK.syncNow() calls.
resumeSync() is not a drop-in replacement: it only does anything when a resumable sync session
exists, and is unrelated to triggering a fresh sync round.
getSyncStatus() is now typed as SyncStatus instead of
Record<string, any>, and gained two fields: initialExportDone and isSyncing.
getStoredCredentials() is now typed as StoredCredentials
instead of Record<string, any>, and returns the same eight keys on both platforms.
resumeSync() now resolves false uniformly when there is nothing to resume. It previously
rejected with No resumable sync session on Android while iOS resolved false.
The JS API is identical on both platforms, but two calls cannot behave identically because the underlying native SDKs differ:
| API | iOS | Android |
|---|---|---|
setSyncInterval(minutes) |
No-op. The iOS SDK has no sync-interval API; the system schedules background delivery itself. | Honoured, but floored at 15 minutes by WorkManager. |
configure(host, customSyncURL) |
customSyncURL is ignored β the iOS SDK's configure(host:) accepts no such parameter, and getStoredCredentials().customSyncUrl is therefore always null. |
Both arguments are honoured and reflected in getStoredCredentials(). |
getAvailableProviders() returns a single apple / "Apple Health" entry on iOS, and the installed
provider(s) (google, samsung) on Android. Accordingly setProvider("apple") resolves true on
iOS and any other id resolves false.
Currently, the SDK is only available locally. You can install it using the following command from the project root folder:
npm installWhen we publish the package to npm, we will use the following command (not available yet):
npm install open-wearablesThen, depending if you are using Expo or React Native CLI, follow the instructions below:
Expo projects using the Expo Modules API automatically link native dependencies.
After installing the package, simply run your project.
npx expo run:iosIf your project does not yet contain native directories (ios/ and android/), Expo will automatically generate them.
You can also generate them manually using:
npx expo prebuildFor bare React Native projects, you must ensure that you have installed and configured the expo package before continuing.
After installing the package, install the iOS CocoaPods dependencies:
npx pod-installor manually:
cd ios && pod installThe Android implementation currently relies on a local Maven dependency:
implementation("com.openwearables.health:sdk:0.11.2")
To test the Android integration using mavenLocal, please refer to the setup instructions in the example app:
π example/README.md
You can customize the permission messages displayed to users by configuring the plugin in your app.json or app.config.js.
{
"expo": {
"plugins": [
[
"open-wearables",
{
"healthShareUsage": "Allow $(PRODUCT_NAME) to read your health data.",
"healthUpdateUsage": "Allow $(PRODUCT_NAME) to write health data."
}
]
]
}
}| Option | Description |
|---|---|
| healthShareUsage | Sets the NSHealthShareUsageDescription value in Info.plist. |
| healthUpdateUsage | Sets the NSHealthUpdateUsageDescription value in Info.plist. |
A minimal Expo application demonstrating how to integrate the SDK.
See the example project:
π example/README.md
import OpenWearablesHealthSDK from "open-wearables";
// Configure the SDK with your backend host
OpenWearablesHealthSDK.configure("https://your-api-host.com");
// Sign in (token-based)
OpenWearablesHealthSDK.signIn(userId, accessToken, refreshToken, null);
// Or sign in (API key)
OpenWearablesHealthSDK.signIn(userId, null, null, apiKey);
// Request HealthKit authorization
await OpenWearablesHealthSDK.requestAuthorization([
"steps",
"heartRate",
"sleep",
]);
// Start background sync
await OpenWearablesHealthSDK.startBackgroundSync();Sets the backend host URL for the SDK.
Signs in a user. accessToken, refreshToken, and apiKey are optional.
Signs out the current user.
Updates the stored auth tokens.
Attempts to restore a previously saved session. Synchronous β returns the restored user id, or
null when there is no session to restore.
Returns whether the current session is valid.
Requests HealthKit read permissions for the given data types. Returns true if the authorization was granted.
See [HealthDataType](#healthdatatype) for the full list of supported types.
Starts background health data sync, optionally limiting how many days back to sync. Resolves true
if started successfully, and false when it could not start β most commonly because the SDK is not
configured or no user is signed in. It resolves false rather than rejecting on both platforms.
Widening the range needs
resetAnchors()first. Query anchors survivestopBackgroundSync(), and neither native SDK compares the newsyncDaysBackagainst the previous one. After a first sync completes, a restart with a largersyncDaysBack(orundefined) takes the incremental path and will not backfill the newly-included older days β on iOS because the storedHKQueryAnchoris a position in HealthKit's global change log, on Android because the incremental cursor ismax(storedAnchor, newFloor). Narrowing the range needs no reset.Call
resetAnchors()while sync is stopped, then start:await OpenWearablesHealthSDK.stopBackgroundSync(); OpenWearablesHealthSDK.resetAnchors(); await OpenWearablesHealthSDK.startBackgroundSync(30);Resetting after
startBackgroundSyncalso eventually works, but the immediate full export it tries to trigger is dropped by the SDKs' "sync already in progress" guard, so the backfill is deferred to the next background trigger β and clearing the session and outbox mid-flight can drop batches that were pending upload.
Stops background sync.
Resumes an interrupted sync session, continuing from the records already sent. Resolves false when
there is no resumable session (on both platforms β see Migration to 0.2.0).
Both native SDKs already resume automatically when the app returns to the foreground, so this is a manual retry rather than the primary mechanism β useful when a resume ran and died again while the app stayed foregrounded.
Do not treat the resolved value as "a sync started": calling it while a round is already in
flight is safe but still resolves true having done nothing. Poll getSyncStatus().isSyncing
instead. On iOS the promise only settles once the whole round finishes, which can take minutes.
Gate any "resume" affordance on !isSyncing && hasResumableSession β hasResumableSession stays
true for the duration of an active round, so on its own it does not mean sync is stalled.
Returns whether background sync is currently active.
Returns the current sync status. Synchronous β no await needed.
| Field | Type | Description |
|---|---|---|
hasResumableSession |
boolean |
Whether an interrupted sync session can be resumed. |
sentCount |
number |
Number of records sent in the current session. |
completedTypes |
number |
How many health data types have finished exporting. |
isFullExport |
boolean |
Whether the current session is a full historical export. |
initialExportDone |
boolean |
false while the initial full historical export is still pending or in progress. |
isSyncing |
boolean |
true while a sync round is currently in flight. |
createdAt |
string | null |
ISO8601 timestamp of the current sync session, or null when there is none. |
While initialExportDone === false, the historical backfill has not finished β prompt the user
to keep the app open so the export can complete.
Resets the query anchors, forcing a full re-sync on the next run. Synchronous β no await needed.
Two things to know before calling it:
- The overlapping window is re-uploaded. The next run re-fetches everything inside the current
syncDaysBackrange, including records already sent. There is no client-side dedup β the SDKs expect the backend to deduplicate. - Call it while sync is stopped. If sync is still active,
resetAnchors()immediately kicks off a full export using the currently persistedsyncDaysBack, which is only updated insidestartBackgroundSync.
See the note under startBackgroundSync
for when a reset is required.
Returns the credentials currently stored by the SDK. Synchronous β no await needed.
| Field | Type | Description |
|---|---|---|
userId |
string | null |
The signed-in user id. |
accessToken |
string | null |
Stored access token. |
refreshToken |
string | null |
Stored refresh token. |
apiKey |
string | null |
Stored API key. |
host |
string | null |
Backend host passed to configure(). |
customSyncUrl |
string | null |
Custom sync URL. Always null on iOS β see Platform differences. |
isSyncActive |
boolean |
Whether background sync is currently active. |
provider |
string | null |
"apple" on iOS; "google" or "samsung" on Android; null when none is selected. |
Subscribe to native SDK events using the standard Expo module event emitter:
const subscription = OpenWearablesHealthSDK.addListener(
"onLog",
({ message }) => {
console.log("SDK log:", message);
}
);
const authSub = OpenWearablesHealthSDK.addListener(
"onAuthError",
({ statusCode, message }) => {
console.error(`Auth error ${statusCode}:`, message);
}
);
// Clean up
subscription.remove();
authSub.remove();| Event | Payload | Description |
|---|---|---|
onLog |
{ message: string } |
Log messages emitted by the native SDK |
onAuthError |
{ statusCode: number, message: string } |
Authentication errors |
The following health data type identifiers can be passed to requestAuthorization:
Activity & Mobility
steps, distanceWalkingRunning, distanceCycling, flightsClimbed, walkingSpeed, walkingStepLength, walkingAsymmetryPercentage, walkingDoubleSupportPercentage, sixMinuteWalkTestDistance, activeEnergy, basalEnergy
Heart & Cardiovascular
heartRate, restingHeartRate, heartRateVariabilitySDNN, vo2Max, oxygenSaturation, respiratoryRate
Body Measurements
bodyMass, height, bmi, bodyFatPercentage, leanBodyMass, waistCircumference, bodyTemperature
Blood & Metabolic
bloodGlucose, insulinDelivery, bloodPressureSystolic, bloodPressureDiastolic, bloodPressure
Sleep & Mindfulness
sleep, mindfulSession
Reproductive Health
menstrualFlow, cervicalMucusQuality, ovulationTestResult, sexualActivity
Nutrition
dietaryEnergyConsumed, dietaryCarbohydrates, dietaryProtein, dietaryFatTotal, dietaryWater
Workout
workout
Aliases
restingEnergy, bloodOxygen