expo-app-blocker 0.1.69 → 0.1.70

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,7 +39,6 @@ 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"
43
42
  private let minimumTemporaryUnlockMinutes = 1
44
43
  private var didLoadPersistedConfig = false
45
44
 
@@ -217,30 +216,9 @@ public class ExpoAppBlockerModule: Module {
217
216
  defaults.removeObject(forKey: self.pendingInterceptsKey)
218
217
  defaults.synchronize()
219
218
  }
220
- let invokes = defaults.integer(forKey: self.shieldInvokeCountKey)
221
- NSLog("[appblocker] drainPendingIntercepts: returning \(queue.count) shieldInvokes=\(invokes)")
222
219
  return queue
223
220
  }
224
221
 
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
-
244
222
  Function("isAppBlocked") { (bundleIdentifier: String) -> Bool in
245
223
  self.ensureLoadedPersistedConfig()
246
224
  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.69",
3
+ "version": "0.1.70",
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,16 +244,6 @@ 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
-
257
247
  export function addPendingUnlockListener(
258
248
  handler: () => void
259
249
  ): { remove: () => void } | null {
@@ -8,7 +8,6 @@ class ShieldActionExtension: ShieldActionDelegate {
8
8
  private let pendingUnlockKey = "appBlocker.pendingUnlock.v1"
9
9
  private let pendingInterceptsKey = "appBlocker.pendingIntercepts.v1"
10
10
  private let lastInterceptTsKey = "appBlocker.lastInterceptTs.v1"
11
- private let shieldInvokeCountKey = "appBlocker.shieldInvokeCount.v1"
12
11
  private let interceptDebounceMs: Double = 2_000
13
12
  private let maxPendingIntercepts = 200
14
13
  private let pendingUnlockNotificationIdentifier = "expo.appblocker.pendingUnlock.local"
@@ -70,15 +69,9 @@ class ShieldActionExtension: ShieldActionDelegate {
70
69
  guard let defaults = UserDefaults(suiteName: appGroupIdentifier) else { return }
71
70
  defaults.synchronize()
72
71
 
73
- let invokes = defaults.integer(forKey: shieldInvokeCountKey) + 1
74
- defaults.set(invokes, forKey: shieldInvokeCountKey)
75
-
76
72
  let nowMs = Date().timeIntervalSince1970 * 1000.0
77
73
  let lastMs = defaults.double(forKey: lastInterceptTsKey)
78
- if lastMs > 0, (nowMs - lastMs) < interceptDebounceMs {
79
- defaults.synchronize()
80
- return
81
- }
74
+ if lastMs > 0, (nowMs - lastMs) < interceptDebounceMs { return }
82
75
 
83
76
  var queue: [[String: Any]] = []
84
77
  if let json = defaults.string(forKey: pendingInterceptsKey),
@@ -96,7 +89,6 @@ class ShieldActionExtension: ShieldActionDelegate {
96
89
  }
97
90
  defaults.set(nowMs, forKey: lastInterceptTsKey)
98
91
  defaults.synchronize()
99
- NSLog("[appblocker] ShieldAction recordIntercept queued depth=\(queue.count) invokes=\(invokes)")
100
92
  }
101
93
 
102
94
  private func setPendingUnlockFlag() {
@@ -49,35 +49,22 @@ 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"
53
52
  private let interceptDebounceMs: Double = 2_000
54
53
  private let maxPendingIntercepts = 200
55
54
 
55
+ // Best-effort block recording. iOS caches the shield configuration and
56
+ // does NOT reliably re-invoke this data source per open, so the action
57
+ // handler (ShieldAction) is the primary recorder; this is a bonus path
58
+ // for the cases the system does re-invoke. Writes share the same App
59
+ // Group JSON queue + debounce as ShieldAction.
56
60
  private func recordIntercept(appName: String) {
57
- guard let defaults = UserDefaults(suiteName: appGroupIdentifier) else {
58
- NSLog("[appblocker] recordIntercept: no app-group defaults")
59
- return
60
- }
61
- // The extension is a short-lived process; pull a fresh view before
62
- // read-modify-write so we don't clobber the app's drain.
61
+ guard let defaults = UserDefaults(suiteName: appGroupIdentifier) else { return }
63
62
  defaults.synchronize()
64
63
 
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
-
70
64
  let nowMs = Date().timeIntervalSince1970 * 1000.0
71
65
  let lastMs = defaults.double(forKey: lastInterceptTsKey)
72
- if lastMs > 0, (nowMs - lastMs) < interceptDebounceMs {
73
- defaults.synchronize()
74
- NSLog("[appblocker] recordIntercept: debounced (invokes=\(invokes))")
75
- return
76
- }
66
+ if lastMs > 0, (nowMs - lastMs) < interceptDebounceMs { return }
77
67
 
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
68
  var queue: [[String: Any]] = []
82
69
  if let json = defaults.string(forKey: pendingInterceptsKey),
83
70
  let data = json.data(using: .utf8),
@@ -93,10 +80,7 @@ class ShieldConfigurationExtension: ShieldConfigurationDataSource {
93
80
  defaults.set(json, forKey: pendingInterceptsKey)
94
81
  }
95
82
  defaults.set(nowMs, forKey: lastInterceptTsKey)
96
- // Force a flush to cfprefsd — the OS may suspend/kill the extension
97
- // immediately after rendering, before an unflushed write persists.
98
83
  defaults.synchronize()
99
- NSLog("[appblocker] recordIntercept: queued \(appName), depth=\(queue.count) invokes=\(invokes)")
100
84
  }
101
85
 
102
86
  private func makeConfig(appName: String) -> ShieldConfiguration {