expo-app-blocker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,225 @@
1
+ import {
2
+ requireNativeModule,
3
+ requireNativeViewManager,
4
+ EventEmitter,
5
+ } from "expo-modules-core";
6
+ import { Platform } from "react-native";
7
+ import React from "react";
8
+
9
+ import type {
10
+ PermissionStatus,
11
+ AndroidPermissions,
12
+ IOSPermissions,
13
+ AndroidBlockableApp,
14
+ IOSBlockedItem,
15
+ IOSBlockConfiguration,
16
+ TemporaryUnlockResult,
17
+ RelockResult,
18
+ } from "./ExpoAppBlocker.types";
19
+
20
+ export type {
21
+ PermissionStatus,
22
+ AndroidPermissions,
23
+ IOSPermissions,
24
+ AndroidBlockableApp,
25
+ IOSBlockedItem,
26
+ IOSBlockConfiguration,
27
+ TemporaryUnlockResult,
28
+ RelockResult,
29
+ ShieldConfig,
30
+ PluginConfig,
31
+ } from "./ExpoAppBlocker.types";
32
+
33
+ // ──────────────────────────────────────────────────────────────────────────────
34
+ // Native module bridge
35
+ // ──────────────────────────────────────────────────────────────────────────────
36
+
37
+ const NativeModule = requireNativeModule("ExpoAppBlocker");
38
+
39
+ // ──────────────────────────────────────────────────────────────────────────────
40
+ // Permissions
41
+ // ──────────────────────────────────────────────────────────────────────────────
42
+
43
+ export async function getPermissionStatus(): Promise<PermissionStatus> {
44
+ if (Platform.OS === "android") {
45
+ const overlay = await NativeModule.checkOverlayPermission();
46
+ const usageStats = await NativeModule.checkUsageStatsPermission();
47
+ const details: AndroidPermissions = { platform: "android", overlay, usageStats };
48
+ return { allGranted: overlay && usageStats, details };
49
+ }
50
+
51
+ if (Platform.OS === "ios") {
52
+ const result = NativeModule.getAuthorizationStatus();
53
+ const details: IOSPermissions = {
54
+ platform: "ios",
55
+ authorized: result.authorized,
56
+ status: result.status,
57
+ };
58
+ return { allGranted: result.authorized, details };
59
+ }
60
+
61
+ throw new Error("Unsupported platform");
62
+ }
63
+
64
+ export async function requestPermissions(): Promise<PermissionStatus> {
65
+ if (Platform.OS === "ios") {
66
+ const result = await NativeModule.requestAuthorization();
67
+ const details: IOSPermissions = {
68
+ platform: "ios",
69
+ authorized: result.authorized,
70
+ status: result.status,
71
+ };
72
+ return { allGranted: result.authorized, details };
73
+ }
74
+ return getPermissionStatus();
75
+ }
76
+
77
+ // ──────────────────────────────────────────────────────────────────────────────
78
+ // Android-specific: permission settings
79
+ // ──────────────────────────────────────────────────────────────────────────────
80
+
81
+ export function openOverlaySettings(): void {
82
+ if (Platform.OS !== "android") return;
83
+ NativeModule.openOverlaySettings();
84
+ }
85
+
86
+ export function openUsageStatsSettings(): void {
87
+ if (Platform.OS !== "android") return;
88
+ NativeModule.openUsageStatsSettings();
89
+ }
90
+
91
+ // ──────────────────────────────────────────────────────────────────────────────
92
+ // Android-specific: app list and blocking
93
+ // ──────────────────────────────────────────────────────────────────────────────
94
+
95
+ export async function getInstalledApps(): Promise<AndroidBlockableApp[]> {
96
+ if (Platform.OS !== "android") return [];
97
+ return NativeModule.getInstalledApps();
98
+ }
99
+
100
+ export function setBlockedApps(packageNames: string[]): void {
101
+ if (Platform.OS !== "android") return;
102
+ NativeModule.setBlockedApps(packageNames);
103
+ }
104
+
105
+ export function getBlockedApps(): string[] {
106
+ if (Platform.OS !== "android") return [];
107
+ return NativeModule.getBlockedApps();
108
+ }
109
+
110
+ export function startMonitoring(): void {
111
+ if (Platform.OS !== "android") return;
112
+ NativeModule.startMonitoring();
113
+ }
114
+
115
+ export function stopMonitoring(): void {
116
+ if (Platform.OS !== "android") return;
117
+ NativeModule.stopMonitoring();
118
+ }
119
+
120
+ // ──────────────────────────────────────────────────────────────────────────────
121
+ // iOS-specific: Family Controls
122
+ // ──────────────────────────────────────────────────────────────────────────────
123
+
124
+ export async function presentFamilyActivityPicker(): Promise<IOSBlockedItem[]> {
125
+ if (Platform.OS !== "ios") {
126
+ throw new Error("Family Activity Picker is only available on iOS");
127
+ }
128
+ return NativeModule.presentFamilyActivityPicker();
129
+ }
130
+
131
+ export async function setBlockConfiguration(config: IOSBlockConfiguration): Promise<void> {
132
+ if (Platform.OS !== "ios") {
133
+ throw new Error("Block configuration is only available on iOS");
134
+ }
135
+ return NativeModule.setBlockConfiguration(config);
136
+ }
137
+
138
+ export function getBlockConfiguration(): IOSBlockConfiguration | null {
139
+ if (Platform.OS !== "ios") return null;
140
+ return NativeModule.getBlockConfiguration();
141
+ }
142
+
143
+ export function clearAllBlocks(): void {
144
+ if (Platform.OS !== "ios") return;
145
+ NativeModule.clearAllBlocks();
146
+ }
147
+
148
+ export function isAppBlocked(bundleIdentifier: string): boolean {
149
+ if (Platform.OS !== "ios") return false;
150
+ return NativeModule.isAppBlocked(bundleIdentifier);
151
+ }
152
+
153
+ // ──────────────────────────────────────────────────────────────────────────────
154
+ // iOS-specific: Temporary unlock
155
+ // ──────────────────────────────────────────────────────────────────────────────
156
+
157
+ export async function temporaryUnlock(durationMinutes: number = 15): Promise<TemporaryUnlockResult> {
158
+ if (Platform.OS !== "ios") {
159
+ throw new Error("Temporary unlock is only available on iOS");
160
+ }
161
+ return NativeModule.temporaryUnlock(durationMinutes);
162
+ }
163
+
164
+ export function isTemporarilyUnlocked(): boolean {
165
+ if (Platform.OS !== "ios") return false;
166
+ return NativeModule.isTemporarilyUnlocked();
167
+ }
168
+
169
+ export function getRemainingUnlockTime(): number {
170
+ if (Platform.OS !== "ios") return 0;
171
+ return NativeModule.getRemainingUnlockTime();
172
+ }
173
+
174
+ export async function relockApps(): Promise<RelockResult> {
175
+ if (Platform.OS !== "ios") {
176
+ throw new Error("Relock is only available on iOS");
177
+ }
178
+ return NativeModule.relockApps();
179
+ }
180
+
181
+ export function checkAndClearPendingUnlock(): boolean {
182
+ if (Platform.OS !== "ios") return false;
183
+ return NativeModule.checkAndClearPendingUnlock();
184
+ }
185
+
186
+ export function addPendingUnlockListener(
187
+ handler: () => void
188
+ ): { remove: () => void } | null {
189
+ if (Platform.OS !== "ios") return null;
190
+ const emitter = new EventEmitter(NativeModule);
191
+ return (emitter as any).addListener("onPendingUnlockRequest", handler);
192
+ }
193
+
194
+ // ──────────────────────────────────────────────────────────────────────────────
195
+ // iOS Native View: renders blocked app tokens with real names and icons
196
+ // ──────────────────────────────────────────────────────────────────────────────
197
+
198
+ let NativeBlockedAppsView: any = null;
199
+ if (Platform.OS === "ios") {
200
+ try {
201
+ NativeBlockedAppsView = requireNativeViewManager("ExpoAppBlocker");
202
+ } catch {}
203
+ }
204
+
205
+ export function BlockedAppsNativeList({
206
+ items,
207
+ selectionData,
208
+ style,
209
+ }: {
210
+ items: IOSBlockedItem[];
211
+ selectionData?: string;
212
+ style?: any;
213
+ }) {
214
+ if (!NativeBlockedAppsView || Platform.OS !== "ios") return null;
215
+
216
+ const tokens = items
217
+ .filter((item) => (item.type as string) !== "summary")
218
+ .map((item) => ({ token: item.token, type: item.type }));
219
+
220
+ return React.createElement(NativeBlockedAppsView, {
221
+ selectionData: selectionData || "",
222
+ tokens,
223
+ style: [{ minHeight: 50 }, style],
224
+ });
225
+ }
@@ -0,0 +1,150 @@
1
+ import DeviceActivity
2
+ import ManagedSettings
3
+ import FamilyControls
4
+ import Foundation
5
+
6
+ @available(iOS 15.0, *)
7
+ class AppBlockerDeviceActivityMonitor: DeviceActivityMonitor {
8
+ // CONFIGURE: Replace with your App Group identifier
9
+ private let appGroupIdentifier = "APP_GROUP_PLACEHOLDER"
10
+ private let temporaryUnlockKey = "appBlocker.temporaryUnlock.v1"
11
+ private let blockConfigStorageKey = "appBlocker.blockConfiguration.v1"
12
+
13
+ private let store = ManagedSettingsStore()
14
+ private var sharedDefaults: UserDefaults?
15
+
16
+ override init() {
17
+ super.init()
18
+ sharedDefaults = UserDefaults(suiteName: appGroupIdentifier)
19
+ }
20
+
21
+ override func intervalDidEnd(for activity: DeviceActivityName) {
22
+ super.intervalDidEnd(for: activity)
23
+ sharedDefaults?.removeObject(forKey: temporaryUnlockKey)
24
+ reapplyBlockConfiguration()
25
+ }
26
+
27
+ override func intervalDidStart(for activity: DeviceActivityName) {
28
+ super.intervalDidStart(for: activity)
29
+ }
30
+
31
+ private func reapplyBlockConfiguration() {
32
+ let userDefaults = sharedDefaults ?? UserDefaults.standard
33
+
34
+ guard let configDict = userDefaults.dictionary(forKey: blockConfigStorageKey) else {
35
+ store.shield.applications = nil
36
+ store.shield.applicationCategories = nil
37
+ return
38
+ }
39
+
40
+ guard let blockConfig = parseBlockConfig(configDict) else {
41
+ return
42
+ }
43
+
44
+ applyBlocks(blockConfig)
45
+ }
46
+
47
+ private func parseBlockConfig(_ dict: [String: Any]) -> MonitorBlockConfig? {
48
+ let rawItems: [[String: Any]]
49
+ if let blockedItems = dict["blockedItems"] as? [[String: Any]] {
50
+ rawItems = blockedItems
51
+ } else if let appSelections = dict["appSelections"] as? [[String: Any]] {
52
+ rawItems = appSelections.map { item in
53
+ var normalized = item
54
+ normalized["type"] = "app"
55
+ return normalized
56
+ }
57
+ } else {
58
+ return nil
59
+ }
60
+
61
+ let items: [MonitorBlockedItemInfo] = rawItems.compactMap { selection -> MonitorBlockedItemInfo? in
62
+ guard let tokenString = selection["token"] as? String else {
63
+ return nil
64
+ }
65
+
66
+ let itemTypeRaw = (selection["type"] as? String ?? "app").lowercased()
67
+ let itemType: MonitorBlockedItemType = itemTypeRaw == "category" ? .category : .app
68
+
69
+ return MonitorBlockedItemInfo(
70
+ type: itemType,
71
+ tokenId: tokenString,
72
+ appToken: itemType == .app ? decodeApplicationToken(from: tokenString) : nil,
73
+ categoryToken: itemType == .category ? decodeCategoryToken(from: tokenString) : nil
74
+ )
75
+ }
76
+
77
+ let isActive = dict["isActive"] as? Bool ?? true
78
+ return MonitorBlockConfig(items: items, isActive: isActive)
79
+ }
80
+
81
+ private func applyBlocks(_ config: MonitorBlockConfig) {
82
+ guard config.isActive else {
83
+ store.shield.applications = nil
84
+ store.shield.applicationCategories = nil
85
+ return
86
+ }
87
+
88
+ let validAppTokens = config.items.compactMap { $0.appToken }
89
+ let validCategoryTokens = config.items.compactMap { $0.categoryToken }
90
+
91
+ guard !validAppTokens.isEmpty || !validCategoryTokens.isEmpty else {
92
+ store.shield.applications = nil
93
+ store.shield.applicationCategories = nil
94
+ return
95
+ }
96
+
97
+ if !validAppTokens.isEmpty {
98
+ store.shield.applications = Set(validAppTokens)
99
+ } else {
100
+ store.shield.applications = nil
101
+ }
102
+
103
+ if !validCategoryTokens.isEmpty {
104
+ store.shield.applicationCategories = .specific(Set(validCategoryTokens))
105
+ } else {
106
+ store.shield.applicationCategories = nil
107
+ }
108
+ }
109
+
110
+ private func decodeApplicationToken(from encoded: String) -> ApplicationToken? {
111
+ guard let data = Data(base64Encoded: encoded) else {
112
+ return nil
113
+ }
114
+
115
+ do {
116
+ return try JSONDecoder().decode(ApplicationToken.self, from: data)
117
+ } catch {
118
+ return nil
119
+ }
120
+ }
121
+
122
+ private func decodeCategoryToken(from encoded: String) -> ActivityCategoryToken? {
123
+ guard let data = Data(base64Encoded: encoded) else {
124
+ return nil
125
+ }
126
+
127
+ do {
128
+ return try JSONDecoder().decode(ActivityCategoryToken.self, from: data)
129
+ } catch {
130
+ return nil
131
+ }
132
+ }
133
+ }
134
+
135
+ enum MonitorBlockedItemType: String {
136
+ case app
137
+ case category
138
+ }
139
+
140
+ struct MonitorBlockedItemInfo {
141
+ let type: MonitorBlockedItemType
142
+ let tokenId: String
143
+ let appToken: ApplicationToken?
144
+ let categoryToken: ActivityCategoryToken?
145
+ }
146
+
147
+ struct MonitorBlockConfig {
148
+ let items: [MonitorBlockedItemInfo]
149
+ let isActive: Bool
150
+ }
@@ -0,0 +1,17 @@
1
+ /** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
2
+ module.exports = (config) => {
3
+ const appGroup = config.ios?.entitlements?.["com.apple.security.application-groups"]?.[0]
4
+ || "group.expo.app-blocker";
5
+
6
+ return {
7
+ type: "device-activity-monitor",
8
+ name: "DeviceActivityMonitor",
9
+ deploymentTarget: "16.0",
10
+ bundleIdentifier: ".DeviceActivityMonitor",
11
+ frameworks: ["DeviceActivity", "ManagedSettings", "FamilyControls"],
12
+ entitlements: {
13
+ "com.apple.developer.family-controls": true,
14
+ "com.apple.security.application-groups": [appGroup],
15
+ },
16
+ };
17
+ };
@@ -0,0 +1,86 @@
1
+ import ManagedSettings
2
+ import ManagedSettingsUI
3
+ import UIKit
4
+ import UserNotifications
5
+
6
+ class ShieldActionExtension: ShieldActionDelegate {
7
+ private let appGroupIdentifier = "APP_GROUP_PLACEHOLDER"
8
+ private let pendingUnlockKey = "appBlocker.pendingUnlock.v1"
9
+ private let pendingUnlockNotificationIdentifier = "expo.appblocker.pendingUnlock.local"
10
+
11
+ override func handle(action: ShieldAction, for application: ApplicationToken, completionHandler: @escaping (ShieldActionResponse) -> Void) {
12
+ handleAction(action, completionHandler: completionHandler)
13
+ }
14
+
15
+ override func handle(action: ShieldAction, for webDomain: WebDomainToken, completionHandler: @escaping (ShieldActionResponse) -> Void) {
16
+ handleAction(action, completionHandler: completionHandler)
17
+ }
18
+
19
+ override func handle(action: ShieldAction, for category: ActivityCategoryToken, completionHandler: @escaping (ShieldActionResponse) -> Void) {
20
+ handleAction(action, completionHandler: completionHandler)
21
+ }
22
+
23
+ private func handleAction(_ action: ShieldAction, completionHandler: @escaping (ShieldActionResponse) -> Void) {
24
+ switch action {
25
+ case .primaryButtonPressed:
26
+ setPendingUnlockFlag()
27
+ schedulePendingUnlockNotification { didSchedule in
28
+ let response: ShieldActionResponse = didSchedule ? .none : .defer
29
+ self.complete(on: response, completionHandler: completionHandler)
30
+ }
31
+
32
+ case .secondaryButtonPressed:
33
+ complete(on: .close, completionHandler: completionHandler)
34
+
35
+ @unknown default:
36
+ complete(on: .close, completionHandler: completionHandler)
37
+ }
38
+ }
39
+
40
+ private func complete(on response: ShieldActionResponse, completionHandler: @escaping (ShieldActionResponse) -> Void) {
41
+ if Thread.isMainThread {
42
+ completionHandler(response)
43
+ return
44
+ }
45
+
46
+ DispatchQueue.main.async {
47
+ completionHandler(response)
48
+ }
49
+ }
50
+
51
+ private func setPendingUnlockFlag() {
52
+ guard let sharedDefaults = UserDefaults(suiteName: appGroupIdentifier) else { return }
53
+ sharedDefaults.set(true, forKey: pendingUnlockKey)
54
+ sharedDefaults.synchronize()
55
+
56
+ let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
57
+ CFNotificationCenterPostNotification(
58
+ notificationCenter,
59
+ CFNotificationName("expo.appblocker.pendingUnlock" as CFString),
60
+ nil,
61
+ nil,
62
+ true
63
+ )
64
+ }
65
+
66
+ private func schedulePendingUnlockNotification(completion: @escaping (Bool) -> Void) {
67
+ let center = UNUserNotificationCenter.current()
68
+
69
+ let content = UNMutableNotificationContent()
70
+ content.title = "App Blocker"
71
+ content.body = "Tap to return to the app and complete the unlock challenge."
72
+ content.sound = .default
73
+ content.userInfo = ["link": "/unlock"]
74
+
75
+ let request = UNNotificationRequest(
76
+ identifier: pendingUnlockNotificationIdentifier,
77
+ content: content,
78
+ trigger: nil
79
+ )
80
+
81
+ center.removePendingNotificationRequests(withIdentifiers: [pendingUnlockNotificationIdentifier])
82
+ center.add(request) { error in
83
+ completion(error == nil)
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,17 @@
1
+ /** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
2
+ module.exports = (config) => {
3
+ const appGroup = config.ios?.entitlements?.["com.apple.security.application-groups"]?.[0]
4
+ || "group.expo.app-blocker";
5
+
6
+ return {
7
+ type: "shield-action",
8
+ name: "ShieldAction",
9
+ deploymentTarget: "16.0",
10
+ bundleIdentifier: ".ShieldAction",
11
+ frameworks: ["ManagedSettings", "ManagedSettingsUI"],
12
+ entitlements: {
13
+ "com.apple.developer.family-controls": true,
14
+ "com.apple.security.application-groups": [appGroup],
15
+ },
16
+ };
17
+ };
@@ -0,0 +1,152 @@
1
+ import ManagedSettingsUI
2
+ import ManagedSettings
3
+ import UIKit
4
+
5
+ class ShieldConfigurationExtension: ShieldConfigurationDataSource {
6
+
7
+ private let appGroupIdentifier = "APP_GROUP_PLACEHOLDER"
8
+
9
+ // Grandmizer design system colors
10
+ private let primaryGreen = UIColor(red: 0.486, green: 0.710, blue: 0.094, alpha: 1.0) // #7cb518
11
+ private let darkGreen = UIColor(red: 0.361, green: 0.502, blue: 0.004, alpha: 1.0) // #5c8001
12
+ private let accentOrange = UIColor(red: 0.984, green: 0.380, blue: 0.027, alpha: 1.0) // #fb6107
13
+ private let darkText = UIColor(red: 0.067, green: 0.067, blue: 0.067, alpha: 1.0) // #111111
14
+ private let subtitleGray = UIColor(red: 0.45, green: 0.45, blue: 0.45, alpha: 1.0)
15
+
16
+ private var mascotIcon: UIImage? {
17
+ // Load from the extension's own bundle
18
+ let bundle = Bundle(for: type(of: self))
19
+ return UIImage(named: "shield-icon", in: bundle, compatibleWith: nil)
20
+ ?? UIImage(contentsOfFile: bundle.path(forResource: "shield-icon", ofType: "png") ?? "")
21
+ }
22
+
23
+ private func getBlockedAppCount() -> Int {
24
+ guard let defaults = UserDefaults(suiteName: appGroupIdentifier) else { return 0 }
25
+ guard let config = defaults.dictionary(forKey: "appBlocker.blockConfiguration.v1") else { return 0 }
26
+ if let items = config["blockedItems"] as? [[String: Any]] {
27
+ return items.count
28
+ }
29
+ return 0
30
+ }
31
+
32
+ private func isTemporarilyUnlocked() -> Bool {
33
+ guard let defaults = UserDefaults(suiteName: appGroupIdentifier) else { return false }
34
+ guard let expiration = defaults.object(forKey: "appBlocker.temporaryUnlock.v1") as? Date else { return false }
35
+ return Date() < expiration
36
+ }
37
+
38
+ override func configuration(shielding application: Application) -> ShieldConfiguration {
39
+ let appName = application.localizedDisplayName ?? "This app"
40
+
41
+ // If temporarily unlocked, show a different message
42
+ if isTemporarilyUnlocked() {
43
+ return ShieldConfiguration(
44
+ backgroundBlurStyle: .systemThickMaterial,
45
+ backgroundColor: nil,
46
+ icon: mascotIcon,
47
+ title: ShieldConfiguration.Label(
48
+ text: "Almost there!",
49
+ color: darkText
50
+ ),
51
+ subtitle: ShieldConfiguration.Label(
52
+ text: "Your free time is loading. Try again in a moment.",
53
+ color: subtitleGray
54
+ ),
55
+ primaryButtonLabel: ShieldConfiguration.Label(
56
+ text: "OK",
57
+ color: .white
58
+ ),
59
+ primaryButtonBackgroundColor: primaryGreen,
60
+ secondaryButtonLabel: nil
61
+ )
62
+ }
63
+
64
+ let count = getBlockedAppCount()
65
+ let contextLine = count > 1
66
+ ? "You have \(count) apps blocked. Stay focused!"
67
+ : "Stay focused! Take a quick quiz to earn free time."
68
+
69
+ return ShieldConfiguration(
70
+ backgroundBlurStyle: .systemThickMaterial,
71
+ backgroundColor: nil,
72
+ icon: mascotIcon,
73
+ title: ShieldConfiguration.Label(
74
+ text: "Hold on!",
75
+ color: darkText
76
+ ),
77
+ subtitle: ShieldConfiguration.Label(
78
+ text: "\(appName) is blocked. \(contextLine)",
79
+ color: subtitleGray
80
+ ),
81
+ primaryButtonLabel: ShieldConfiguration.Label(
82
+ text: "Earn Free Time",
83
+ color: .white
84
+ ),
85
+ primaryButtonBackgroundColor: accentOrange,
86
+ secondaryButtonLabel: ShieldConfiguration.Label(
87
+ text: "Not now",
88
+ color: subtitleGray
89
+ )
90
+ )
91
+ }
92
+
93
+ override func configuration(shielding application: Application,
94
+ in category: ActivityCategory) -> ShieldConfiguration {
95
+ let categoryName = category.localizedDisplayName ?? "This category"
96
+
97
+ return ShieldConfiguration(
98
+ backgroundBlurStyle: .systemThickMaterial,
99
+ backgroundColor: nil,
100
+ icon: mascotIcon,
101
+ title: ShieldConfiguration.Label(
102
+ text: "Hold on!",
103
+ color: darkText
104
+ ),
105
+ subtitle: ShieldConfiguration.Label(
106
+ text: "\(categoryName) is blocked. Take a quick quiz to earn free time!",
107
+ color: subtitleGray
108
+ ),
109
+ primaryButtonLabel: ShieldConfiguration.Label(
110
+ text: "Earn Free Time",
111
+ color: .white
112
+ ),
113
+ primaryButtonBackgroundColor: accentOrange,
114
+ secondaryButtonLabel: ShieldConfiguration.Label(
115
+ text: "Not now",
116
+ color: subtitleGray
117
+ )
118
+ )
119
+ }
120
+
121
+ override func configuration(shielding webDomain: WebDomain) -> ShieldConfiguration {
122
+ let domain = webDomain.domain ?? "This website"
123
+
124
+ return ShieldConfiguration(
125
+ backgroundBlurStyle: .systemThickMaterial,
126
+ backgroundColor: nil,
127
+ icon: mascotIcon,
128
+ title: ShieldConfiguration.Label(
129
+ text: "Hold on!",
130
+ color: darkText
131
+ ),
132
+ subtitle: ShieldConfiguration.Label(
133
+ text: "\(domain) is blocked. Take a quick quiz to earn free time!",
134
+ color: subtitleGray
135
+ ),
136
+ primaryButtonLabel: ShieldConfiguration.Label(
137
+ text: "Earn Free Time",
138
+ color: .white
139
+ ),
140
+ primaryButtonBackgroundColor: accentOrange,
141
+ secondaryButtonLabel: ShieldConfiguration.Label(
142
+ text: "Not now",
143
+ color: subtitleGray
144
+ )
145
+ )
146
+ }
147
+
148
+ override func configuration(shielding webDomain: WebDomain,
149
+ in category: ActivityCategory) -> ShieldConfiguration {
150
+ return configuration(shielding: webDomain)
151
+ }
152
+ }
@@ -0,0 +1,17 @@
1
+ /** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
2
+ module.exports = (config) => {
3
+ const appGroup = config.ios?.entitlements?.["com.apple.security.application-groups"]?.[0]
4
+ || "group.expo.app-blocker";
5
+
6
+ return {
7
+ type: "shield-config",
8
+ name: "ShieldConfiguration",
9
+ deploymentTarget: "16.0",
10
+ bundleIdentifier: ".ShieldConfiguration",
11
+ frameworks: ["ManagedSettings", "ManagedSettingsUI"],
12
+ entitlements: {
13
+ "com.apple.developer.family-controls": true,
14
+ "com.apple.security.application-groups": [appGroup],
15
+ },
16
+ };
17
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "commonjs",
5
+ "lib": ["esnext"],
6
+ "jsx": "react-native",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "declaration": true,
11
+ "outDir": "./build",
12
+ "rootDir": "."
13
+ },
14
+ "include": ["src/**/*", "plugin/**/*"],
15
+ "exclude": ["node_modules", "build"]
16
+ }