expo-app-blocker 0.1.71 → 0.1.73
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.
|
@@ -540,21 +540,42 @@ public class ExpoAppBlockerModule: Module {
|
|
|
540
540
|
let activityName = DeviceActivityName(unlockActivityName)
|
|
541
541
|
let calendar = Calendar.current
|
|
542
542
|
let now = Date()
|
|
543
|
-
let startComponents = calendar.dateComponents([.hour, .minute, .second], from: now)
|
|
544
|
-
let endComponents = calendar.dateComponents([.hour, .minute, .second], from: expirationDate)
|
|
545
|
-
let nowDay = calendar.startOfDay(for: now)
|
|
546
|
-
let expirationDay = calendar.startOfDay(for: expirationDate)
|
|
547
543
|
|
|
544
|
+
// Apple rejects DeviceActivitySchedule intervals shorter than 15 minutes, so
|
|
545
|
+
// a short earned grant can't simply end the interval at its real expiration.
|
|
546
|
+
// Pad the interval to the 15-min minimum and use `warningTime` so
|
|
547
|
+
// `intervalWillEndWarning` fires at the REAL expiration — letting the monitor
|
|
548
|
+
// re-block while the user is still inside the blocked app (sub-15-min grants).
|
|
549
|
+
// Grants ≥ 15 min end the interval exactly at expiration (no warning needed).
|
|
550
|
+
let minIntervalSec: TimeInterval = 15 * 60
|
|
551
|
+
let durationSec = max(0, expirationDate.timeIntervalSince(now))
|
|
552
|
+
|
|
553
|
+
let intervalEndDate: Date
|
|
554
|
+
var warningSec = 0
|
|
555
|
+
if durationSec >= minIntervalSec {
|
|
556
|
+
intervalEndDate = expirationDate
|
|
557
|
+
} else {
|
|
558
|
+
intervalEndDate = now.addingTimeInterval(minIntervalSec)
|
|
559
|
+
warningSec = Int(intervalEndDate.timeIntervalSince(expirationDate).rounded())
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let startComponents = calendar.dateComponents([.hour, .minute, .second], from: now)
|
|
548
563
|
let schedule: DeviceActivitySchedule
|
|
549
|
-
|
|
564
|
+
|
|
565
|
+
if calendar.isDate(now, inSameDayAs: intervalEndDate) {
|
|
566
|
+
let endComponents = calendar.dateComponents([.hour, .minute, .second], from: intervalEndDate)
|
|
567
|
+
let warning: DateComponents? =
|
|
568
|
+
warningSec > 0 ? DateComponents(minute: warningSec / 60, second: warningSec % 60) : nil
|
|
550
569
|
schedule = DeviceActivitySchedule(
|
|
551
570
|
intervalStart: startComponents,
|
|
552
571
|
intervalEnd: endComponents,
|
|
553
|
-
repeats: false
|
|
572
|
+
repeats: false,
|
|
573
|
+
warningTime: warning
|
|
554
574
|
)
|
|
555
575
|
} else {
|
|
556
|
-
//
|
|
557
|
-
// relock
|
|
576
|
+
// Padded interval crosses midnight — cap at end-of-day (no warning); the
|
|
577
|
+
// host-side relock (getRemainingUnlockTime poll / foreground check) covers
|
|
578
|
+
// the remainder on next return to the app.
|
|
558
579
|
schedule = DeviceActivitySchedule(
|
|
559
580
|
intervalStart: startComponents,
|
|
560
581
|
intervalEnd: DateComponents(hour: 23, minute: 59, second: 59),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-blocker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.73",
|
|
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",
|
|
@@ -24,24 +24,38 @@ class DeviceActivityMonitorExtension: DeviceActivityMonitor {
|
|
|
24
24
|
sharedDefaults = UserDefaults(suiteName: appGroupIdentifier)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Fires at `intervalEnd − warningTime`, which the host aligns to the grant's
|
|
28
|
+
// real expiration. This is the ONLY callback that fires for sub-15-min grants
|
|
29
|
+
// (Apple's schedule interval minimum is 15 min), so it's the primary
|
|
30
|
+
// re-block-while-inside path. `intervalDidEnd` covers the ≥15-min case + safety.
|
|
31
|
+
override func intervalWillEndWarning(for activity: DeviceActivityName) {
|
|
32
|
+
super.intervalWillEndWarning(for: activity)
|
|
33
|
+
relockIfExpired()
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
override func intervalDidEnd(for activity: DeviceActivityName) {
|
|
28
37
|
super.intervalDidEnd(for: activity)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
relockIfExpired()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override func intervalDidStart(for activity: DeviceActivityName) {
|
|
42
|
+
super.intervalDidStart(for: activity)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Re-apply the shield once the grant's wall-clock expiration has arrived.
|
|
46
|
+
/// Guards against the spurious callback that `stopMonitoring()` fires during a
|
|
47
|
+
/// re-grant: if the stored expiration is still comfortably in the future
|
|
48
|
+
/// (> 60s), this is a re-arm — not an expiry — so the fresh grant is kept.
|
|
49
|
+
/// The 60s tolerance also absorbs callback/clock skew at the real boundary.
|
|
50
|
+
private func relockIfExpired() {
|
|
33
51
|
if let expiration = sharedDefaults?.object(forKey: temporaryUnlockKey) as? Date,
|
|
34
|
-
|
|
52
|
+
expiration.timeIntervalSinceNow > 60 {
|
|
35
53
|
return
|
|
36
54
|
}
|
|
37
55
|
sharedDefaults?.removeObject(forKey: temporaryUnlockKey)
|
|
38
56
|
reapplyBlockConfiguration()
|
|
39
57
|
}
|
|
40
58
|
|
|
41
|
-
override func intervalDidStart(for activity: DeviceActivityName) {
|
|
42
|
-
super.intervalDidStart(for: activity)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
59
|
private func reapplyBlockConfiguration() {
|
|
46
60
|
let userDefaults = sharedDefaults ?? UserDefaults.standard
|
|
47
61
|
|