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. `{appName}` replaced with blocked app name |
91
- | `ios.shield.titleColor` | `string` | `"#111111"` | Title text color (hex) |
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.secondaryButtonLabelColor` | `string` | `"#8c8c8c"` | Secondary button text color (hex) |
99
- | `ios.shield.backgroundColor` | `string\|null` | `null` | Background tint color over blur (hex, supports alpha e.g. `"#FF000033"`) |
100
- | `ios.shield.backgroundBlurStyle` | `string` | `"systemThickMaterial"` | Blur style. Options: `extraLight`, `light`, `dark`, `regular`, `prominent`, `systemUltraThinMaterial`, `systemThinMaterial`, `systemMaterial`, `systemThickMaterial`, `systemChromeMaterial`, and light/dark forced variants |
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",
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",
@@ -141,23 +141,45 @@ function withAppBlockerIOS(config, pluginConfig) {
141
141
  }
142
142
  }
143
143
 
144
- // Inject config values into extension Swift files
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
- "SHIELD_SUBTITLE_COLOR_PLACEHOLDER": shield.subtitleColor || "#8c8c8c",
152
- "SHIELD_PRIMARY_LABEL_PLACEHOLDER": shield.primaryButtonLabel || "Earn Free Time",
153
- "SHIELD_PRIMARY_LABEL_COLOR_PLACEHOLDER": shield.primaryButtonLabelColor || "#ffffff",
154
- "SHIELD_PRIMARY_BG_COLOR_PLACEHOLDER": shield.primaryButtonBackgroundColor || shield.primaryButtonColor || "#7cb518",
155
- "SHIELD_SECONDARY_LABEL_PLACEHOLDER": shield.secondaryButtonLabel === null ? "NONE" : (shield.secondaryButtonLabel || "Not now"),
156
- "SHIELD_SECONDARY_LABEL_COLOR_PLACEHOLDER": shield.secondaryButtonLabelColor || "#8c8c8c",
157
- "SHIELD_BG_COLOR_PLACEHOLDER": shield.backgroundColor || "NONE",
158
- "SHIELD_BLUR_STYLE_PLACEHOLDER": shield.backgroundBlurStyle || "systemThickMaterial",
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
- let changed = false;
173
- for (const [placeholder, value] of Object.entries(replacements)) {
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
- if (changed) fs.writeFileSync(filePath, content);
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.icon;
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 text. Use {appName} as placeholder for the blocked app name. Default: "Hold on!" */
69
+ /** Title shown on the shield. Default: "Hold on!" */
70
70
  title?: string;
71
- /** Title text color (hex). Default: "#111111" */
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
- /** Subtitle text color (hex). Default: "#8c8c8c" */
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
- /** Primary button label text color (hex). Default: "#ffffff" */
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
- /** Secondary button label text color (hex). Default: "#8c8c8c" */
86
- secondaryButtonLabelColor?: string;
87
- /**
88
- * Background tint color (hex, supports alpha e.g. "#FF000033").
89
- * Applied as overlay on top of the blur. Default: null (no tint)
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
- // ── Configurable values (injected by plugin at prebuild) ──────────────
10
- private let cfgTitle = "SHIELD_TITLE_PLACEHOLDER" // default: "Hold on!"
11
- private let cfgTitleColor = "SHIELD_TITLE_COLOR_PLACEHOLDER" // default: "#111111"
12
- private let cfgSubtitle = "SHIELD_SUBTITLE_PLACEHOLDER" // default: "{appName} is blocked."
13
- private let cfgSubtitleColor = "SHIELD_SUBTITLE_COLOR_PLACEHOLDER" // default: "#8c8c8c"
14
- private let cfgPrimaryLabel = "SHIELD_PRIMARY_LABEL_PLACEHOLDER" // default: "Earn Free Time"
15
- private let cfgPrimaryLabelColor = "SHIELD_PRIMARY_LABEL_COLOR_PLACEHOLDER" // default: "#ffffff"
16
- private let cfgPrimaryBgColor = "SHIELD_PRIMARY_BG_COLOR_PLACEHOLDER" // default: "#7cb518"
17
- private let cfgSecondaryLabel = "SHIELD_SECONDARY_LABEL_PLACEHOLDER" // default: "Not now", "NONE" to hide
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]] { return items.count }
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 exp = defaults.object(forKey: "appBlocker.temporaryUnlock.v1") as? Date else { return false }
89
- return Date() < exp
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 buildConfig(appName: String) -> ShieldConfiguration {
40
+ private func makeConfig(appName: String) -> ShieldConfiguration {
93
41
  if isTemporarilyUnlocked() {
94
42
  return ShieldConfiguration(
95
- backgroundBlurStyle: resolveBlurStyle(cfgBlurStyle),
96
- backgroundColor: cfgBgColor == "NONE" ? nil : hexColor(cfgBgColor),
43
+ backgroundBlurStyle: shieldBackgroundColor == nil ? .systemThickMaterial : nil,
44
+ backgroundColor: shieldBackgroundColor,
97
45
  icon: mascotIcon,
98
- title: ShieldConfiguration.Label(text: "Almost there!", color: hexColor(cfgTitleColor)),
99
- subtitle: ShieldConfiguration.Label(text: "Your free time is loading. Try again in a moment.", color: hexColor(cfgSubtitleColor)),
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: hexColor(cfgPrimaryBgColor),
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 fullSubtitle = count > 1
110
- ? "\(subtitle) You have \(count) apps blocked."
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 secondaryLabel: ShieldConfiguration.Label? =
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: resolveBlurStyle(cfgBlurStyle),
119
- backgroundColor: cfgBgColor == "NONE" ? nil : hexColor(cfgBgColor),
61
+ backgroundBlurStyle: shieldBackgroundColor == nil ? .systemThickMaterial : nil,
62
+ backgroundColor: shieldBackgroundColor,
120
63
  icon: mascotIcon,
121
- title: ShieldConfiguration.Label(text: title, color: hexColor(cfgTitleColor)),
122
- subtitle: ShieldConfiguration.Label(text: fullSubtitle, color: hexColor(cfgSubtitleColor)),
123
- primaryButtonLabel: ShieldConfiguration.Label(text: cfgPrimaryLabel, color: hexColor(cfgPrimaryLabelColor)),
124
- primaryButtonBackgroundColor: hexColor(cfgPrimaryBgColor),
125
- secondaryButtonLabel: secondaryLabel
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
- buildConfig(appName: application.localizedDisplayName ?? "This app")
73
+ makeConfig(appName: application.localizedDisplayName ?? "This app")
133
74
  }
134
75
 
135
76
  override func configuration(shielding application: Application, in category: ActivityCategory) -> ShieldConfiguration {
136
- buildConfig(appName: category.localizedDisplayName ?? "This category")
77
+ makeConfig(appName: category.localizedDisplayName ?? "This category")
137
78
  }
138
79
 
139
80
  override func configuration(shielding webDomain: WebDomain) -> ShieldConfiguration {
140
- buildConfig(appName: webDomain.domain ?? "This website")
81
+ makeConfig(appName: webDomain.domain ?? "This website")
141
82
  }
142
83
 
143
84
  override func configuration(shielding webDomain: WebDomain, in category: ActivityCategory) -> ShieldConfiguration {
144
- buildConfig(appName: webDomain.domain ?? "This website")
85
+ makeConfig(appName: webDomain.domain ?? "This website")
145
86
  }
146
87
  }