expo-app-blocker 0.1.34 → 0.1.36
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.
|
@@ -161,6 +161,7 @@ public class ExpoAppBlockerModule: Module {
|
|
|
161
161
|
self.cancelRelockActivity()
|
|
162
162
|
self.store.shield.applications = nil
|
|
163
163
|
self.store.shield.applicationCategories = nil
|
|
164
|
+
self.store.shield.webDomains = nil
|
|
164
165
|
self.currentBlockConfig = nil
|
|
165
166
|
self.userDefaults.removeObject(forKey: self.blockConfigStorageKey)
|
|
166
167
|
self.sharedDefaults?.removeObject(forKey: self.blockConfigStorageKey)
|
|
@@ -204,6 +205,7 @@ public class ExpoAppBlockerModule: Module {
|
|
|
204
205
|
DispatchQueue.main.async {
|
|
205
206
|
self.store.shield.applications = nil
|
|
206
207
|
self.store.shield.applicationCategories = nil
|
|
208
|
+
self.store.shield.webDomains = nil
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
// Try to schedule relock, but don't fail if schedule is too short
|
|
@@ -353,6 +355,7 @@ public class ExpoAppBlockerModule: Module {
|
|
|
353
355
|
DispatchQueue.main.async {
|
|
354
356
|
self.store.shield.applications = nil
|
|
355
357
|
self.store.shield.applicationCategories = nil
|
|
358
|
+
self.store.shield.webDomains = nil
|
|
356
359
|
}
|
|
357
360
|
|
|
358
361
|
do {
|
|
@@ -393,16 +396,26 @@ public class ExpoAppBlockerModule: Module {
|
|
|
393
396
|
}
|
|
394
397
|
|
|
395
398
|
let itemTypeRaw = (selection["type"] as? String ?? "app").lowercased()
|
|
396
|
-
let itemType: BlockedItemType
|
|
399
|
+
let itemType: BlockedItemType
|
|
400
|
+
switch itemTypeRaw {
|
|
401
|
+
case "category":
|
|
402
|
+
itemType = .category
|
|
403
|
+
case "webdomain":
|
|
404
|
+
itemType = .webDomain
|
|
405
|
+
default:
|
|
406
|
+
itemType = .app
|
|
407
|
+
}
|
|
397
408
|
|
|
398
409
|
return BlockedItemInfo(
|
|
399
410
|
type: itemType,
|
|
400
411
|
tokenId: tokenString,
|
|
401
412
|
appToken: itemType == .app ? self.decodeApplicationToken(from: tokenString) : nil,
|
|
402
413
|
categoryToken: itemType == .category ? self.decodeCategoryToken(from: tokenString) : nil,
|
|
414
|
+
webDomainToken: itemType == .webDomain ? self.decodeWebDomainToken(from: tokenString) : nil,
|
|
403
415
|
bundleIdentifier: selection["bundleIdentifier"] as? String,
|
|
404
416
|
displayName: selection["displayName"] as? String,
|
|
405
417
|
categoryName: selection["categoryName"] as? String,
|
|
418
|
+
domain: selection["domain"] as? String,
|
|
406
419
|
iconBase64: selection["iconBase64"] as? String
|
|
407
420
|
)
|
|
408
421
|
}
|
|
@@ -426,21 +439,25 @@ public class ExpoAppBlockerModule: Module {
|
|
|
426
439
|
guard config.isActive else {
|
|
427
440
|
store.shield.applications = nil
|
|
428
441
|
store.shield.applicationCategories = nil
|
|
442
|
+
store.shield.webDomains = nil
|
|
429
443
|
return
|
|
430
444
|
}
|
|
431
445
|
|
|
432
446
|
if isTemporarilyUnlockedInternal() {
|
|
433
447
|
store.shield.applications = nil
|
|
434
448
|
store.shield.applicationCategories = nil
|
|
449
|
+
store.shield.webDomains = nil
|
|
435
450
|
return
|
|
436
451
|
}
|
|
437
452
|
|
|
438
453
|
let validAppTokens = config.items.compactMap { $0.appToken }
|
|
439
454
|
let validCategoryTokens = config.items.compactMap { $0.categoryToken }
|
|
455
|
+
let validWebDomainTokens = config.items.compactMap { $0.webDomainToken }
|
|
440
456
|
|
|
441
|
-
guard !validAppTokens.isEmpty || !validCategoryTokens.isEmpty else {
|
|
457
|
+
guard !validAppTokens.isEmpty || !validCategoryTokens.isEmpty || !validWebDomainTokens.isEmpty else {
|
|
442
458
|
store.shield.applications = nil
|
|
443
459
|
store.shield.applicationCategories = nil
|
|
460
|
+
store.shield.webDomains = nil
|
|
444
461
|
return
|
|
445
462
|
}
|
|
446
463
|
|
|
@@ -455,6 +472,12 @@ public class ExpoAppBlockerModule: Module {
|
|
|
455
472
|
} else {
|
|
456
473
|
store.shield.applicationCategories = nil
|
|
457
474
|
}
|
|
475
|
+
|
|
476
|
+
if !validWebDomainTokens.isEmpty {
|
|
477
|
+
store.shield.webDomains = .specific(Set(validWebDomainTokens))
|
|
478
|
+
} else {
|
|
479
|
+
store.shield.webDomains = nil
|
|
480
|
+
}
|
|
458
481
|
}
|
|
459
482
|
|
|
460
483
|
private func relockApps() {
|
|
@@ -558,6 +581,10 @@ public class ExpoAppBlockerModule: Module {
|
|
|
558
581
|
if let token = tokenInfo.categoryToken, let encoded = self.encodeCategoryToken(token) {
|
|
559
582
|
tokenId = encoded
|
|
560
583
|
}
|
|
584
|
+
case .webDomain:
|
|
585
|
+
if let token = tokenInfo.webDomainToken, let encoded = self.encodeWebDomainToken(token) {
|
|
586
|
+
tokenId = encoded
|
|
587
|
+
}
|
|
561
588
|
}
|
|
562
589
|
}
|
|
563
590
|
|
|
@@ -579,6 +606,9 @@ public class ExpoAppBlockerModule: Module {
|
|
|
579
606
|
if let categoryName = tokenInfo.categoryName {
|
|
580
607
|
dict["categoryName"] = categoryName
|
|
581
608
|
}
|
|
609
|
+
if let domain = tokenInfo.domain {
|
|
610
|
+
dict["domain"] = domain
|
|
611
|
+
}
|
|
582
612
|
if let iconBase64 = tokenInfo.iconBase64 {
|
|
583
613
|
dict["iconBase64"] = iconBase64
|
|
584
614
|
}
|
|
@@ -669,6 +699,22 @@ public class ExpoAppBlockerModule: Module {
|
|
|
669
699
|
return Self.decodeCategoryTokenStatic(from: encoded)
|
|
670
700
|
}
|
|
671
701
|
|
|
702
|
+
private func encodeWebDomainToken(_ token: WebDomainToken) -> String? {
|
|
703
|
+
do {
|
|
704
|
+
let data = try JSONEncoder().encode(token)
|
|
705
|
+
return data.base64EncodedString()
|
|
706
|
+
} catch {
|
|
707
|
+
return nil
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
private func decodeWebDomainToken(from encoded: String) -> WebDomainToken? {
|
|
712
|
+
guard let data = Data(base64Encoded: encoded) else {
|
|
713
|
+
return nil
|
|
714
|
+
}
|
|
715
|
+
return try? JSONDecoder().decode(WebDomainToken.self, from: data)
|
|
716
|
+
}
|
|
717
|
+
|
|
672
718
|
// Static versions for use in View prop closures
|
|
673
719
|
static func decodeApplicationTokenStatic(from encoded: String) -> ApplicationToken? {
|
|
674
720
|
guard let data = Data(base64Encoded: encoded) else { return nil }
|
|
@@ -789,6 +835,7 @@ struct BlockedAppsContentView: View {
|
|
|
789
835
|
enum BlockedItemType: String {
|
|
790
836
|
case app
|
|
791
837
|
case category
|
|
838
|
+
case webDomain
|
|
792
839
|
}
|
|
793
840
|
|
|
794
841
|
struct BlockedItemInfo {
|
|
@@ -796,9 +843,11 @@ struct BlockedItemInfo {
|
|
|
796
843
|
let tokenId: String
|
|
797
844
|
let appToken: ApplicationToken?
|
|
798
845
|
let categoryToken: ActivityCategoryToken?
|
|
846
|
+
let webDomainToken: WebDomainToken?
|
|
799
847
|
let bundleIdentifier: String?
|
|
800
848
|
let displayName: String?
|
|
801
849
|
let categoryName: String?
|
|
850
|
+
let domain: String?
|
|
802
851
|
let iconBase64: String?
|
|
803
852
|
}
|
|
804
853
|
|
|
@@ -932,17 +981,30 @@ struct FamilyActivityPickerView: View {
|
|
|
932
981
|
]
|
|
933
982
|
}
|
|
934
983
|
|
|
984
|
+
let webDomainItems: [[String: Any]] = selection.webDomainTokens.compactMap { webDomainToken in
|
|
985
|
+
guard let tokenId = encodeSelectionWebDomainToken(webDomainToken) else {
|
|
986
|
+
return nil
|
|
987
|
+
}
|
|
988
|
+
let domain = String(describing: webDomainToken)
|
|
989
|
+
return [
|
|
990
|
+
"type": "webDomain",
|
|
991
|
+
"token": tokenId,
|
|
992
|
+
"domain": domain
|
|
993
|
+
]
|
|
994
|
+
}
|
|
995
|
+
|
|
935
996
|
// Serialize the full FamilyActivitySelection for the native view
|
|
936
997
|
var selectionBase64 = ""
|
|
937
998
|
if let selectionData = try? JSONEncoder().encode(selection) {
|
|
938
999
|
selectionBase64 = selectionData.base64EncodedString()
|
|
939
1000
|
}
|
|
940
1001
|
|
|
941
|
-
var result: [[String: Any]] = appItems + categoryItems
|
|
1002
|
+
var result: [[String: Any]] = appItems + categoryItems + webDomainItems
|
|
942
1003
|
result.append([
|
|
943
1004
|
"type": "summary",
|
|
944
1005
|
"totalApps": selection.applications.count,
|
|
945
1006
|
"totalCategories": selection.categoryTokens.count,
|
|
1007
|
+
"totalWebDomains": selection.webDomainTokens.count,
|
|
946
1008
|
"selectionData": selectionBase64
|
|
947
1009
|
])
|
|
948
1010
|
|
|
@@ -970,6 +1032,15 @@ struct FamilyActivityPickerView: View {
|
|
|
970
1032
|
}
|
|
971
1033
|
}
|
|
972
1034
|
|
|
1035
|
+
private func encodeSelectionWebDomainToken(_ token: WebDomainToken) -> String? {
|
|
1036
|
+
do {
|
|
1037
|
+
let data = try JSONEncoder().encode(token)
|
|
1038
|
+
return data.base64EncodedString()
|
|
1039
|
+
} catch {
|
|
1040
|
+
return nil
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
973
1044
|
private func resolveCategoryName(_ token: ActivityCategoryToken) -> String {
|
|
974
1045
|
let raw = String(describing: token)
|
|
975
1046
|
return raw.isEmpty ? "Category" : raw
|
|
@@ -34,7 +34,7 @@ public class ExpoAppBlockerPickerModule: Module {
|
|
|
34
34
|
// MARK: - ViewModel
|
|
35
35
|
|
|
36
36
|
class FamilyActivityPickerViewModel: ObservableObject {
|
|
37
|
-
@Published var selection = FamilyActivitySelection()
|
|
37
|
+
@Published var selection = FamilyActivitySelection(includeEntireCategory: true)
|
|
38
38
|
@Published var colorScheme: ColorScheme? = nil
|
|
39
39
|
var didSetInitial = false
|
|
40
40
|
}
|
|
@@ -70,7 +70,7 @@ class FamilyActivityPickerNativeView: ExpoView {
|
|
|
70
70
|
func setInitialSelection(_ selection: FamilyActivitySelection) {
|
|
71
71
|
guard !viewModel.didSetInitial else { return }
|
|
72
72
|
viewModel.didSetInitial = true
|
|
73
|
-
viewModel.selection = selection
|
|
73
|
+
viewModel.selection = normalizeSelection(selection)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/// Increments-only: first snapshot records baseline without clearing; higher values clear UI.
|
|
@@ -89,11 +89,21 @@ class FamilyActivityPickerNativeView: ExpoView {
|
|
|
89
89
|
var transaction = Transaction()
|
|
90
90
|
transaction.disablesAnimations = true
|
|
91
91
|
withTransaction(transaction) {
|
|
92
|
-
viewModel.selection = FamilyActivitySelection()
|
|
92
|
+
viewModel.selection = FamilyActivitySelection(includeEntireCategory: true)
|
|
93
93
|
}
|
|
94
94
|
viewModel.didSetInitial = false
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
private func normalizeSelection(_ selection: FamilyActivitySelection) -> FamilyActivitySelection {
|
|
98
|
+
// Apple forum reports indicate includeEntireCategory may not survive some encode/decode paths.
|
|
99
|
+
// Rehydrate into a fresh includeEntireCategory=true value so category picks expand to app tokens.
|
|
100
|
+
var normalized = FamilyActivitySelection(includeEntireCategory: true)
|
|
101
|
+
normalized.applicationTokens = selection.applicationTokens
|
|
102
|
+
normalized.categoryTokens = selection.categoryTokens
|
|
103
|
+
normalized.webDomainTokens = selection.webDomainTokens
|
|
104
|
+
return normalized
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
func setTheme(_ theme: String) {
|
|
98
108
|
switch theme.lowercased() {
|
|
99
109
|
case "light":
|
|
@@ -126,12 +136,23 @@ class FamilyActivityPickerNativeView: ExpoView {
|
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
138
|
|
|
139
|
+
var webDomainItems: [[String: Any]] = []
|
|
140
|
+
for token in selection.webDomainTokens {
|
|
141
|
+
if let data = try? JSONEncoder().encode(token) {
|
|
142
|
+
webDomainItems.append([
|
|
143
|
+
"type": "webDomain",
|
|
144
|
+
"token": data.base64EncodedString(),
|
|
145
|
+
"domain": String(describing: token)
|
|
146
|
+
])
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
129
150
|
var selectionBase64 = ""
|
|
130
151
|
if let selectionData = try? JSONEncoder().encode(selection) {
|
|
131
152
|
selectionBase64 = selectionData.base64EncodedString()
|
|
132
153
|
}
|
|
133
154
|
|
|
134
|
-
let items = appItems + categoryItems
|
|
155
|
+
let items = appItems + categoryItems + webDomainItems
|
|
135
156
|
|
|
136
157
|
// Do not append a synthetic "summary" row to `items` — JS counts `items.length` for UI and
|
|
137
158
|
// `totalApps` / `totalCategories` / `selectionData` already carry the same metadata.
|
|
@@ -139,6 +160,7 @@ class FamilyActivityPickerNativeView: ExpoView {
|
|
|
139
160
|
"items": items,
|
|
140
161
|
"totalApps": selection.applicationTokens.count,
|
|
141
162
|
"totalCategories": selection.categoryTokens.count,
|
|
163
|
+
"totalWebDomains": selection.webDomainTokens.count,
|
|
142
164
|
"selectionData": selectionBase64
|
|
143
165
|
])
|
|
144
166
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-blocker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
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",
|
|
@@ -30,11 +30,12 @@ export interface AndroidBlockableApp {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface IOSBlockedItem {
|
|
33
|
-
type: "app" | "category";
|
|
33
|
+
type: "app" | "category" | "webDomain";
|
|
34
34
|
token: string;
|
|
35
35
|
bundleIdentifier?: string;
|
|
36
36
|
displayName?: string;
|
|
37
37
|
categoryName?: string;
|
|
38
|
+
domain?: string;
|
|
38
39
|
iconBase64?: string;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -63,12 +64,14 @@ export interface RelockResult {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export interface FamilyActivityPickerSelectionEvent {
|
|
66
|
-
/** Selected apps and
|
|
67
|
+
/** Selected apps, categories, and web domains (pass to setBlockConfiguration) */
|
|
67
68
|
items: IOSBlockedItem[];
|
|
68
69
|
/** Number of individual apps selected */
|
|
69
70
|
totalApps: number;
|
|
70
71
|
/** Number of categories selected */
|
|
71
72
|
totalCategories: number;
|
|
73
|
+
/** Number of web domains selected */
|
|
74
|
+
totalWebDomains: number;
|
|
72
75
|
/** Base64 string - save and pass back as initialSelection to restore state */
|
|
73
76
|
selectionData: string;
|
|
74
77
|
}
|
package/src/index.ts
CHANGED
|
@@ -262,7 +262,9 @@ export function FamilyActivityPickerView({
|
|
|
262
262
|
const ne = e.nativeEvent;
|
|
263
263
|
const items = (ne.items ?? []).filter(
|
|
264
264
|
(item: { type?: string }) =>
|
|
265
|
-
item?.type === "app" ||
|
|
265
|
+
item?.type === "app" ||
|
|
266
|
+
item?.type === "category" ||
|
|
267
|
+
item?.type === "webDomain",
|
|
266
268
|
);
|
|
267
269
|
onSelectionChange({ ...ne, items });
|
|
268
270
|
}
|
|
@@ -34,6 +34,7 @@ class AppBlockerDeviceActivityMonitor: DeviceActivityMonitor {
|
|
|
34
34
|
guard let configDict = userDefaults.dictionary(forKey: blockConfigStorageKey) else {
|
|
35
35
|
store.shield.applications = nil
|
|
36
36
|
store.shield.applicationCategories = nil
|
|
37
|
+
store.shield.webDomains = nil
|
|
37
38
|
return
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -64,13 +65,22 @@ class AppBlockerDeviceActivityMonitor: DeviceActivityMonitor {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
let itemTypeRaw = (selection["type"] as? String ?? "app").lowercased()
|
|
67
|
-
let itemType: MonitorBlockedItemType
|
|
68
|
+
let itemType: MonitorBlockedItemType
|
|
69
|
+
switch itemTypeRaw {
|
|
70
|
+
case "category":
|
|
71
|
+
itemType = .category
|
|
72
|
+
case "webdomain":
|
|
73
|
+
itemType = .webDomain
|
|
74
|
+
default:
|
|
75
|
+
itemType = .app
|
|
76
|
+
}
|
|
68
77
|
|
|
69
78
|
return MonitorBlockedItemInfo(
|
|
70
79
|
type: itemType,
|
|
71
80
|
tokenId: tokenString,
|
|
72
81
|
appToken: itemType == .app ? decodeApplicationToken(from: tokenString) : nil,
|
|
73
|
-
categoryToken: itemType == .category ? decodeCategoryToken(from: tokenString) : nil
|
|
82
|
+
categoryToken: itemType == .category ? decodeCategoryToken(from: tokenString) : nil,
|
|
83
|
+
webDomainToken: itemType == .webDomain ? decodeWebDomainToken(from: tokenString) : nil
|
|
74
84
|
)
|
|
75
85
|
}
|
|
76
86
|
|
|
@@ -82,15 +92,18 @@ class AppBlockerDeviceActivityMonitor: DeviceActivityMonitor {
|
|
|
82
92
|
guard config.isActive else {
|
|
83
93
|
store.shield.applications = nil
|
|
84
94
|
store.shield.applicationCategories = nil
|
|
95
|
+
store.shield.webDomains = nil
|
|
85
96
|
return
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
let validAppTokens = config.items.compactMap { $0.appToken }
|
|
89
100
|
let validCategoryTokens = config.items.compactMap { $0.categoryToken }
|
|
101
|
+
let validWebDomainTokens = config.items.compactMap { $0.webDomainToken }
|
|
90
102
|
|
|
91
|
-
guard !validAppTokens.isEmpty || !validCategoryTokens.isEmpty else {
|
|
103
|
+
guard !validAppTokens.isEmpty || !validCategoryTokens.isEmpty || !validWebDomainTokens.isEmpty else {
|
|
92
104
|
store.shield.applications = nil
|
|
93
105
|
store.shield.applicationCategories = nil
|
|
106
|
+
store.shield.webDomains = nil
|
|
94
107
|
return
|
|
95
108
|
}
|
|
96
109
|
|
|
@@ -105,6 +118,12 @@ class AppBlockerDeviceActivityMonitor: DeviceActivityMonitor {
|
|
|
105
118
|
} else {
|
|
106
119
|
store.shield.applicationCategories = nil
|
|
107
120
|
}
|
|
121
|
+
|
|
122
|
+
if !validWebDomainTokens.isEmpty {
|
|
123
|
+
store.shield.webDomains = .specific(Set(validWebDomainTokens))
|
|
124
|
+
} else {
|
|
125
|
+
store.shield.webDomains = nil
|
|
126
|
+
}
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
private func decodeApplicationToken(from encoded: String) -> ApplicationToken? {
|
|
@@ -130,11 +149,24 @@ class AppBlockerDeviceActivityMonitor: DeviceActivityMonitor {
|
|
|
130
149
|
return nil
|
|
131
150
|
}
|
|
132
151
|
}
|
|
152
|
+
|
|
153
|
+
private func decodeWebDomainToken(from encoded: String) -> WebDomainToken? {
|
|
154
|
+
guard let data = Data(base64Encoded: encoded) else {
|
|
155
|
+
return nil
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
do {
|
|
159
|
+
return try JSONDecoder().decode(WebDomainToken.self, from: data)
|
|
160
|
+
} catch {
|
|
161
|
+
return nil
|
|
162
|
+
}
|
|
163
|
+
}
|
|
133
164
|
}
|
|
134
165
|
|
|
135
166
|
enum MonitorBlockedItemType: String {
|
|
136
167
|
case app
|
|
137
168
|
case category
|
|
169
|
+
case webDomain
|
|
138
170
|
}
|
|
139
171
|
|
|
140
172
|
struct MonitorBlockedItemInfo {
|
|
@@ -142,6 +174,7 @@ struct MonitorBlockedItemInfo {
|
|
|
142
174
|
let tokenId: String
|
|
143
175
|
let appToken: ApplicationToken?
|
|
144
176
|
let categoryToken: ActivityCategoryToken?
|
|
177
|
+
let webDomainToken: WebDomainToken?
|
|
145
178
|
}
|
|
146
179
|
|
|
147
180
|
struct MonitorBlockConfig {
|