Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ React Native wrapper for Apple's Screen Time, Device Activity, and Family Contro
- [Select Apps to track](#select-apps-to-track)
- [Time tracking](#time-tracking)
- [Block the shield](#block-the-shield)
- [Web Content Filter Policy](#web-content-filter-policy)
- [Alternative Example: Blocking Apps for a Time Slot](#alternative-example-blocking-apps-for-a-time-slot)
- [Key Concepts Explained](#key-concepts-explained)
- [API Reference](#api-reference-the-list-is-not-exhaustive-yet-please-refer-to-the-typescript-types-for-the-full-list)
Expand Down Expand Up @@ -428,6 +429,78 @@ ReactNativeDeviceActivity.updateShield(
)
```

### Web Content Filter Policy

Use this when you want to block web content without changing your app/category shield behavior, for example enabling adult/explicit site filtering during school/work hours while keeping existing app rules untouched. Under the hood this maps to Apple's [`WebContentSettings`](https://developer.apple.com/documentation/managedsettings/webcontentsettings) on [`ManagedSettingsStore.webContent`](https://developer.apple.com/documentation/managedsettings/managedsettingsstore/webcontent), including filter modes for [`auto(_:except:)`](https://developer.apple.com/documentation/managedsettings/webcontentsettings/filterpolicy/auto%28_%3Aexcept%3A%29), [`specific(_:)`](https://developer.apple.com/documentation/managedsettings/webcontentsettings/filterpolicy/specific%28_%3A%29), and [`all(except:)`](https://developer.apple.com/documentation/managedsettings/webcontentsettings/filterpolicy/all%28except%3A%29).

```typescript
import * as ReactNativeDeviceActivity from "react-native-device-activity";

// Block adult/explicit websites using Apple's automatic filter.
ReactNativeDeviceActivity.setWebContentFilterPolicy({
type: "auto",
});
```

You can also provide explicit blocked and exception domains:

```typescript
ReactNativeDeviceActivity.setWebContentFilterPolicy({
type: "auto",
domains: ["example-adult-site.com"],
exceptDomains: ["example.com"],
});
```

`specific` and `all` are also supported:

```typescript
// Block only the listed domains
ReactNativeDeviceActivity.setWebContentFilterPolicy({
type: "specific",
domains: ["example.com", "another-example.com"],
});

// Block all websites except the listed domains
ReactNativeDeviceActivity.setWebContentFilterPolicy({
type: "all",
exceptDomains: ["example.com"],
});
```

To clear web-content filtering:

```typescript
ReactNativeDeviceActivity.clearWebContentFilterPolicy();
```

To check whether a filter policy is currently active:

```typescript
const isActive = ReactNativeDeviceActivity.isWebContentFilterPolicyActive();
```

You can configure this from background actions as well:

```typescript
ReactNativeDeviceActivity.configureActions({
activityName: "school-hours",
callbackName: "intervalDidStart",
actions: [
{
type: "setWebContentFilterPolicy",
policy: {
type: "auto",
},
},
],
});
```

Notes:
- Apple currently limits `domains` and `exceptDomains` to 50 entries each (depending on selected filter policy).
- Invalid or malformed policy input throws (for example: unknown `type`, missing required arrays, empty domains, or lists over the limit).

## Alternative Example: Blocking Apps for a Time Slot

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 blocked 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.
Expand Down Expand Up @@ -623,6 +696,9 @@ For a complete implementation, see the [example app](https://github.com/Kingstin
| `setFamilyActivitySelectionId` | `{ id: string, familyActivitySelection: string }` | void | Store a family activity selection with given ID |
| `updateShield` | `config`: ShieldConfiguration<br>`actions`: ShieldActions | void | Update the shield UI and actions |
| `configureActions` | `{ activityName: string, callbackName: string, actions: Action[] }` | void | Configure actions for monitor events |
| `setWebContentFilterPolicy` | `policy`: WebContentFilterPolicyInput<br>`triggeredBy?`: string | void | Apply web filtering policy (`auto`, `specific`, `all`, `none`) |
| `clearWebContentFilterPolicy` | `triggeredBy?`: string | void | Clear only the web content filter policy |
| `isWebContentFilterPolicyActive` | None | boolean | Return whether web content filter policy is active |
| `getEvents` | None | DeviceActivityEvent[] | Get history of triggered events |
| `userDefaultsSet` | `key`: string<br>`value`: any | void | Store value in shared UserDefaults |
| `userDefaultsGet` | `key`: string | any | Retrieve value from shared UserDefaults |
Expand Down
4 changes: 2 additions & 2 deletions apps/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1601,7 +1601,7 @@ PODS:
- React-logger
- React-perflogger
- React-utils (= 0.76.9)
- ReactNativeDeviceActivity (0.4.30):
- ReactNativeDeviceActivity (0.5.3):
- ExpoModulesCore
- RNVectorIcons (10.2.0):
- DoubleConversion
Expand Down Expand Up @@ -1947,7 +1947,7 @@ SPEC CHECKSUMS:
React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f
ReactCodegen: 2a46abb2e345dc8efaff0a724f5f8639230eb974
ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9
ReactNativeDeviceActivity: 1017837e25b5b5247b85d7a542b5176b27ee85d3
ReactNativeDeviceActivity: 49c0579e2cbb940f4366b917d9e17491a70c5bd7
RNVectorIcons: 4330d8f8f8f4184f436e0c08ae9950431ffe466e
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
SwiftLint: 365bcd9ffc83d0deb874e833556d82549919d6cd
Expand Down
151 changes: 151 additions & 0 deletions apps/example/ios/Tests/WebContentFilterPolicyTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import ManagedSettings
import XCTest

@available(iOS 15.0, *)
class WebContentFilterPolicyTests: XCTestCase {
override func setUp() {
super.setUp()
clearWebContentFilterPolicy(triggeredBy: "WebContentFilterPolicyTests.setUp")
}

override func tearDown() {
clearWebContentFilterPolicy(triggeredBy: "WebContentFilterPolicyTests.tearDown")
super.tearDown()
}

func testAutoPolicyParsesDomainsAndExceptions() throws {
let parsed = try parseWebContentFilterPolicyInput(
policyInput: [
"type": "auto",
"domains": [
"https://adult.example.com/path"
],
"exceptDomains": [
"safe.example.com"
]
]
)

switch parsed.policy {
case .auto(let domains, except: let exceptDomains):
XCTAssertEqual(domains.compactMap(\.domain).sorted(), ["adult.example.com"])
XCTAssertEqual(exceptDomains.compactMap(\.domain).sorted(), ["safe.example.com"])
default:
XCTFail("Expected auto policy")
}
}

func testSpecificPolicyRejectsMoreThanFiftyDomains() {
// 51 unique domains should fail (Apple limit is 50)
let domains = (1...51).map { index in
return "blocked-\(index).example.com"
}

XCTAssertThrowsError(
try parseWebContentFilterPolicyInput(
policyInput: [
"type": "specific",
"domains": domains
]
)
)
}

func testSpecificPolicyAllowsExactlyFiftyDomains() throws {
let domains = (1...50).map { index in
return "blocked-\(index).example.com"
}

let parsed = try parseWebContentFilterPolicyInput(
policyInput: [
"type": "specific",
"domains": domains
]
)

switch parsed.policy {
case .specific(let parsedDomains):
XCTAssertEqual(parsedDomains.count, 50)
default:
XCTFail("Expected specific policy")
}
}

func testAllPolicyRejectsMoreThanFiftyExceptions() {
// 51 unique domains should fail (Apple limit is 50)
let domains = (1...51).map { index in
return "allowed-\(index).example.com"
}

XCTAssertThrowsError(
try parseWebContentFilterPolicyInput(
policyInput: [
"type": "all",
"exceptDomains": domains
]
)
)
}

func testAutoPolicyNormalizesQueryAndFragmentDomains() throws {
let parsed = try parseWebContentFilterPolicyInput(
policyInput: [
"type": "auto",
"domains": ["example.com?foo=1"],
"exceptDomains": ["safe.example.com#top"]
]
)

switch parsed.policy {
case .auto(let domains, except: let exceptDomains):
XCTAssertEqual(domains.compactMap(\.domain).sorted(), ["example.com"])
XCTAssertEqual(exceptDomains.compactMap(\.domain).sorted(), ["safe.example.com"])
default:
XCTFail("Expected auto policy")
}
}

func testClearPolicyDeactivatesFilter() throws {
try setWebContentFilterPolicy(
policyInput: [
"type": "auto"
],
triggeredBy: "WebContentFilterPolicyTests.testClearPolicyDeactivatesFilter"
)

XCTAssertTrue(isWebContentFilterPolicyActive())

clearWebContentFilterPolicy(
triggeredBy: "WebContentFilterPolicyTests.testClearPolicyDeactivatesFilter"
)

XCTAssertFalse(isWebContentFilterPolicyActive())
}

func testExecuteGenericActionAppliesAndClearsPolicy() {
executeGenericAction(
action: [
"type": "setWebContentFilterPolicy",
"policy": [
"type": "auto",
"domains": ["adult.example.com"],
"exceptDomains": ["safe.example.com"]
]
],
placeholders: [:],
triggeredBy: "WebContentFilterPolicyTests.testExecuteGenericActionAppliesAndClearsPolicy"
)

XCTAssertTrue(isWebContentFilterPolicyActive())

executeGenericAction(
action: [
"type": "clearWebContentFilterPolicy"
],
placeholders: [:],
triggeredBy: "WebContentFilterPolicyTests.testExecuteGenericActionAppliesAndClearsPolicy"
)

XCTAssertFalse(isWebContentFilterPolicyActive())
}
}
78 changes: 78 additions & 0 deletions apps/example/screens/AllTheThings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ export function AllTheThings() {
const [isShieldActive, setIsShieldActive] = useState(false);
const [isShieldActiveWithSelection, setIsShieldActiveWithSelection] =
useState(false);
const [isWebContentFilterPolicyActive, setIsWebContentFilterPolicyActive] =
useState(false);

const refreshIsShieldActive = useCallback(() => {
setIsShieldActive(ReactNativeDeviceActivity.isShieldActive());
Expand All @@ -200,6 +202,17 @@ export function AllTheThings() {
}
}, []);

const refreshWebContentFilterPolicyActive = useCallback(() => {
setIsWebContentFilterPolicyActive(
ReactNativeDeviceActivity.isWebContentFilterPolicyActive(),
);
}, []);

useEffect(() => {
refreshIsShieldActive();
refreshWebContentFilterPolicyActive();
}, [refreshIsShieldActive, refreshWebContentFilterPolicyActive]);

const [pickerVisible, setPickerVisible] = useState(false);

return (
Expand All @@ -221,6 +234,10 @@ export function AllTheThings() {
Shielding current selection:
{isShieldActiveWithSelection ? "✅" : "❌"}
</Text>
<Text>
Web content filter active:
{isWebContentFilterPolicyActive ? "✅" : "❌"}
</Text>

<Button
title={
Expand Down Expand Up @@ -249,6 +266,67 @@ export function AllTheThings() {
/>

<Button title="Get events" onPress={refreshEvents} />
<Button
title="Refresh web filter status"
onPress={refreshWebContentFilterPolicyActive}
/>
<Button
title="Web filter: auto (adult + explicit)"
onPress={() => {
try {
ReactNativeDeviceActivity.setWebContentFilterPolicy({
type: "auto",
});
refreshWebContentFilterPolicyActive();
} catch (error) {
Alert.alert(
"Failed to set web filter",
error instanceof Error ? error.message : "Unknown error",
);
}
}}
/>
<Button
title="Web filter: specific domains only"
onPress={() => {
try {
ReactNativeDeviceActivity.setWebContentFilterPolicy({
type: "specific",
domains: ["example.com", "example.org"],
});
refreshWebContentFilterPolicyActive();
} catch (error) {
Alert.alert(
"Failed to set web filter",
error instanceof Error ? error.message : "Unknown error",
);
}
}}
/>
<Button
title="Web filter: all except example.com"
onPress={() => {
try {
ReactNativeDeviceActivity.setWebContentFilterPolicy({
type: "all",
exceptDomains: ["example.com"],
});
refreshWebContentFilterPolicyActive();
} catch (error) {
Alert.alert(
"Failed to set web filter",
error instanceof Error ? error.message : "Unknown error",
);
}
}}
/>
<Button
title="Clear web filter policy"
onPress={() => {
ReactNativeDeviceActivity.clearWebContentFilterPolicy();
refreshWebContentFilterPolicyActive();
}}
/>

<Button
title="Block all apps"
Expand Down
Loading
Loading