# react-native-device-activity
`react-native-device-activity` is a React Native library (v0.6.1) that provides a TypeScript/JavaScript wrapper around Apple's Screen Time, Device Activity, and Family Controls APIs on iOS 15+. It enables developers to monitor app usage, block applications on schedules, customize the blocking shield UI, set web content filter policies, and react to device activity events — all from React Native code. The library is built with Expo Modules Core and requires special entitlements from Apple (Family Controls distribution) to be used in production builds.
The library is organized around four core Apple APIs: **FamilyControls** (authorization and app selection), **DeviceActivity** (scheduling monitors and threshold events), **ManagedSettings** (applying blocks, whitelists, and web filters), and **ManagedSettingsUI** (configuring the shield appearance shown to users when content is blocked). All persistent state is stored in a shared `UserDefaults` App Group so native extension processes (ActivityMonitor, ShieldAction, ShieldConfiguration) can read and act on configurations even when the main app is in the background or killed.
---
## Installation
Install the package and configure the Expo plugin with your Apple Team ID and App Group.
```bash
npm install react-native-device-activity
```
```json
// app.json
{
"plugins": [
["expo-build-properties", { "ios": { "deploymentTarget": "15.1" } }],
[
"react-native-device-activity",
{
"appleTeamId": "ABCDE12345",
"appGroup": "group.com.myapp.screentime"
}
]
]
}
```
```bash
npx expo prebuild --platform ios
```
---
## `isAvailable`
Returns `true` only on iOS 15+ with the native module loaded; safe to call on Android without crashing.
```typescript
import { isAvailable } from 'react-native-device-activity';
if (!isAvailable()) {
console.log('Screen Time APIs not available on this platform/version');
return;
}
```
---
## `requestAuthorization` / `revokeAuthorization` / `getAuthorizationStatus`
`requestAuthorization` prompts the user for Screen Time permission (individual or child account). `getAuthorizationStatus` synchronously returns the current status (0 = notDetermined, 1 = denied, 2 = approved). `revokeAuthorization` revokes the permission and returns the updated status.
```typescript
import {
requestAuthorization,
revokeAuthorization,
getAuthorizationStatus,
AuthorizationStatus,
} from 'react-native-device-activity';
// Request permission on mount
useEffect(() => {
async function setup() {
const status = getAuthorizationStatus();
if (status === AuthorizationStatus.notDetermined) {
try {
await requestAuthorization('individual'); // or 'child'
} catch (e) {
console.error('Authorization failed:', e);
}
}
}
setup();
}, []);
// Revoke later
const handleRevoke = async () => {
const newStatus = await revokeAuthorization();
console.log('Status after revoke:', newStatus); // 0 | 1 | 2
};
```
---
## `useAuthorizationStatus`
A React hook that reactively tracks authorization status changes (including changes made outside the app on next foreground).
```typescript
import { useAuthorizationStatus, AuthorizationStatus } from 'react-native-device-activity';
function AuthBanner() {
const authorizationStatus = useAuthorizationStatus();
const label = {
[AuthorizationStatus.notDetermined]: 'Not determined',
[AuthorizationStatus.denied]: 'Denied',
[AuthorizationStatus.approved]: 'Approved ✅',
}[authorizationStatus];
return {label} ;
}
```
---
## `pollAuthorizationStatus`
Polls `getAuthorizationStatus` at an interval until a non-`notDetermined` status is returned or max attempts is reached. Useful immediately after `requestAuthorization` since the native API can lag.
```typescript
import { requestAuthorization, pollAuthorizationStatus, AuthorizationStatus } from 'react-native-device-activity';
const handleAuth = async () => {
await requestAuthorization('individual');
const finalStatus = await pollAuthorizationStatus({
pollIntervalMs: 300,
maxAttempts: 15,
});
if (finalStatus === AuthorizationStatus.approved) {
console.log('Ready to monitor');
} else {
console.warn('User denied or timed out:', finalStatus);
}
};
```
---
## `onAuthorizationStatusChange`
Subscribes to native authorization status change events. Returns an `EventSubscription` — call `.remove()` to unsubscribe.
```typescript
import { onAuthorizationStatusChange, AuthorizationStatus } from 'react-native-device-activity';
useEffect(() => {
const sub = onAuthorizationStatusChange(({ authorizationStatus }) => {
if (authorizationStatus === AuthorizationStatus.approved) {
console.log('User approved Screen Time access');
}
});
return () => sub.remove();
}, []);
```
---
## `DeviceActivitySelectionView`
An inline native iOS SwiftUI view that lets the user pick apps, app categories, and web domains to monitor or block. Prone to crashes on large category browsing — always provide a fallback UI behind it.
```typescript
import { useState } from 'react';
import { Modal, View } from 'react-native';
import { DeviceActivitySelectionView } from 'react-native-device-activity';
function AppPicker({ visible, onDismiss }) {
const [selection, setSelection] = useState(null);
return (
{/* Fallback behind native view in case of SwiftUI crash */}
Loading picker…
{
// event.nativeEvent: { familyActivitySelection, applicationCount, categoryCount, webDomainCount, includeEntireCategory }
console.log(`Selected ${event.nativeEvent.applicationCount} apps`);
setSelection(event.nativeEvent.familyActivitySelection);
onDismiss();
}}
/>
);
}
```
---
## `DeviceActivitySelectionSheetView`
Uses Apple's native `.familyActivityPicker(isPresented:)` sheet with built-in Cancel/Done navigation controls. Mount it as a 1×1 invisible anchor; the native side presents the full-screen sheet automatically.
```typescript
import { useState } from 'react';
import { DeviceActivitySelectionSheetView } from 'react-native-device-activity';
function NativeSheetPicker() {
const [visible, setVisible] = useState(false);
const [selection, setSelection] = useState(null);
return (
<>
setVisible(true)} />
{visible && (
setVisible(false)}
onSelectionChange={(event) => {
setSelection(event.nativeEvent.familyActivitySelection);
}}
/>
)}
>
);
}
```
---
## `DeviceActivitySelectionViewPersisted` / `DeviceActivitySelectionSheetViewPersisted`
Persisted variants that store and retrieve the `familyActivitySelection` token on the native side by a string ID. Avoids passing large base64 tokens through JavaScript.
```typescript
import { DeviceActivitySelectionSheetViewPersisted } from 'react-native-device-activity';
// The selection is auto-persisted to UserDefaults under the key "my-blocked-apps"
function PersistedPicker({ visible, onDismiss }) {
return (
<>
{visible && (
{
// event.nativeEvent: { applicationCount, categoryCount, webDomainCount, includeEntireCategory }
console.log('Saved selection with', event.nativeEvent.applicationCount, 'apps');
}}
/>
)}
>
);
}
```
---
## `setFamilyActivitySelectionId` / `getFamilyActivitySelectionId`
Manually store and retrieve a `familyActivitySelection` token by a named ID in the shared `UserDefaults`. This is necessary when using `blockSelection` or `configureActions` with `familyActivitySelectionId`.
```typescript
import {
setFamilyActivitySelectionId,
getFamilyActivitySelectionId,
} from 'react-native-device-activity';
// After the user picks apps, store the token under a stable ID
const handleSelectionChange = (event) => {
setFamilyActivitySelectionId({
id: 'evening-block-selection',
familyActivitySelection: event.nativeEvent.familyActivitySelection,
});
};
// Later, verify it is stored
const token = getFamilyActivitySelectionId('evening-block-selection');
console.log('Stored token:', token ? token.substring(0, 20) + '...' : 'none');
```
---
## `startMonitoring`
Starts a `DeviceActivityMonitor` session for the given activity name, schedule, and optional threshold events. The schedule uses `DateComponents` (Apple's calendar components). Up to 20 simultaneous monitors are allowed.
```typescript
import { startMonitoring } from 'react-native-device-activity';
await startMonitoring(
'evening-block', // activityName: unique string identifier
{
intervalStart: { hour: 19, minute: 0 }, // 7:00 PM daily
intervalEnd: { hour: 23, minute: 59 },
repeats: true,
warningTime: { minute: 5 }, // fire warning callbacks 5 min before
},
[
{
eventName: 'reached_30_minutes',
familyActivitySelection: token, // base64 token from DeviceActivitySelectionView
threshold: { minute: 30 },
includesPastActivity: false,
},
{
eventName: 'reached_60_minutes',
familyActivitySelection: token,
threshold: { minute: 60 },
},
],
);
```
---
## `stopMonitoring`
Stops one or more named monitors. If called with no arguments, stops all active monitors.
```typescript
import { stopMonitoring } from 'react-native-device-activity';
// Stop a specific monitor
stopMonitoring(['evening-block']);
// Stop all monitors
stopMonitoring();
```
---
## `getActivities` / `useActivities`
`getActivities` synchronously returns the names of currently active (running) monitors. `useActivities` is a React hook that re-renders on changes and also returns a `refresh` callback.
```typescript
import { useActivities, stopMonitoring, cleanUpAfterActivity } from 'react-native-device-activity';
function ActiveMonitorsList() {
const [activities, refresh] = useActivities();
return (
<>
{activities.map((name) => (
{name}
{
cleanUpAfterActivity(name); // removes action/event keys from UserDefaults
stopMonitoring([name]);
refresh();
}}
/>
))}
>
);
}
```
---
## `configureActions`
Stores an array of `Action` objects in `UserDefaults` under a key tied to an activity name and callback name (e.g. `actions_for_evening-block_intervalDidStart`). The native ActivityMonitor extension reads and executes these actions in the background when the callback fires.
```typescript
import { configureActions } from 'react-native-device-activity';
// Block apps when the interval starts
configureActions({
activityName: 'evening-block',
callbackName: 'intervalDidStart',
actions: [
{
type: 'blockSelection',
familyActivitySelectionId: 'evening-block-selection',
shieldId: 'evening-shield',
},
],
});
// Unblock when the interval ends
configureActions({
activityName: 'evening-block',
callbackName: 'intervalDidEnd',
actions: [
{ type: 'unblockSelection', familyActivitySelectionId: 'evening-block-selection' },
{ type: 'sendNotification', payload: { title: 'Good night!', body: 'Screen time restrictions lifted.' } },
],
});
// On a specific threshold event, send an HTTP webhook and a notification
configureActions({
activityName: 'time-tracker',
callbackName: 'eventDidReachThreshold',
eventName: 'reached_30_minutes',
actions: [
{
type: 'sendHttpRequest',
url: 'https://api.myapp.com/webhooks/usage',
options: {
method: 'POST',
body: { activityName: 'time-tracker', minutesReached: 30 },
headers: { 'Authorization': 'Bearer MY_TOKEN' },
},
},
{
type: 'sendNotification',
payload: {
title: '30 minutes reached',
body: 'You have used {applicationOrDomainDisplayName} for 30 minutes.',
sound: 'default',
interruptionLevel: 'active',
},
skipIfAlreadyTriggeredWithinMS: 60_000, // don't re-fire within 1 minute
},
],
});
```
---
## `blockSelection` / `unblockSelection`
Immediately apply or remove a `ManagedSettings` block for a saved selection ID or an inline token. The block persists until explicitly cleared.
```typescript
import { blockSelection, unblockSelection, isShieldActive } from 'react-native-device-activity';
// Block by persisted selection ID
blockSelection({ activitySelectionId: 'evening-block-selection' }, 'manual-block');
console.log('Shield active:', isShieldActive()); // true
// Unblock after 5 seconds
setTimeout(() => {
unblockSelection({ activitySelectionId: 'evening-block-selection' }, 'manual-unblock');
console.log('Shield active:', isShieldActive()); // false
}, 5000);
```
---
## `enableBlockAllMode` / `disableBlockAllMode` / `resetBlocks`
`enableBlockAllMode` blocks all apps/websites (no selection needed). `disableBlockAllMode` removes the all-apps block. `resetBlocks` clears the entire blocklist including whitelists and block-all mode.
```typescript
import { enableBlockAllMode, disableBlockAllMode, resetBlocks } from 'react-native-device-activity';
// Block everything during an exam
enableBlockAllMode('exam-started');
// After exam, disable block-all but keep selection blocks
disableBlockAllMode('exam-ended');
// Full reset — clear everything
resetBlocks('end-of-day');
```
---
## `updateShield` / `updateShieldWithId`
Sets the `ShieldConfiguration` (visual appearance) and `ShieldActions` (button behaviors) stored in `UserDefaults`. The native `ShieldConfiguration` and `ShieldAction` extensions read these values when rendering the block screen.
```typescript
import { updateShield, UIBlurEffectStyle } from 'react-native-device-activity';
updateShield(
{
title: 'Take a Break 🌙',
titleColor: { red: 255, green: 255, blue: 255 },
subtitle: 'This app is blocked until midnight.',
subtitleColor: { red: 200, green: 200, blue: 200 },
backgroundBlurStyle: UIBlurEffectStyle.systemMaterialDark,
iconSystemName: 'moon.stars.fill', // SF Symbols name
primaryButtonLabel: 'OK',
primaryButtonLabelColor: { red: 255, green: 255, blue: 255 },
primaryButtonBackgroundColor: { red: 50, green: 50, blue: 200 },
secondaryButtonLabel: 'Open App',
secondaryButtonLabelColor: { red: 180, green: 180, blue: 180 },
},
{
primary: { behavior: 'close' },
secondary: {
behavior: 'defer',
type: 'unblockPossibleFamilyActivitySelection',
},
},
);
```
---
## `addSelectionToWhitelistAndUpdateBlock` / `removeSelectionFromWhitelistAndUpdateBlock` / `clearWhitelistAndUpdateBlock`
Manage the whitelist: apps in the whitelist are exempt from block-all mode. After any change the block is automatically re-applied to reflect the new whitelist.
```typescript
import {
enableBlockAllMode,
addSelectionToWhitelistAndUpdateBlock,
removeSelectionFromWhitelistAndUpdateBlock,
clearWhitelistAndUpdateBlock,
} from 'react-native-device-activity';
// Block everything, but whitelist a specific selection
enableBlockAllMode();
addSelectionToWhitelistAndUpdateBlock(
{ activitySelectionId: 'allowed-apps' },
'whitelist-setup',
);
// Later, remove from whitelist (block-all applies again for that selection)
removeSelectionFromWhitelistAndUpdateBlock(
{ activitySelectionId: 'allowed-apps' },
'whitelist-removal',
);
// Clear the entire whitelist (block-all now applies to everything)
clearWhitelistAndUpdateBlock('full-reset');
```
---
## `setWebContentFilterPolicy` / `clearWebContentFilterPolicy` / `isWebContentFilterPolicyActive`
Apply Apple's `WebContentSettings` filter policy to block or allow web domains independently of app blocking.
```typescript
import {
setWebContentFilterPolicy,
clearWebContentFilterPolicy,
isWebContentFilterPolicyActive,
} from 'react-native-device-activity';
// Auto-filter: block adult content, with additional blocked domains
setWebContentFilterPolicy({
type: 'auto',
domains: ['gambling-site.com'],
exceptDomains: ['safe-exception.com'],
});
// Allowlist-only: block everything except listed domains
setWebContentFilterPolicy({
type: 'all',
exceptDomains: ['school-portal.edu', 'wikipedia.org'],
});
// Block only specific domains
setWebContentFilterPolicy({
type: 'specific',
domains: ['distracting-site.com', 'social-media.com'],
});
console.log('Filter active:', isWebContentFilterPolicyActive()); // true
// Remove web filter
clearWebContentFilterPolicy();
```
---
## `getEvents`
Returns a sorted array of all triggered activity events parsed from `UserDefaults`. Optionally filter by activity name.
```typescript
import { getEvents } from 'react-native-device-activity';
// All events, sorted oldest-first
const allEvents = getEvents();
/*
[
{
activityName: 'time-tracker',
callbackName: 'eventDidReachThreshold',
eventName: 'reached_30_minutes',
lastCalledAt: Date(2024-01-15T09:30:00),
},
...
]
*/
// Filter by activity
const trackerEvents = getEvents('time-tracker');
const totalMinutes = trackerEvents
.filter(e => e.callbackName === 'eventDidReachThreshold')
.reduce((max, e) => {
const mins = parseInt(e.eventName?.replace('reached_', '') ?? '0', 10);
return Math.max(max, mins);
}, 0);
console.log(`Max tracked usage: ${totalMinutes} minutes`);
```
---
## `onDeviceActivityMonitorEvent`
Subscribes to real-time `DeviceActivityMonitor` callback events while the app is alive. The `callbackName` matches the monitor lifecycle: `intervalDidStart`, `intervalDidEnd`, `eventDidReachThreshold`, `intervalWillStartWarning`, `intervalWillEndWarning`, `eventWillReachThresholdWarning`.
```typescript
import { onDeviceActivityMonitorEvent } from 'react-native-device-activity';
useEffect(() => {
const sub = onDeviceActivityMonitorEvent((event) => {
// event: { callbackName: CallbackEventName }
switch (event.callbackName) {
case 'intervalDidStart':
console.log('Monitoring interval started — blocks now active');
break;
case 'intervalDidEnd':
console.log('Monitoring interval ended — blocks lifted');
break;
case 'eventDidReachThreshold':
console.log('Usage threshold reached');
break;
}
});
return () => sub.remove();
}, []);
```
---
## `intersection` / `union` / `difference` / `symmetricDifference`
Perform set operations on two `FamilyActivitySelection` tokens or the current blocklist/whitelist. Returns an `ActivitySelectionWithMetadata` (token + counts).
```typescript
import { intersection, union, difference, symmetricDifference } from 'react-native-device-activity';
const selA = { activitySelectionToken: tokenA };
const selB = { activitySelectionToken: tokenB };
const common = intersection(selA, selB, { stripToken: true });
// { familyActivitySelection: null, applicationCount: 2, categoryCount: 0, webDomainCount: 0 }
const combined = union(selA, selB, {
persistAsActivitySelectionId: 'combined-selection', // save result for later use
});
const onlyInA = difference(selA, selB, { stripToken: true });
const notInBoth = symmetricDifference(selA, selB, { stripToken: true });
console.log('Apps only in A:', onlyInA?.applicationCount);
// Compare current blocklist against a selection
const overlap = intersection(
{ currentBlocklist: true },
selB,
{ stripToken: true },
);
```
---
## `activitySelectionMetadata` / `activitySelectionWithMetadata` / `isEqual` / `isSubsetOf`
Inspect a selection's counts or test relationships between selections without full set operations.
```typescript
import {
activitySelectionMetadata,
activitySelectionWithMetadata,
isEqual,
isSubsetOf,
} from 'react-native-device-activity';
const meta = activitySelectionMetadata({ activitySelectionToken: tokenA });
// { applicationCount: 5, categoryCount: 1, webDomainCount: 0, includeEntireCategory: false }
const withToken = activitySelectionWithMetadata({ activitySelectionToken: tokenA });
// { familyActivitySelection: '...base64...', applicationCount: 5, ... }
// Check if two selections are identical
const same = isEqual(
{ activitySelectionToken: tokenA },
{ activitySelectionId: 'saved-selection' },
);
// Check if a small selection is a subset of a larger one
const isContained = isSubsetOf(
{ activitySelectionToken: smallToken },
{ currentBlocklist: true },
);
console.log('All blocked?', isContained);
```
---
## `userDefaultsSet` / `userDefaultsGet` / `userDefaultsRemove` / `userDefaultsAll` / `userDefaultsClear` / `userDefaultsClearWithPrefix`
Low-level access to the shared App Group `UserDefaults` store. Used internally by `configureActions` and `updateShield`; also useful for custom data sharing between the main app and native extensions.
```typescript
import {
userDefaultsSet,
userDefaultsGet,
userDefaultsRemove,
userDefaultsAll,
userDefaultsClear,
userDefaultsClearWithPrefix,
} from 'react-native-device-activity';
// Store custom config
userDefaultsSet('myapp.config', { theme: 'dark', userId: 'abc123' });
// Retrieve
const config = userDefaultsGet<{ theme: string; userId: string }>('myapp.config');
console.log(config?.theme); // 'dark'
// Remove a single key
userDefaultsRemove('myapp.config');
// Dump all stored values (useful for debugging)
const all = userDefaultsAll();
console.log(JSON.stringify(all, null, 2));
// Clear all keys with a given prefix (e.g., all action configs for one activity)
userDefaultsClearWithPrefix('actions_for_evening-block');
// Nuclear option — wipe entire UserDefaults store
userDefaultsClear();
```
---
## `cleanUpAfterActivity`
Convenience function that clears all `UserDefaults` keys for a given activity name (both `actions_for_${name}` and `events_${name}` prefixes). Call this before stopping monitoring to avoid stale data.
```typescript
import { cleanUpAfterActivity, stopMonitoring } from 'react-native-device-activity';
const ACTIVITY = 'evening-block';
// Clean up stored actions and events, then stop
cleanUpAfterActivity(ACTIVITY);
stopMonitoring([ACTIVITY]);
```
---
## `moveFile` / `copyFile` / `getAppGroupFileDirectory`
File utility functions that operate within the shared App Group container, enabling file sharing between the main app and native extensions (e.g., storing custom shield images).
```typescript
import { getAppGroupFileDirectory, copyFile } from 'react-native-device-activity';
// Get the shared directory path
const sharedDir = getAppGroupFileDirectory();
// e.g. /private/var/mobile/Containers/Shared/AppGroup/ABC.../
// Copy a local image into the shared directory for use as a shield icon
const sourceUri = FileSystem.documentDirectory + 'shield-icon.png';
const destUri = sharedDir + 'shield-icon.png';
copyFile(sourceUri, destUri, true /* overwrite */);
// Reference the relative path in ShieldConfiguration
updateShield(
{ iconAppGroupRelativePath: 'shield-icon.png' },
{ primary: { behavior: 'close' } },
);
```
---
## `reloadDeviceActivityCenter` / `refreshManagedSettingsStore` / `clearAllManagedSettingsStoreSettings`
Utility reset functions for troubleshooting. `reloadDeviceActivityCenter` re-initializes the monitor process. `refreshManagedSettingsStore` forces re-reading of the managed settings. `clearAllManagedSettingsStoreSettings` removes all active blocks, whitelists, and web filters.
```typescript
import {
reloadDeviceActivityCenter,
refreshManagedSettingsStore,
clearAllManagedSettingsStoreSettings,
} from 'react-native-device-activity';
// If events stop arriving, try reloading the center
reloadDeviceActivityCenter();
// If shield state seems stale
refreshManagedSettingsStore();
// Full nuclear reset of all applied settings (does not clear UserDefaults)
clearAllManagedSettingsStoreSettings();
```
---
## Summary
`react-native-device-activity` is the go-to library for building parental control, digital wellbeing, and focus-mode features in React Native iOS apps. The primary use cases are: (1) **scheduled app blocking** — using `startMonitoring` + `configureActions` to automatically block/unblock selections at specific times or after usage thresholds; (2) **real-time usage tracking** — setting up threshold events that fire webhooks, notifications, or app-opens when users reach N minutes of screen time; (3) **whitelist-based focus mode** — calling `enableBlockAllMode` with an exception whitelist so only permitted apps remain accessible; and (4) **web content filtering** — applying `setWebContentFilterPolicy` to restrict or allow specific domains during defined periods, independent of app blocking.
Integration follows a predictable pattern: request authorization → present `DeviceActivitySelectionView` to collect the user's app selection → persist the selection with `setFamilyActivitySelectionId` → call `configureActions` for each lifecycle callback → call `startMonitoring` with the desired schedule. All action execution happens inside Apple's native extension processes, meaning blocks and notifications fire reliably even when the app is terminated. The shared `UserDefaults` App Group is the communication channel between JS and those extensions, so `configureActions` and `updateShield` must be called before the schedule's `intervalDidStart` fires. For debugging, `getEvents()` provides a full history of triggered callbacks, and the various `userDefaults*` utilities let you inspect or reset all stored state.