expo-app-blocker 0.1.67 → 0.1.68
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.
|
@@ -39,6 +39,7 @@ public class ExpoAppBlockerModule: Module {
|
|
|
39
39
|
private let maxUsageSteps = 60
|
|
40
40
|
private let pendingUnlockKey = "appBlocker.pendingUnlock.v1"
|
|
41
41
|
private let pendingInterceptsKey = "appBlocker.pendingIntercepts.v1"
|
|
42
|
+
private let shieldInvokeCountKey = "appBlocker.shieldInvokeCount.v1"
|
|
42
43
|
private let minimumTemporaryUnlockMinutes = 1
|
|
43
44
|
private var didLoadPersistedConfig = false
|
|
44
45
|
|
|
@@ -206,15 +207,40 @@ public class ExpoAppBlockerModule: Module {
|
|
|
206
207
|
// Refresh the cached suite so writes made by the (separate) shield
|
|
207
208
|
// extension process are visible to this long-lived app process.
|
|
208
209
|
defaults.synchronize()
|
|
209
|
-
|
|
210
|
-
if
|
|
210
|
+
var queue: [[String: Any]] = []
|
|
211
|
+
if let json = defaults.string(forKey: self.pendingInterceptsKey),
|
|
212
|
+
let data = json.data(using: .utf8),
|
|
213
|
+
let parsed = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
|
|
214
|
+
queue = parsed
|
|
215
|
+
}
|
|
216
|
+
if defaults.string(forKey: self.pendingInterceptsKey) != nil {
|
|
211
217
|
defaults.removeObject(forKey: self.pendingInterceptsKey)
|
|
212
218
|
defaults.synchronize()
|
|
213
219
|
}
|
|
214
|
-
|
|
220
|
+
let invokes = defaults.integer(forKey: self.shieldInvokeCountKey)
|
|
221
|
+
NSLog("[appblocker] drainPendingIntercepts: returning \(queue.count) shieldInvokes=\(invokes)")
|
|
215
222
|
return queue
|
|
216
223
|
}
|
|
217
224
|
|
|
225
|
+
// TEMP diagnostic: surfaces whether the shield extension is being
|
|
226
|
+
// invoked at all (shieldInvokeCount) and the current queue depth,
|
|
227
|
+
// without clearing anything.
|
|
228
|
+
Function("debugInterceptState") { () -> [String: Any] in
|
|
229
|
+
guard let defaults = self.sharedDefaults else { return ["available": false] }
|
|
230
|
+
defaults.synchronize()
|
|
231
|
+
var depth = 0
|
|
232
|
+
if let json = defaults.string(forKey: self.pendingInterceptsKey),
|
|
233
|
+
let data = json.data(using: .utf8),
|
|
234
|
+
let parsed = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
|
|
235
|
+
depth = parsed.count
|
|
236
|
+
}
|
|
237
|
+
return [
|
|
238
|
+
"available": true,
|
|
239
|
+
"shieldInvokeCount": defaults.integer(forKey: self.shieldInvokeCountKey),
|
|
240
|
+
"queueDepth": depth,
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
|
|
218
244
|
Function("isAppBlocked") { (bundleIdentifier: String) -> Bool in
|
|
219
245
|
self.ensureLoadedPersistedConfig()
|
|
220
246
|
guard let config = self.currentBlockConfig else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-blocker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.68",
|
|
4
4
|
"description": "Expo module for cross-platform app blocking. Android: UsageStatsManager + Overlay. iOS: Screen Time API (FamilyControls + ManagedSettings + DeviceActivity).",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -244,6 +244,16 @@ export function drainPendingIntercepts(): PendingIntercept[] {
|
|
|
244
244
|
return NativeModule.drainPendingIntercepts() ?? [];
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* TEMP diagnostic: returns `{ shieldInvokeCount, queueDepth }` (iOS) without
|
|
249
|
+
* clearing the queue. `shieldInvokeCount` proves whether the shield
|
|
250
|
+
* extension is being invoked by the OS at all.
|
|
251
|
+
*/
|
|
252
|
+
export function debugInterceptState(): Record<string, unknown> {
|
|
253
|
+
if (Platform.OS !== "ios" && Platform.OS !== "android") return {};
|
|
254
|
+
return (NativeModule.debugInterceptState?.() as Record<string, unknown>) ?? {};
|
|
255
|
+
}
|
|
256
|
+
|
|
247
257
|
export function addPendingUnlockListener(
|
|
248
258
|
handler: () => void
|
|
249
259
|
): { remove: () => void } | null {
|
|
@@ -49,6 +49,7 @@ class ShieldConfigurationExtension: ShieldConfigurationDataSource {
|
|
|
49
49
|
// those bursts into one logical block event.
|
|
50
50
|
private let pendingInterceptsKey = "appBlocker.pendingIntercepts.v1"
|
|
51
51
|
private let lastInterceptTsKey = "appBlocker.lastInterceptTs.v1"
|
|
52
|
+
private let shieldInvokeCountKey = "appBlocker.shieldInvokeCount.v1"
|
|
52
53
|
private let interceptDebounceMs: Double = 2_000
|
|
53
54
|
private let maxPendingIntercepts = 200
|
|
54
55
|
|
|
@@ -60,24 +61,42 @@ class ShieldConfigurationExtension: ShieldConfigurationDataSource {
|
|
|
60
61
|
// The extension is a short-lived process; pull a fresh view before
|
|
61
62
|
// read-modify-write so we don't clobber the app's drain.
|
|
62
63
|
defaults.synchronize()
|
|
64
|
+
|
|
65
|
+
// Diagnostic: count every shield render, before any debounce/branch,
|
|
66
|
+
// so the app can confirm the extension is actually being invoked.
|
|
67
|
+
let invokes = defaults.integer(forKey: shieldInvokeCountKey) + 1
|
|
68
|
+
defaults.set(invokes, forKey: shieldInvokeCountKey)
|
|
69
|
+
|
|
63
70
|
let nowMs = Date().timeIntervalSince1970 * 1000.0
|
|
64
71
|
let lastMs = defaults.double(forKey: lastInterceptTsKey)
|
|
65
72
|
if lastMs > 0, (nowMs - lastMs) < interceptDebounceMs {
|
|
66
|
-
|
|
73
|
+
defaults.synchronize()
|
|
74
|
+
NSLog("[appblocker] recordIntercept: debounced (invokes=\(invokes))")
|
|
67
75
|
return
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
|
|
78
|
+
// Persist as a JSON string — the most robust representation to carry a
|
|
79
|
+
// nested array across the extension→app process boundary via the App
|
|
80
|
+
// Group (a raw [[String: Any]] does not reliably survive).
|
|
81
|
+
var queue: [[String: Any]] = []
|
|
82
|
+
if let json = defaults.string(forKey: pendingInterceptsKey),
|
|
83
|
+
let data = json.data(using: .utf8),
|
|
84
|
+
let parsed = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
|
|
85
|
+
queue = parsed
|
|
86
|
+
}
|
|
71
87
|
queue.append(["appName": appName, "interceptedAt": nowMs])
|
|
72
88
|
if queue.count > maxPendingIntercepts {
|
|
73
89
|
queue = Array(queue.suffix(maxPendingIntercepts))
|
|
74
90
|
}
|
|
75
|
-
|
|
91
|
+
if let data = try? JSONSerialization.data(withJSONObject: queue),
|
|
92
|
+
let json = String(data: data, encoding: .utf8) {
|
|
93
|
+
defaults.set(json, forKey: pendingInterceptsKey)
|
|
94
|
+
}
|
|
76
95
|
defaults.set(nowMs, forKey: lastInterceptTsKey)
|
|
77
96
|
// Force a flush to cfprefsd — the OS may suspend/kill the extension
|
|
78
97
|
// immediately after rendering, before an unflushed write persists.
|
|
79
98
|
defaults.synchronize()
|
|
80
|
-
NSLog("[appblocker] recordIntercept: queued \(appName), depth=\(queue.count)")
|
|
99
|
+
NSLog("[appblocker] recordIntercept: queued \(appName), depth=\(queue.count) invokes=\(invokes)")
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
private func makeConfig(appName: String) -> ShieldConfiguration {
|