From 35ac1779c2bbe9e5ab8fcfadc369412dee0acee2 Mon Sep 17 00:00:00 2001 From: arlevoy Date: Mon, 28 Apr 2025 10:10:57 +0200 Subject: [PATCH 01/12] Enhance README.md with detailed explanations of Screen Time APIs, installation instructions, and examples for using the FamilyControl, ShieldConfiguration, and ActivityMonitor APIs. --- README.md | 423 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 338 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index da56bde..afa5ed0 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,170 @@ +# react-native-device-activity + [![Test Status](https://github.com/Kingstinct/react-native-device-activity/actions/workflows/test.yml/badge.svg)](https://github.com/Kingstinct/react-native-device-activity/actions/workflows/test.yml) [![Latest version on NPM](https://img.shields.io/npm/v/react-native-device-activity)](https://www.npmjs.com/package/react-native-device-activity) [![Downloads on NPM](https://img.shields.io/npm/dt/react-native-device-activity)](https://www.npmjs.com/package/react-native-device-activity) [![Discord](https://dcbadge.vercel.app/api/server/5wQGsRfS?style=flat)](https://discord.gg/5wQGsRfS) -# react-native-device-activity +React Native wrapper for Apple's Screen Time, Device Activity and Family Controls APIs. -Provides direct access to Apples Screen Time, Device Activity and Shielding APIs. +⚠️ **Important**: These APIs require [special approval and entitlements from Apple](https://github.com/Kingstinct/react-native-device-activity#family-controls-distribution-entitlement-requires-approval-from-apple). Request this approval as early as possible in your development process. -⚠️ Before planning and starting using these APIs it is highly recommended to familiarize yourself with the [special approval and entitlements required](https://github.com/Kingstinct/react-native-device-activity#family-controls-distribution-entitlement-requires-approval-from-apple). +## Apple's Screen Time APIs Explained (for official details: [WWDC21](https://www.youtube.com/watch?v=DKH0cw9LhtM)) -Please note that it only supports iOS (and requires iOS 15 or higher) and requires a Custom Dev Client to work with Expo. For Android I'd probably look into [UsageStats](https://developer.android.com/reference/android/app/usage/UsageStats), which seems provide more granularity. +Note: Depending on your use case you might not need all the APIs hence not all the new bundle identifier and capabilities are required. Below is a quick overview of the APIs available. -# Examples & Use Cases +### FamilyControl API -## Handle permissions +The FamilyControl API allows your app to access Screen Time data and manage restrictions on apps and websites. -To block apps, you need to request Screen Time permissions. Note that some features (for example, events) may still trigger without permissions; however, this behavior is not guaranteed. +**What it does**: Provides access to selection and monitoring of app/website usage +**Example**: Selecting which apps (e.g., Instagram, TikTok) to monitor or block -```TypeScript -import React, { useEffect } from 'react'; -import * as ReactNativeDeviceActivity from "react-native-device-activity"; -import React, { useEffect } from 'react'; +### ShieldConfiguration API -useEffect(() => { - ReactNativeDeviceActivity.requestAuthorization(); -}, []) +Defines the visual appearance and text shown when users attempt to access blocked content. -You can also revoke permissions: +**What it does**: Customizes the blocking screen UI +**Example**: -```TypeScript +```typescript +const shieldConfig = { + title: "Time for a Break!", + subtitle: "These apps are unavailable until midnight.", + primaryButtonLabel: "OK", + iconSystemName: "moon.stars.fill", +}; +``` + +### ShieldAction API + +Defines what happens when users interact with shield buttons. + +**What it does**: Controls behavior when users tap buttons on the shield +**Example**: + +```typescript +const shieldActions = { + primary: { + behavior: "close", // Just close the shield when OK is tapped + }, +}; +``` + +### ActivityMonitor API + +Schedules and manages when restrictions should be applied or removed. This is what will activate the shield when your app is killed. + +**What it does**: Monitors device activity against schedules and thresholds +**Example**: + +```typescript +// Block social media from 7PM to midnight daily +ReactNativeDeviceActivity.startMonitoring( + "evening_block", + { + intervalStart: { hour: 19, minute: 0 }, + intervalEnd: { hour: 23, minute: 59 }, + repeats: true, + }, + [], +); +``` + +## Installation in managed Expo projects + +1. Install the package: + + ```bash + npm install react-native-device-activity + # or + yarn add react-native-device-activity + ``` + +2. Configure the Expo plugin in your `app.json` or `app.config.js`: + + ```json + "plugins": [ + [ + "expo-build-properties", + { + "ios": { + "deploymentTarget": "15.1" + }, + }, + ], + [ + "react-native-device-activity", + { + "appleTeamId": "", + "appGroup": "group.", + } + ] + ], + ``` + +3. Generate the native projects: + + ```bash + npx expo prebuild --platform ios + ``` + +4. Verify Xcode Targets: After running prebuild, open the `ios` directory in Xcode (`open ios/YourProject.xcworkspace`). Check that you have the following targets in addition to your main app target: + +- `ActivityMonitorExtension` +- `ShieldAction` +- `ShieldConfiguration` + +### Some notes + +- It's not possible to 100% know which familyActivitySelection an event being handled is triggered for in the context of the Shield UI/actions. We try to make a best guess here - prioritizing apps/websites in an activitySelection over categories, and smaller activitySelections over larger ones (i.e. "Instagram" over "Instagram + Facebook" over "Social Media Apps"). This means that if you display a shield specific for the Instagram selection that will take precedence over the less specific shields. +- When determining which familyActivitySelectionId that should be used it will only look for familyActivitySelectionIds that are contained in any of the currently monitored activity names (i.e. if familyActivitySelectionId is "social-media-apps" it will only trigger if there is an activity name that contains "social-media-apps"). This might be a limitation for some implementations, it would probably be nice to make this configurable. + +### Data model + +Almost all the functionality is built around persisting configuration as well as event history to UserDefaults. + +- familyActivitySelectionId mapping. This makes it possible for us to tie a familyActivitySelection token to an id that we can reuse and refer to at a later stage. +- Triggers. This includes configuring shield UI/actions as well as sending web requests or notifications from the Swift background side, in the context of the device activity monitor process. Prefixed like actions*for*${goalId} in userDefaults. This is how we do blocking of apps, updates to shield UI/actions etc. +- Event history. Contains information of which events have been triggered and when. Prefixed like events\_${goalId} in userDefaults. This can be useful for tracking time spent. +- ShieldIds. To reduce the storage strain on userDefaults shields are referenced with shieldIds. + +## Installation in bare React Native projects + +For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing. + +### Add the package to your npm dependencies + +``` +npm install react-native-device-activity +``` + +### Configure for iOS + +Run `npx pod-install` after installing the npm package. + +## Family Controls (distribution) entitlement requires approval from Apple + +As early as possible you want to [request approval from Apple](https://developer.apple.com/contact/request/family-controls-distribution), since it can take time to get approved. + +Note that until you have approval for all bundleIdentifiers you want to use, you are stuck with local development builds in XCode. I.e. you can't even build an Expo Dev Client. + +For every base bundleIdentifier you need approval for 4 bundleIdentifiers (if you want to use all the native extensions that is, you can potentially just use the Shield-related ones if you have no need to listen to the events, or similarly just use the ActivityMonitor if you do not need control over the Shield UI): + +- com.your-bundleIdentifier +- com.your-bundleIdentifier.ActivityMonitor +- com.your-bundleIdentifier.ShieldAction +- com.your-bundleIdentifier.ShieldConfiguration + +Once you've gotten approval you need to manually add the "Family Controls (Distribution)" under Additional Capabilities for each of the bundleIdentifiers on [developer.apple.com](https://developer.apple.com/account/resources/identifiers/list) mentioned above. If you use Expo/EAS this has to be done only once, and after that provisioning will be handled automatically. + +⚠️ If you don't do all the above you will run in to a lot of strange provisioning errors. + +## Basic Example: Event Tracking Approach + +Here's another example that focuses on tracking app usage with time thresholds: + +````typescript import * as ReactNativeDeviceActivity from "react-native-device-activity"; ReactNativeDeviceActivity.revokeAuthorization(); @@ -61,12 +197,12 @@ const DeviceActivityPicker = () => { ) } } -``` +```` Some things worth noting here: - This is a SwiftUI view, which is prone to crashing, especially when browsing larger categories of apps or searching for apps. It's recommended to provide a fallback view (positioned behind the SwiftUI view) that allows the user to know what's happening and reload the view and tailor that to your app's design and UX. -The activitySelection tokens can be particularly large (especially if you use includeEntireCategory flag), so you probably want to reference them through a familyActivitySelectionId instead of always passing the string token around. Most functions in this library accept a familyActivitySelectionId as well as the familyActivitySelection token directly. + The activitySelection tokens can be particularly large (especially if you use includeEntireCategory flag), so you probably want to reference them through a familyActivitySelectionId instead of always passing the string token around. Most functions in this library accept a familyActivitySelectionId as well as the familyActivitySelection token directly. ## Time tracking @@ -116,7 +252,7 @@ const events = ReactNativeDeviceActivityModule.getEvents(); Some things worth noting here: -Depending on your use case (if you need different schedules for different days, for example) you might need multiple monitors. There's a hard limit on 20 monitors at the same time. Study the [DateComponents](https://developer.apple.com/documentation/foundation/datecomponents) object to model this to your use case. +Depending on your use case (if you need different schedules for different days, for example) you might need multiple monitors. There's a hard limit on 20 monitors at the same time. Study the [DateComponents](https://developer.apple.com/documentation/foundation/datecomponents) object to model this to your use case. ## Block the shield @@ -210,90 +346,207 @@ ReactNativeDeviceActivity.updateShield( ) ``` -# Installation in managed Expo projects - -For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release. - -The package requires native code, which includes a custom app target. Currently it requires targeting iOS 15 or higher, so populate app.json/app.config.json as follows: - -``` -"plugins": [ - [ - "expo-build-properties", - { - "ios": { - "deploymentTarget": "15.1" - }, - }, - ], - [ - "react-native-device-activity", - { - "appleTeamId": "", - "appGroup": "group.", - } - ] - ], -``` - -The Swift files for the iOS target will be copied to your local `/targets` directory. You might want to add it to your .gitignore (or if you have other targets in there, you might want to specifically add the three targets added by this library). - -For Expo to be able to automatically handle provisioning you need to specify extra.eas.build.experimental.ios.appExtensions in your app.json/app.config.ts [as seen here](https://github.com/Intentional-Digital/react-native-device-activity/blob/main/example/app.json#L57). - -## Customize native code +## Alternative Example: Blocking Apps for a Time Slot -You can potentially modify the targets manually, although you risk the library and your app code diverging. If you want to disable the automatic copying of the targets, you can set `copyToTargetFolder` to `false` in the plugin configuration [as seen here](https://github.com/Intentional-Digital/react-native-device-activity/blob/main/example/app.json#L53). +This example shows how to implement a complete app blocking system on a given interval. The main principle is that you're configuring these apps to be blocled with FamilyControl API and then schedule when the shield should be shown with ActivityMonitor API. You're customizing the shield UI and actions with ShieldConfiguration and ShieldAction APIs. -## Some notes +```typescript +import { useEffect, useState } from 'react'; +import { Alert } from 'react-native'; +import * as ReactNativeDeviceActivity from 'react-native-device-activity'; -- It's not possible to 100% know which familyActivitySelection an event being handled is triggered for in the context of the Shield UI/actions. We try to make the best guess here, prioritizing apps/websites in an activitySelection over categories, and smaller activitySelections over larger ones (i.e. "Instagram" over "Instagram + Facebook" over "Social Media Apps"). This means that if you display a shield specific for the Instagram selection that will take precedence over the less specific shields. -- When determining which familyActivitySelectionId that should be used it will only look for familyActivitySelectionIds that are contained in any of the currently monitored activity names (i.e. if familyActivitySelectionId is "social-media-apps" it will only trigger if there is an activity name that contains "social-media-apps"). This might be a limitation for some implementations, it would probably be nice to make this configurable. - -## Data model - -Almost all the functionality is built around persisting configuration as well as event history to UserDefaults. +// Constants for identifying your selections, shields and scheduled activities +const SELECTION_ID = "evening_block_selection"; +const SHIELD_CONFIG_ID = "evening_shield_config"; +const ACTIVITY_NAME = "evening_block"; -- familyActivitySelectionId mapping. This makes it possible for us to tie a familyActivitySelection token to an id that we can reuse and refer to at a later stage. -- Triggers. This includes configuring shield UI/actions as well as sending web requests or notifications from the Swift background side, in the context of the device activity monitor process. Prefixed like actions*for*${goalId} in userDefaults. This is how we do blocking of apps, updates to shield UI/actions etc. -- Event history. Contains information of which events have been triggered and when. Prefixed like events\_${goalId} in userDefaults. This can be useful for tracking time spent. -- ShieldIds. To reduce the storage strain on userDefaults shields are referenced with shieldIds. - -# Installation in bare React Native projects - -For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing. - -### Add the package to your npm dependencies +const AppBlocker = () => { + // Step 1: Request authorization when component mounts + useEffect(() => { + ReactNativeDeviceActivity.requestAuthorization().then((status) => { + console.info("Authorization status:", status); + // You need to handle various status outcomes: + // "authorized", "denied", "notDetermined", etc. + }); + }, []); + + // Step 2: Manage the selection state of apps/websites to block + const [currentFamilyActivitySelection, setCurrentFamilyActivitySelection] = + useState(null); + + // Step 3: Handle selection changes from the native selection UI + const handleSelectionChange = (event) => { + // The selection is a serialized string containing the user's app selections + setCurrentFamilyActivitySelection(event.nativeEvent.familyActivitySelection); + }; + + // Step 4: Save the selection for use by the extension + const saveSelection = () => { + if (!currentFamilyActivitySelection) { + Alert.alert("Error", "Please select at least one app to block"); + return; + } + + // Store the selection with a consistent ID so the extension can access it + ReactNativeDeviceActivity.setFamilyActivitySelectionId({ + id: SELECTION_ID, + familyActivitySelection: currentFamilyActivitySelection + }); + + // Now configure the blocking schedule + configureBlocking(); + }; + + // Step 5: Configure the shield (blocking screen UI) + const configureBlocking = () => { + // Define how the blocking screen looks + const shieldConfig = { + title: "App Blocked", + subtitle: "This app is currently unavailable", + primaryButtonLabel: "OK", + iconSystemName: "moon.stars.fill" // SF Symbols icon name + }; + + // Define what happens when users interact with the shield + const shieldActions = { + primary: { + behavior: "close" // Just close the shield when OK is tapped + } + }; + + // Apply the shield configuration + ReactNativeDeviceActivity.updateShield(shieldConfig, shieldActions); + + // Configure what happens when the scheduled interval begins + ReactNativeDeviceActivity.configureActions({ + activityName: ACTIVITY_NAME, + callbackName: "intervalDidStart", // Called when the scheduled time begins + actions: [{ + type: "blockSelection", + familyActivitySelectionId: SELECTION_ID, // The stored selection ID + shieldId: SHIELD_CONFIG_ID // The shield to show when blocked + }] + }); + + // Configure what happens when the scheduled interval ends + ReactNativeDeviceActivity.configureActions({ + activityName: ACTIVITY_NAME, + callbackName: "intervalDidEnd", // Called when the scheduled time ends + actions: [{ + type: "unblockSelection", + familyActivitySelectionId: SELECTION_ID // Unblock the same selection + }] + }); + + // Start the monitoring schedule + startScheduledBlocking(); + }; + + // Step 6: Define and start the blocking schedule + const startScheduledBlocking = async () => { + try { + // Define when blocking should occur (7 PM to midnight daily) + const schedule = { + intervalStart: { hour: 19, minute: 0 }, // 7:00 PM + intervalEnd: { hour: 23, minute: 59 }, // 11:59 PM + repeats: true // Repeat this schedule daily + // Optional: warningTime: { minutes: 5 } // Warn user 5 minutes before blocking starts + }; + + // For testing, you might want a shorter interval that starts soon: + const testSchedule = { + intervalStart: { + hour: new Date().getHours(), + minute: new Date().getMinutes(), + second: (new Date().getSeconds() + 10) % 60, // +10 seconds from now + }, + intervalEnd: { + hour: new Date().getHours() + Math.floor((new Date().getMinutes() + 5) / 60), + minute: (new Date().getMinutes() + 5) % 60, // +5 minutes from start + }, + repeats: false, // One-time test + }; + + // Start monitoring with the schedule + // The empty array is for event monitors (optional) + await ReactNativeDeviceActivity.startMonitoring( + ACTIVITY_NAME, + schedule, // Use testSchedule for testing + [] + ); + + Alert.alert("Success", "Blocking schedule has been set up!"); + } catch (error) { + console.error("Failed to start scheduled blocking:", error); + Alert.alert("Error", "Failed to set up blocking schedule"); + } + }; -``` -npm install react-native-device-activity + return ( + + {/* Native selection view for choosing apps to block */} + + + {/* Save button */} +