react-native-dynamic-island-telemetry
v0.1.0
Published
Unified React Native hook that drives iOS Live Activities / Dynamic Island and Android persistent notification progress cards from a single JavaScript payload.
Maintainers
Readme
react-native-dynamic-island-telemetry
Unified React Native hook that drives iOS Live Activities / Dynamic Island and Android persistent notification progress cards from a single JavaScript payload — with a safe in-memory JS fallback everywhere else.
const { startActivity, updateActivity, endActivity } = useLiveTelemetry();
await startActivity({ title: 'Thai Basil Kitchen', subtitle: 'Order #4821', progress: 0 });
await updateActivity({ progress: 0.6, status: 'Courier is on the way' });
await endActivity({ finalStatus: 'Delivered' });One payload, three surfaces: the Dynamic Island / Lock Screen Live Activity on
iOS 16.1+, an ongoing NotificationCompat progress card on Android, and a
warn-once in-memory fallback when neither is available (Expo Go, web,
old iOS, denied notification permission).
Install
npm install react-native-dynamic-island-telemetry
# or
yarn add react-native-dynamic-island-telemetryPeer dependencies: react (18 or 19) and react-native (>= 0.72). The
package itself has zero runtime dependencies and ships ESM + CJS builds
with bundled type declarations.
Native setup (required for real system UI)
The npm package is pure JS. To render actual system UI you add a small native
module named DynamicIslandTelemetry to your app — reference implementations
and step-by-step guides ship inside the package:
- iOS —
native-setup/ios/README.md: copyTelemetryModule.swift, add theRCT_EXTERN_MODULEshim, create a Live Activity widget extension, and setNSSupportsLiveActivities: YESin Info.plist. - Android —
native-setup/android/README.md: copyTelemetryModule.kt, register theReactPackage, declare and requestPOST_NOTIFICATIONS(API 33+), notification channel included.
Without native setup everything still works — the hook just routes to the JS fallback and no system UI appears.
Quickstart
import { useLiveTelemetry } from 'react-native-dynamic-island-telemetry';
function DeliveryTracker() {
const { startActivity, updateActivity, endActivity, isActive, isSupported, backend } =
useLiveTelemetry();
const track = async () => {
await startActivity({ title: 'Order #4821', subtitle: 'Thai Basil Kitchen', progress: 0 });
await updateActivity({ progress: 0.45, status: 'Courier picked up your order' });
// ... stream more updates ...
await endActivity({ finalStatus: 'Delivered' });
};
return (
<Button
title={isActive ? 'Tracking…' : `Track order (${backend})`}
onPress={track}
disabled={isActive}
/>
);
}A complete simulated food-delivery screen (order stages streaming progress
0 → 1 with an in-app progress bar mirroring the system UI) lives in
example/FoodDeliveryScreen.tsx.
API
useLiveTelemetry(): UseLiveTelemetryResult
| Member | Type | Description |
| --- | --- | --- |
| startActivity | (payload: StartActivityPayload) => Promise<string> | Validates + sanitizes the payload, starts the activity, resolves with an opaque activity id. Starting while another activity is active ends the previous one first (with a warning). |
| updateActivity | (payload: UpdateActivityPayload) => Promise<void> | Pushes new progress/status. Called before startActivity() → warned no-op. Called while a start is still resolving → queued and flushed in order once the id lands. |
| endActivity | (payload?: EndActivityPayload) => Promise<void> | Ends the running activity, optionally with a final status. Warned no-op when nothing is active. |
| isActive | boolean | true while an activity started by this hook instance is live. |
| isSupported | boolean | true once the native module is linked and its isSupported() resolved true. Settles asynchronously after mount. |
| backend | 'ios-live-activity' \| 'android-notification' \| 'fallback' | Which surface renders the activity. Starts as 'fallback', settles after the native probe. |
The hook automatically ends a still-active activity on unmount.
Payload contract
All payloads are validated before crossing the bridge; a malformed payload
throws a typed TelemetryPayloadError (with a code) and never reaches the
native side. Unknown keys are stripped; progress is clamped.
| Payload | Field | Type | Rules |
| --- | --- | --- | --- |
| StartActivityPayload | title | string | required, non-empty |
| | subtitle | string? | optional |
| | progress | number | required; clamped to 0..1, NaN → 0 |
| UpdateActivityPayload | progress | number? | optional; clamped to 0..1, NaN → 0 |
| | status | string? | optional |
| EndActivityPayload | finalStatus | string? | optional (whole payload may be omitted) |
TelemetryPayloadError.code is one of INVALID_PAYLOAD, INVALID_TITLE,
INVALID_SUBTITLE, INVALID_PROGRESS, INVALID_STATUS,
INVALID_FINAL_STATUS.
Other exports
clampProgress(progress)— the 0..1 clamp used internally.validateStartPayload/validateUpdatePayload/validateEndPayload— the sanitizers, exported for advanced use.TelemetryNativeModule— the rigid TypeScript contract the native module must fulfil (startActivity,updateActivity,endActivity,isSupported).getTelemetryNativeModule()/NATIVE_MODULE_NAME— the resolver forNativeModules.DynamicIslandTelemetry(returnsnullwhen not linked).fallbackTelemetryModule— the JS fallback backend (same interface), for manual wiring.TELEMETRY_BACKENDS,TelemetryBackend, payload types.
Fallback behavior matrix
| Environment | backend | isSupported | What happens |
| --- | --- | --- | --- |
| iOS ≥ 16.1, module linked, Live Activities enabled | ios-live-activity | true | Dynamic Island + Lock Screen Live Activity |
| iOS < 16.1, or Live Activities disabled in Settings | fallback | false | Native isSupported() → false; in-memory JS state, warns once |
| Android, module linked, notifications permitted | android-notification | true | Ongoing notification progress card |
| Android, POST_NOTIFICATIONS denied / notifications off | fallback | false | Native isSupported() → false; JS fallback |
| Expo Go (no custom native code) | fallback | false | Module not linked; JS fallback, warns once |
| Web / macOS / Windows / tests | fallback | false | JS fallback |
The fallback keeps the full lifecycle working (ids look like fallback-<n>),
logs a single console.warn on first use, and renders no system UI — your
in-app UI (e.g. the example's progress bar) keeps working unchanged.
License
MIT © Dinesh Kumar
