expo-app-blocker 0.1.68 → 0.1.69

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-app-blocker",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
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",
@@ -6,6 +6,11 @@ import UserNotifications
6
6
  class ShieldActionExtension: ShieldActionDelegate {
7
7
  private let appGroupIdentifier = "APP_GROUP_PLACEHOLDER"
8
8
  private let pendingUnlockKey = "appBlocker.pendingUnlock.v1"
9
+ private let pendingInterceptsKey = "appBlocker.pendingIntercepts.v1"
10
+ private let lastInterceptTsKey = "appBlocker.lastInterceptTs.v1"
11
+ private let shieldInvokeCountKey = "appBlocker.shieldInvokeCount.v1"
12
+ private let interceptDebounceMs: Double = 2_000
13
+ private let maxPendingIntercepts = 200
9
14
  private let pendingUnlockNotificationIdentifier = "expo.appblocker.pendingUnlock.local"
10
15
  // Notification copy + behavior — configurable via plugin options so apps
11
16
  // can localize without forking. Defaults preserve the original English
@@ -27,6 +32,11 @@ class ShieldActionExtension: ShieldActionDelegate {
27
32
  }
28
33
 
29
34
  private func handleAction(_ action: ShieldAction, completionHandler: @escaping (ShieldActionResponse) -> Void) {
35
+ // Any interaction with the shield is a confirmed block event. The
36
+ // ShieldConfiguration data source is cached by the system and not
37
+ // re-invoked per open, so this — the action handler, which fires every
38
+ // time — is the reliable place to record the block.
39
+ recordIntercept()
30
40
  switch action {
31
41
  case .primaryButtonPressed:
32
42
  setPendingUnlockFlag()
@@ -54,6 +64,41 @@ class ShieldActionExtension: ShieldActionDelegate {
54
64
  }
55
65
  }
56
66
 
67
+ /// Queue a block event (JSON-string queue in the App Group), debounced,
68
+ /// for the app to drain into `blocker_intercepts`.
69
+ private func recordIntercept() {
70
+ guard let defaults = UserDefaults(suiteName: appGroupIdentifier) else { return }
71
+ defaults.synchronize()
72
+
73
+ let invokes = defaults.integer(forKey: shieldInvokeCountKey) + 1
74
+ defaults.set(invokes, forKey: shieldInvokeCountKey)
75
+
76
+ let nowMs = Date().timeIntervalSince1970 * 1000.0
77
+ let lastMs = defaults.double(forKey: lastInterceptTsKey)
78
+ if lastMs > 0, (nowMs - lastMs) < interceptDebounceMs {
79
+ defaults.synchronize()
80
+ return
81
+ }
82
+
83
+ var queue: [[String: Any]] = []
84
+ if let json = defaults.string(forKey: pendingInterceptsKey),
85
+ let data = json.data(using: .utf8),
86
+ let parsed = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
87
+ queue = parsed
88
+ }
89
+ queue.append(["appName": NSNull(), "interceptedAt": nowMs])
90
+ if queue.count > maxPendingIntercepts {
91
+ queue = Array(queue.suffix(maxPendingIntercepts))
92
+ }
93
+ if let data = try? JSONSerialization.data(withJSONObject: queue),
94
+ let json = String(data: data, encoding: .utf8) {
95
+ defaults.set(json, forKey: pendingInterceptsKey)
96
+ }
97
+ defaults.set(nowMs, forKey: lastInterceptTsKey)
98
+ defaults.synchronize()
99
+ NSLog("[appblocker] ShieldAction recordIntercept queued depth=\(queue.count) invokes=\(invokes)")
100
+ }
101
+
57
102
  private func setPendingUnlockFlag() {
58
103
  guard let sharedDefaults = UserDefaults(suiteName: appGroupIdentifier) else { return }
59
104
  sharedDefaults.set(true, forKey: pendingUnlockKey)