expo-app-blocker 0.1.3 → 0.1.4
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/README.md
CHANGED
|
@@ -87,18 +87,13 @@ Add the plugin to your `app.json`:
|
|
|
87
87
|
| Option | Type | Default | Description |
|
|
88
88
|
|---|---|---|---|
|
|
89
89
|
| `ios.appGroup` | `string` | Required | App Group identifier for shared data |
|
|
90
|
-
| `ios.shield.title` | `string` | `"Hold on!"` | Shield title
|
|
91
|
-
| `ios.shield.
|
|
92
|
-
| `ios.shield.subtitle` | `string` | `"{appName} is blocked."` | Shield subtitle. `{appName}` replaced with blocked app name |
|
|
93
|
-
| `ios.shield.subtitleColor` | `string` | `"#8c8c8c"` | Subtitle text color (hex) |
|
|
90
|
+
| `ios.shield.title` | `string` | `"Hold on!"` | Shield overlay title |
|
|
91
|
+
| `ios.shield.subtitle` | `string` | `"{appName} is blocked."` | Shield subtitle. `{appName}` is replaced with the blocked app name |
|
|
94
92
|
| `ios.shield.primaryButtonLabel` | `string` | `"Earn Free Time"` | Primary button text |
|
|
95
|
-
| `ios.shield.primaryButtonLabelColor` | `string` | `"#ffffff"` | Primary button text color (hex) |
|
|
96
|
-
| `ios.shield.primaryButtonBackgroundColor` | `string` | `"#7cb518"` | Primary button background color (hex) |
|
|
97
93
|
| `ios.shield.secondaryButtonLabel` | `string\|null` | `"Not now"` | Secondary button text. Set to `null` to hide |
|
|
98
|
-
| `ios.shield.
|
|
99
|
-
| `ios.shield.
|
|
100
|
-
| `ios.shield.
|
|
101
|
-
| `ios.shield.icon` | `string` | SF Symbol | Path to custom shield icon PNG (relative to project root) |
|
|
94
|
+
| `ios.shield.primaryButtonColor` | `string` | `"#7cb518"` | Primary button background color (hex) |
|
|
95
|
+
| `ios.shield.backgroundBlurStyle` | `string` | `"systemThickMaterial"` | iOS blur style |
|
|
96
|
+
| `ios.shield.icon` | `string` | SF Symbol | Path to custom shield icon PNG (relative to project root, e.g. `"./assets/shield-icon.png"`) |
|
|
102
97
|
| `android.notificationTitle` | `string` | `"App Blocked"` | Notification title |
|
|
103
98
|
| `android.notificationText` | `string` | `"{appName} is blocked."` | Notification text |
|
|
104
99
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-blocker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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/plugin/src/index.js
CHANGED
|
@@ -141,23 +141,45 @@ function withAppBlockerIOS(config, pluginConfig) {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
//
|
|
144
|
+
// Helper: hex to RGB floats
|
|
145
|
+
function hexToRgb(hex) {
|
|
146
|
+
const h = hex.replace("#", "");
|
|
147
|
+
return {
|
|
148
|
+
r: (parseInt(h.substring(0, 2), 16) / 255).toFixed(3),
|
|
149
|
+
g: (parseInt(h.substring(2, 4), 16) / 255).toFixed(3),
|
|
150
|
+
b: (parseInt(h.substring(4, 6), 16) / 255).toFixed(3),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Shield config defaults
|
|
145
155
|
const shield = pluginConfig?.ios?.shield || {};
|
|
156
|
+
const primaryColor = hexToRgb(shield.primaryButtonColor || "#fb6107");
|
|
157
|
+
const titleColor = hexToRgb(shield.titleColor || "#111111");
|
|
158
|
+
const subtitleColor = hexToRgb(shield.subtitleColor || "#737373");
|
|
159
|
+
const bgColor = shield.backgroundColor ? hexToRgb(shield.backgroundColor) : null;
|
|
160
|
+
|
|
161
|
+
// All placeholder replacements
|
|
146
162
|
const replacements = {
|
|
147
163
|
"APP_GROUP_PLACEHOLDER": appGroup,
|
|
148
164
|
"SHIELD_TITLE_PLACEHOLDER": shield.title || "Hold on!",
|
|
149
|
-
"SHIELD_TITLE_COLOR_PLACEHOLDER": shield.titleColor || "#111111",
|
|
150
165
|
"SHIELD_SUBTITLE_PLACEHOLDER": shield.subtitle || "{appName} is blocked.",
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
166
|
+
"SHIELD_PRIMARY_BUTTON_PLACEHOLDER": shield.primaryButtonLabel || "Earn Free Time",
|
|
167
|
+
"SHIELD_SECONDARY_BUTTON_PLACEHOLDER": shield.secondaryButtonLabel === null ? "none" : (shield.secondaryButtonLabel || "Not now"),
|
|
168
|
+
"SHIELD_PRIMARY_R_PLACEHOLDER": primaryColor.r,
|
|
169
|
+
"SHIELD_PRIMARY_G_PLACEHOLDER": primaryColor.g,
|
|
170
|
+
"SHIELD_PRIMARY_B_PLACEHOLDER": primaryColor.b,
|
|
171
|
+
"SHIELD_TITLE_R_PLACEHOLDER": titleColor.r,
|
|
172
|
+
"SHIELD_TITLE_G_PLACEHOLDER": titleColor.g,
|
|
173
|
+
"SHIELD_TITLE_B_PLACEHOLDER": titleColor.b,
|
|
174
|
+
"SHIELD_SUBTITLE_R_PLACEHOLDER": subtitleColor.r,
|
|
175
|
+
"SHIELD_SUBTITLE_G_PLACEHOLDER": subtitleColor.g,
|
|
176
|
+
"SHIELD_SUBTITLE_B_PLACEHOLDER": subtitleColor.b,
|
|
177
|
+
"SHIELD_BG_PLACEHOLDER": bgColor
|
|
178
|
+
? `UIColor(red: ${bgColor.r}, green: ${bgColor.g}, blue: ${bgColor.b}, alpha: 1.0)`
|
|
179
|
+
: "nil",
|
|
159
180
|
};
|
|
160
181
|
|
|
182
|
+
// Inject all placeholders into extension Swift files
|
|
161
183
|
const targetsDir = path.join(path.dirname(platformRoot), "targets");
|
|
162
184
|
if (fs.existsSync(targetsDir)) {
|
|
163
185
|
const dirs = fs.readdirSync(targetsDir);
|
|
@@ -169,20 +191,16 @@ function withAppBlockerIOS(config, pluginConfig) {
|
|
|
169
191
|
if (!file.endsWith(".swift")) continue;
|
|
170
192
|
const filePath = path.join(dirPath, file);
|
|
171
193
|
let content = fs.readFileSync(filePath, "utf-8");
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (content.includes(placeholder)) {
|
|
175
|
-
content = content.replace(new RegExp(placeholder, "g"), value);
|
|
176
|
-
changed = true;
|
|
177
|
-
}
|
|
194
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
195
|
+
content = content.replace(new RegExp(key, "g"), value);
|
|
178
196
|
}
|
|
179
|
-
|
|
197
|
+
fs.writeFileSync(filePath, content);
|
|
180
198
|
}
|
|
181
199
|
}
|
|
182
200
|
}
|
|
183
201
|
|
|
184
202
|
// Copy shield icon to ShieldConfiguration target assets
|
|
185
|
-
const shieldIcon = shield
|
|
203
|
+
const shieldIcon = pluginConfig?.ios?.shield?.icon;
|
|
186
204
|
if (shieldIcon) {
|
|
187
205
|
const projectRoot = path.dirname(platformRoot);
|
|
188
206
|
const iconSrc = path.resolve(projectRoot, shieldIcon);
|
|
@@ -66,39 +66,23 @@ export interface RelockResult {
|
|
|
66
66
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
67
67
|
|
|
68
68
|
export interface ShieldConfig {
|
|
69
|
-
/** Title
|
|
69
|
+
/** Title shown on the shield. Default: "Hold on!" */
|
|
70
70
|
title?: string;
|
|
71
|
-
/**
|
|
72
|
-
titleColor?: string;
|
|
73
|
-
/** Subtitle text. Use {appName} as placeholder. Default: "{appName} is blocked." */
|
|
71
|
+
/** Subtitle shown on the shield. Use {appName} as placeholder. Default: "{appName} is blocked." */
|
|
74
72
|
subtitle?: string;
|
|
75
|
-
/**
|
|
76
|
-
subtitleColor?: string;
|
|
77
|
-
/** Primary button label text. Default: "Earn Free Time" */
|
|
73
|
+
/** Primary button label. Default: "Earn Free Time" */
|
|
78
74
|
primaryButtonLabel?: string;
|
|
79
|
-
/**
|
|
80
|
-
primaryButtonLabelColor?: string;
|
|
81
|
-
/** Primary button background color (hex). Default: "#7cb518" */
|
|
82
|
-
primaryButtonBackgroundColor?: string;
|
|
83
|
-
/** Secondary button label text. Set to null to hide the button. Default: "Not now" */
|
|
75
|
+
/** Secondary button label. Set to null to hide. Default: "Not now" */
|
|
84
76
|
secondaryButtonLabel?: string | null;
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
/** Primary button background color (hex). Default: "#fb6107" */
|
|
78
|
+
primaryButtonColor?: string;
|
|
79
|
+
/** Title text color (hex). Default: "#111111" */
|
|
80
|
+
titleColor?: string;
|
|
81
|
+
/** Subtitle text color (hex). Default: "#737373" */
|
|
82
|
+
subtitleColor?: string;
|
|
83
|
+
/** Background color (hex). Default: null (uses system blur). Set to a color like "#f6f6f6" for a solid light background. */
|
|
91
84
|
backgroundColor?: string | null;
|
|
92
|
-
/**
|
|
93
|
-
* Background blur style. Default: "systemThickMaterial"
|
|
94
|
-
*
|
|
95
|
-
* Options: "extraLight", "light", "dark", "regular", "prominent",
|
|
96
|
-
* "systemUltraThinMaterial", "systemThinMaterial", "systemMaterial",
|
|
97
|
-
* "systemThickMaterial", "systemChromeMaterial",
|
|
98
|
-
* and light/dark forced variants (e.g. "systemMaterialDark")
|
|
99
|
-
*/
|
|
100
|
-
backgroundBlurStyle?: string;
|
|
101
|
-
/** Path to custom shield icon PNG (relative to project root). Optional. */
|
|
85
|
+
/** Path to shield icon image (PNG). Optional. */
|
|
102
86
|
icon?: string;
|
|
103
87
|
}
|
|
104
88
|
|
|
@@ -6,69 +6,15 @@ class ShieldConfigurationExtension: ShieldConfigurationDataSource {
|
|
|
6
6
|
|
|
7
7
|
private let appGroupIdentifier = "APP_GROUP_PLACEHOLDER"
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
private let
|
|
11
|
-
private let
|
|
12
|
-
private let
|
|
13
|
-
private let
|
|
14
|
-
private let
|
|
15
|
-
private let
|
|
16
|
-
private let
|
|
17
|
-
private let
|
|
18
|
-
private let cfgSecondaryLabelColor = "SHIELD_SECONDARY_LABEL_COLOR_PLACEHOLDER" // default: "#8c8c8c"
|
|
19
|
-
private let cfgBgColor = "SHIELD_BG_COLOR_PLACEHOLDER" // default: "NONE" (no tint)
|
|
20
|
-
private let cfgBlurStyle = "SHIELD_BLUR_STYLE_PLACEHOLDER" // default: "systemThickMaterial"
|
|
21
|
-
|
|
22
|
-
// ── Helpers ───────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
private func hexColor(_ hex: String) -> UIColor {
|
|
25
|
-
var h = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
26
|
-
if h.hasPrefix("#") { h.removeFirst() }
|
|
27
|
-
|
|
28
|
-
var rgb: UInt64 = 0
|
|
29
|
-
Scanner(string: h).scanHexInt64(&rgb)
|
|
30
|
-
|
|
31
|
-
if h.count == 8 { // RRGGBBAA
|
|
32
|
-
return UIColor(
|
|
33
|
-
red: CGFloat((rgb >> 24) & 0xFF) / 255,
|
|
34
|
-
green: CGFloat((rgb >> 16) & 0xFF) / 255,
|
|
35
|
-
blue: CGFloat((rgb >> 8) & 0xFF) / 255,
|
|
36
|
-
alpha: CGFloat(rgb & 0xFF) / 255
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
return UIColor(
|
|
40
|
-
red: CGFloat((rgb >> 16) & 0xFF) / 255,
|
|
41
|
-
green: CGFloat((rgb >> 8) & 0xFF) / 255,
|
|
42
|
-
blue: CGFloat(rgb & 0xFF) / 255,
|
|
43
|
-
alpha: 1.0
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
private func resolveBlurStyle(_ name: String) -> UIBlurEffect.Style {
|
|
48
|
-
switch name {
|
|
49
|
-
case "extraLight": return .extraLight
|
|
50
|
-
case "light": return .light
|
|
51
|
-
case "dark": return .dark
|
|
52
|
-
case "regular": return .regular
|
|
53
|
-
case "prominent": return .prominent
|
|
54
|
-
case "systemUltraThinMaterial": return .systemUltraThinMaterial
|
|
55
|
-
case "systemThinMaterial": return .systemThinMaterial
|
|
56
|
-
case "systemMaterial": return .systemMaterial
|
|
57
|
-
case "systemThickMaterial": return .systemThickMaterial
|
|
58
|
-
case "systemChromeMaterial": return .systemChromeMaterial
|
|
59
|
-
case "systemUltraThinMaterialLight": return .systemUltraThinMaterialLight
|
|
60
|
-
case "systemThinMaterialLight": return .systemThinMaterialLight
|
|
61
|
-
case "systemMaterialLight": return .systemMaterialLight
|
|
62
|
-
case "systemThickMaterialLight": return .systemThickMaterialLight
|
|
63
|
-
case "systemChromeMaterialLight": return .systemChromeMaterialLight
|
|
64
|
-
case "systemUltraThinMaterialDark": return .systemUltraThinMaterialDark
|
|
65
|
-
case "systemThinMaterialDark": return .systemThinMaterialDark
|
|
66
|
-
case "systemMaterialDark": return .systemMaterialDark
|
|
67
|
-
case "systemThickMaterialDark": return .systemThickMaterialDark
|
|
68
|
-
case "systemChromeMaterialDark": return .systemChromeMaterialDark
|
|
69
|
-
default: return .systemThickMaterial
|
|
70
|
-
}
|
|
71
|
-
}
|
|
9
|
+
// All values below are replaced by the config plugin at prebuild time
|
|
10
|
+
private let shieldTitle = "SHIELD_TITLE_PLACEHOLDER"
|
|
11
|
+
private let shieldSubtitle = "SHIELD_SUBTITLE_PLACEHOLDER"
|
|
12
|
+
private let shieldPrimaryButtonLabel = "SHIELD_PRIMARY_BUTTON_PLACEHOLDER"
|
|
13
|
+
private let shieldSecondaryButtonLabel = "SHIELD_SECONDARY_BUTTON_PLACEHOLDER"
|
|
14
|
+
private let shieldPrimaryButtonColor = UIColor(red: SHIELD_PRIMARY_R_PLACEHOLDER, green: SHIELD_PRIMARY_G_PLACEHOLDER, blue: SHIELD_PRIMARY_B_PLACEHOLDER, alpha: 1.0)
|
|
15
|
+
private let shieldBackgroundColor: UIColor? = SHIELD_BG_PLACEHOLDER
|
|
16
|
+
private let shieldTitleColor = UIColor(red: SHIELD_TITLE_R_PLACEHOLDER, green: SHIELD_TITLE_G_PLACEHOLDER, blue: SHIELD_TITLE_B_PLACEHOLDER, alpha: 1.0)
|
|
17
|
+
private let shieldSubtitleColor = UIColor(red: SHIELD_SUBTITLE_R_PLACEHOLDER, green: SHIELD_SUBTITLE_G_PLACEHOLDER, blue: SHIELD_SUBTITLE_B_PLACEHOLDER, alpha: 1.0)
|
|
72
18
|
|
|
73
19
|
private var mascotIcon: UIImage? {
|
|
74
20
|
let bundle = Bundle(for: type(of: self))
|
|
@@ -79,68 +25,63 @@ class ShieldConfigurationExtension: ShieldConfigurationDataSource {
|
|
|
79
25
|
private func getBlockedAppCount() -> Int {
|
|
80
26
|
guard let defaults = UserDefaults(suiteName: appGroupIdentifier) else { return 0 }
|
|
81
27
|
guard let config = defaults.dictionary(forKey: "appBlocker.blockConfiguration.v1") else { return 0 }
|
|
82
|
-
if let items = config["blockedItems"] as? [[String: Any]] {
|
|
28
|
+
if let items = config["blockedItems"] as? [[String: Any]] {
|
|
29
|
+
return items.count
|
|
30
|
+
}
|
|
83
31
|
return 0
|
|
84
32
|
}
|
|
85
33
|
|
|
86
34
|
private func isTemporarilyUnlocked() -> Bool {
|
|
87
35
|
guard let defaults = UserDefaults(suiteName: appGroupIdentifier) else { return false }
|
|
88
|
-
guard let
|
|
89
|
-
return Date() <
|
|
36
|
+
guard let expiration = defaults.object(forKey: "appBlocker.temporaryUnlock.v1") as? Date else { return false }
|
|
37
|
+
return Date() < expiration
|
|
90
38
|
}
|
|
91
39
|
|
|
92
|
-
private func
|
|
40
|
+
private func makeConfig(appName: String) -> ShieldConfiguration {
|
|
93
41
|
if isTemporarilyUnlocked() {
|
|
94
42
|
return ShieldConfiguration(
|
|
95
|
-
backgroundBlurStyle:
|
|
96
|
-
backgroundColor:
|
|
43
|
+
backgroundBlurStyle: shieldBackgroundColor == nil ? .systemThickMaterial : nil,
|
|
44
|
+
backgroundColor: shieldBackgroundColor,
|
|
97
45
|
icon: mascotIcon,
|
|
98
|
-
title: ShieldConfiguration.Label(text: "Almost there!", color:
|
|
99
|
-
subtitle: ShieldConfiguration.Label(text: "Your free time is loading. Try again in a moment.", color:
|
|
46
|
+
title: ShieldConfiguration.Label(text: "Almost there!", color: shieldTitleColor),
|
|
47
|
+
subtitle: ShieldConfiguration.Label(text: "Your free time is loading. Try again in a moment.", color: shieldSubtitleColor),
|
|
100
48
|
primaryButtonLabel: ShieldConfiguration.Label(text: "OK", color: .white),
|
|
101
|
-
primaryButtonBackgroundColor:
|
|
49
|
+
primaryButtonBackgroundColor: shieldPrimaryButtonColor,
|
|
102
50
|
secondaryButtonLabel: nil
|
|
103
51
|
)
|
|
104
52
|
}
|
|
105
53
|
|
|
106
|
-
let title = cfgTitle.replacingOccurrences(of: "{appName}", with: appName)
|
|
107
|
-
let subtitle = cfgSubtitle.replacingOccurrences(of: "{appName}", with: appName)
|
|
108
54
|
let count = getBlockedAppCount()
|
|
109
|
-
let
|
|
110
|
-
|
|
111
|
-
: subtitle
|
|
55
|
+
let context = count > 1 ? " You have \(count) apps blocked." : ""
|
|
56
|
+
let subtitle = shieldSubtitle.replacingOccurrences(of: "{appName}", with: appName) + context
|
|
112
57
|
|
|
113
|
-
let
|
|
114
|
-
cfgSecondaryLabel == "NONE" ? nil :
|
|
115
|
-
ShieldConfiguration.Label(text: cfgSecondaryLabel, color: hexColor(cfgSecondaryLabelColor))
|
|
58
|
+
let hasSecondary = !shieldSecondaryButtonLabel.isEmpty && shieldSecondaryButtonLabel != "none"
|
|
116
59
|
|
|
117
60
|
return ShieldConfiguration(
|
|
118
|
-
backgroundBlurStyle:
|
|
119
|
-
backgroundColor:
|
|
61
|
+
backgroundBlurStyle: shieldBackgroundColor == nil ? .systemThickMaterial : nil,
|
|
62
|
+
backgroundColor: shieldBackgroundColor,
|
|
120
63
|
icon: mascotIcon,
|
|
121
|
-
title: ShieldConfiguration.Label(text:
|
|
122
|
-
subtitle: ShieldConfiguration.Label(text:
|
|
123
|
-
primaryButtonLabel: ShieldConfiguration.Label(text:
|
|
124
|
-
primaryButtonBackgroundColor:
|
|
125
|
-
secondaryButtonLabel:
|
|
64
|
+
title: ShieldConfiguration.Label(text: shieldTitle, color: shieldTitleColor),
|
|
65
|
+
subtitle: ShieldConfiguration.Label(text: subtitle, color: shieldSubtitleColor),
|
|
66
|
+
primaryButtonLabel: ShieldConfiguration.Label(text: shieldPrimaryButtonLabel, color: .white),
|
|
67
|
+
primaryButtonBackgroundColor: shieldPrimaryButtonColor,
|
|
68
|
+
secondaryButtonLabel: hasSecondary ? ShieldConfiguration.Label(text: shieldSecondaryButtonLabel, color: shieldSubtitleColor) : nil
|
|
126
69
|
)
|
|
127
70
|
}
|
|
128
71
|
|
|
129
|
-
// ── Overrides ─────────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
72
|
override func configuration(shielding application: Application) -> ShieldConfiguration {
|
|
132
|
-
|
|
73
|
+
makeConfig(appName: application.localizedDisplayName ?? "This app")
|
|
133
74
|
}
|
|
134
75
|
|
|
135
76
|
override func configuration(shielding application: Application, in category: ActivityCategory) -> ShieldConfiguration {
|
|
136
|
-
|
|
77
|
+
makeConfig(appName: category.localizedDisplayName ?? "This category")
|
|
137
78
|
}
|
|
138
79
|
|
|
139
80
|
override func configuration(shielding webDomain: WebDomain) -> ShieldConfiguration {
|
|
140
|
-
|
|
81
|
+
makeConfig(appName: webDomain.domain ?? "This website")
|
|
141
82
|
}
|
|
142
83
|
|
|
143
84
|
override func configuration(shielding webDomain: WebDomain, in category: ActivityCategory) -> ShieldConfiguration {
|
|
144
|
-
|
|
85
|
+
makeConfig(appName: webDomain.domain ?? "This website")
|
|
145
86
|
}
|
|
146
87
|
}
|