-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathindex.ts
More file actions
351 lines (307 loc) · 14.4 KB
/
Copy pathindex.ts
File metadata and controls
351 lines (307 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
import {
requireNativeModule,
requireNativeViewManager,
EventEmitter,
} from "expo-modules-core";
import { Platform } from "react-native";
import React from "react";
import type {
PermissionStatus,
AndroidPermissions,
IOSPermissions,
AndroidBlockableApp,
AndroidConfig,
IOSBlockedItem,
IOSBlockConfiguration,
TemporaryUnlockResult,
RelockResult,
FamilyActivityPickerSelectionEvent,
FamilyActivityPickerViewProps,
BlockedAppsNativeListProps,
} from "./ExpoAppBlocker.types";
export type {
PermissionStatus,
AndroidPermissions,
IOSPermissions,
AndroidBlockableApp,
IOSBlockedItem,
IOSBlockConfiguration,
TemporaryUnlockResult,
RelockResult,
ShieldConfig,
AndroidConfig,
PluginConfig,
FamilyActivityPickerSelectionEvent,
FamilyActivityPickerViewProps,
BlockedAppsNativeListProps,
} from "./ExpoAppBlocker.types";
// ──────────────────────────────────────────────────────────────────────────────
// Native module bridge
// ──────────────────────────────────────────────────────────────────────────────
const NativeModule = requireNativeModule("ExpoAppBlocker");
// ──────────────────────────────────────────────────────────────────────────────
// Permissions
// ──────────────────────────────────────────────────────────────────────────────
export async function getPermissionStatus(): Promise<PermissionStatus> {
if (Platform.OS === "android") {
const overlay = await NativeModule.checkOverlayPermission();
const usageStats = await NativeModule.checkUsageStatsPermission();
const notifications = await NativeModule.checkNotificationPermission();
const details: AndroidPermissions = { platform: "android", overlay, usageStats, notifications };
return { allGranted: overlay && usageStats && notifications, details };
}
if (Platform.OS === "ios") {
const result = NativeModule.getAuthorizationStatus();
const details: IOSPermissions = {
platform: "ios",
authorized: result.authorized,
status: result.status,
};
return { allGranted: result.authorized, details };
}
throw new Error("Unsupported platform");
}
export async function requestPermissions(): Promise<PermissionStatus> {
if (Platform.OS === "ios") {
const result = await NativeModule.requestAuthorization();
const details: IOSPermissions = {
platform: "ios",
authorized: result.authorized,
status: result.status,
};
return { allGranted: result.authorized, details };
}
return getPermissionStatus();
}
// ──────────────────────────────────────────────────────────────────────────────
// Android-specific: permission settings
// ──────────────────────────────────────────────────────────────────────────────
export function openOverlaySettings(): void {
if (Platform.OS !== "android") return;
NativeModule.openOverlaySettings();
}
export function openUsageStatsSettings(): void {
if (Platform.OS !== "android") return;
NativeModule.openUsageStatsSettings();
}
// ──────────────────────────────────────────────────────────────────────────────
// Android-specific: app list and blocking
// ──────────────────────────────────────────────────────────────────────────────
export async function getInstalledApps(): Promise<AndroidBlockableApp[]> {
if (Platform.OS !== "android") return [];
return NativeModule.getInstalledApps();
}
export function setBlockedApps(packageNames: string[]): void {
if (Platform.OS !== "android") return;
NativeModule.setBlockedApps(packageNames);
}
export function getBlockedApps(): string[] {
if (Platform.OS !== "android") return [];
return NativeModule.getBlockedApps();
}
export function configureAndroid(config: AndroidConfig): void {
if (Platform.OS !== "android") return;
NativeModule.setAndroidConfig(config);
}
export function startMonitoring(): void {
if (Platform.OS !== "android") return;
NativeModule.startMonitoring();
}
export function stopMonitoring(): void {
if (Platform.OS !== "android") return;
NativeModule.stopMonitoring();
}
// ──────────────────────────────────────────────────────────────────────────────
// iOS-specific: Family Controls
// ──────────────────────────────────────────────────────────────────────────────
export async function presentFamilyActivityPicker(): Promise<IOSBlockedItem[]> {
if (Platform.OS !== "ios") {
throw new Error("Family Activity Picker is only available on iOS");
}
return NativeModule.presentFamilyActivityPicker();
}
export async function setBlockConfiguration(config: IOSBlockConfiguration): Promise<void> {
if (Platform.OS !== "ios") {
throw new Error("Block configuration is only available on iOS");
}
return NativeModule.setBlockConfiguration(config);
}
export function getBlockConfiguration(): IOSBlockConfiguration | null {
if (Platform.OS !== "ios") return null;
return NativeModule.getBlockConfiguration();
}
export function clearAllBlocks(): void {
if (Platform.OS !== "ios") return;
NativeModule.clearAllBlocks();
}
export function isAppBlocked(bundleIdentifier: string): boolean {
if (Platform.OS !== "ios") return false;
return NativeModule.isAppBlocked(bundleIdentifier);
}
// ──────────────────────────────────────────────────────────────────────────────
// iOS-specific: Temporary unlock
// ──────────────────────────────────────────────────────────────────────────────
/**
* Suppress blocking for `durationMinutes`, then auto-resume.
*
* iOS removes the Family Controls shields; Android pauses the foreground-service
* poll (the timer lives in the service, so it survives app backgrounding).
* Calling again replaces any active unlock. Android rounds to a whole minute (min 1).
*/
export async function temporaryUnlock(durationMinutes: number = 15): Promise<TemporaryUnlockResult> {
if (Platform.OS === "android") {
NativeModule.temporaryUnlockAndroid(Math.max(1, Math.round(durationMinutes)));
return { unlocked: true, expiresAt: Date.now() + durationMinutes * 60_000 };
}
return NativeModule.temporaryUnlock(durationMinutes);
}
/** iOS only — returns `false` on Android. On Android use `getRemainingUnlockTime() > 0`. */
export function isTemporarilyUnlocked(): boolean {
if (Platform.OS !== "ios") return false;
return NativeModule.isTemporarilyUnlocked();
}
/**
* Seconds remaining on the active temporary unlock, or 0 if none.
*
* Platform divergence: on **Android** this ticks down live as the budget is spent
* inside blocked apps (and freezes when you leave). On **iOS** Apple does not expose
* live cumulative usage, so this returns the *granted* budget and stays flat until
* the usage threshold re-applies the shield (then drops to 0). Don't rely on a
* smooth iOS countdown.
*/
export function getRemainingUnlockTime(): number {
if (Platform.OS === "android") return NativeModule.getRemainingUnlockTimeAndroid();
return NativeModule.getRemainingUnlockTime();
}
/**
* End an active temporary unlock immediately and re-block.
*
* iOS restores the shields; Android cancels the unlock and re-blocks the
* foreground app on the next poll. Safe to call when nothing is unlocked.
*/
export async function relockApps(): Promise<RelockResult> {
if (Platform.OS === "android") {
NativeModule.relockAndroid();
return { locked: true };
}
return NativeModule.relockApps();
}
export function checkAndClearPendingUnlock(): boolean {
if (Platform.OS !== "ios") return false;
return NativeModule.checkAndClearPendingUnlock();
}
/**
* Android-only, last-resort recovery: forces a genuine process kill and
* relaunch. Some native-layer failures (observed: an expo-sqlite connection
* that NPEs on every operation, even a fresh `openDatabaseAsync` against a
* freshly-rebuilt database file) can only be cleared by a real process
* restart — closing/reopening the JS-side handle doesn't reach far enough.
* Apps that run this module's `AppBlockerService` as a foreground service
* can end up with an unusually long-lived Android process (the OS won't
* kill it just because the Activity was "closed"), which surfaces this kind
* of native-module degradation far more than it would in a normal app.
*
* Queues a relaunch (optionally straight back into `deepLink`, e.g. the
* blocker intercept URL the caller was trying to reach), then calls
* `Process.killProcess`. If that succeeds the process is gone before the
* call would otherwise return. The native call itself can still reject
* (e.g. a missing Android permission) — this is already the last-resort
* path, so that failure is swallowed rather than left as an unhandled
* rejection.
*
* No-op on iOS: that platform's process model doesn't exhibit this failure
* mode, and there is no equivalent restart primitive.
*/
export function restartAppForRecovery(deepLink?: string): void {
if (Platform.OS !== "android") return;
NativeModule.restartApp(deepLink ?? null).catch((err: unknown) => {
console.warn("[expo-app-blocker] restartAppForRecovery failed", err);
});
}
/**
* One OS-level block event: the blocker intercepted a blocked app (iOS
* shield render / Android foreground block). `interceptedAt` is epoch
* milliseconds; `appName` is the localized app name when the platform
* can resolve it (null otherwise).
*/
export interface PendingIntercept {
appName: string | null;
interceptedAt: number;
}
/**
* Drain and clear the queue of block events recorded natively since the
* last call. Implemented on both platforms (iOS App Group queue / Android
* SharedPreferences queue). The app calls this on foreground and persists
* the results to power the "blocks" counter.
*/
export function drainPendingIntercepts(): PendingIntercept[] {
if (Platform.OS !== "ios" && Platform.OS !== "android") return [];
return NativeModule.drainPendingIntercepts() ?? [];
}
export function addPendingUnlockListener(
handler: () => void
): { remove: () => void } | null {
if (Platform.OS !== "ios") return null;
const emitter = new EventEmitter(NativeModule);
return (emitter as any).addListener("onPendingUnlockRequest", handler);
}
// ──────────────────────────────────────────────────────────────────────────────
// iOS Native View: renders blocked app tokens with real names and icons
// ──────────────────────────────────────────────────────────────────────────────
let NativeBlockedAppsView: any = null;
if (Platform.OS === "ios") {
try {
NativeBlockedAppsView = requireNativeViewManager("ExpoAppBlocker");
} catch {}
}
export function BlockedAppsNativeList({
items,
selectionData,
style,
}: BlockedAppsNativeListProps) {
if (!NativeBlockedAppsView || Platform.OS !== "ios") return null;
const tokens = items
.filter((item) => (item.type as string) !== "summary")
.map((item) => ({ token: item.token, type: item.type }));
return React.createElement(NativeBlockedAppsView, {
selectionData: selectionData || "",
tokens,
style: [{ minHeight: 50 }, style],
});
}
// ──────────────────────────────────────────────────────────────────────────────
// iOS Native View: inline FamilyActivityPicker (embedded in your UI)
// ──────────────────────────────────────────────────────────────────────────────
let NativePickerView: any = null;
if (Platform.OS === "ios") {
try {
NativePickerView = requireNativeViewManager("ExpoAppBlockerPicker");
} catch {}
}
export function FamilyActivityPickerView({
initialSelection,
onSelectionChange,
theme,
style,
clearTrigger,
}: FamilyActivityPickerViewProps) {
if (!NativePickerView || Platform.OS !== "ios") return null;
return React.createElement(NativePickerView, {
initialSelection: initialSelection || "",
theme: theme || "system",
onSelectionChange: onSelectionChange
? (e: any) => {
const ne = e.nativeEvent;
const items = (ne.items ?? []).filter(
(item: { type?: string }) =>
item?.type === "app" ||
item?.type === "category" ||
item?.type === "webDomain",
);
onSelectionChange({ ...ne, items });
}
: undefined,
...(clearTrigger !== undefined ? { clearTrigger } : {}),
style: [{ minHeight: 400 }, style],
});
}