expo-app-blocker 0.1.47 → 0.1.49

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
@@ -235,6 +235,57 @@ No special setup required beyond what the config plugin handles automatically.
235
235
  | `ios.notification.attachIcon` | `boolean` | `true` | Whether to attach the shield icon as a notification image. Set to `false` to avoid the duplicate-icon look on iOS notification banners (the system app icon is always shown either way). |
236
236
  | `android.notificationTitle` | `string` | `"App Blocked"` | Notification title |
237
237
  | `android.notificationText` | `string` | `"{appName} is blocked."` | Notification text |
238
+ | `android.overlay.icon` | `string` | — | Path to the brand icon shown above the title in the `SYSTEM_ALERT_WINDOW` overlay. Resolved relative to the project root. PNG with transparent background recommended. Build-time only — not adjustable at runtime. |
239
+
240
+ ### Android Overlay (runtime configurable via `setAndroidConfig`)
241
+
242
+ The `SYSTEM_ALERT_WINDOW` overlay flashed on top of a blocked app is fully themeable. All fields below are optional — defaults preserve the previous "App Blocked" + grey-on-white look. Pass them as a single object to `ExpoAppBlocker.setAndroidConfig({ ... })` once at app boot:
243
+
244
+ | Field | Type | Default | Description |
245
+ |---|---|---|---|
246
+ | `overlayTitle` | `string` | `"App Blocked"` | Bold heading. `{appName}` is replaced with the localized app name (e.g. `"Instagram is blocked"`). |
247
+ | `overlayText` | `string` | `"{appName} is blocked."` | Body line under the title. `{appName}` placeholder supported. |
248
+ | `overlayBackgroundColor` | `string` (hex) | `"#FFFFFF"` | Solid background color for the full-screen overlay. |
249
+ | `overlayTitleColor` | `string` (hex) | `"#111111"` | Title text color. |
250
+ | `overlayTextColor` | `string` (hex) | `"#737373"` | Body text color. |
251
+ | `overlayTitleFontSize` | `number` (sp) | `24` | Title font size. Android `sp` units — scales with system font setting. |
252
+ | `overlayTextFontSize` | `number` (sp) | `16` | Body font size. |
253
+ | `overlayTitleBold` | `boolean` | `true` | Render the title with `Typeface.BOLD`. Set to `false` for a regular weight. |
254
+ | `overlayPadding` | `number` (dp) | `32` | Inner padding on all four sides of the overlay's `LinearLayout`. |
255
+ | `overlayIconSize` | `number` (dp) | `96` | Square icon edge length. Only renders when `android.overlay.icon` was declared in the plugin config (build-time). |
256
+ | `overlayIconBottomMargin` | `number` (dp) | `20` | Vertical gap between the icon and the title. |
257
+ | `overlayTitleBottomMargin` | `number` (dp) | `12` | Vertical gap between the title and the body text. |
258
+ | `overlayShowSpinner` | `boolean` | `false` | Render an indeterminate Material circular spinner below the body text — same shape RN's `<ActivityIndicator>` produces. Useful as a "launching…" cue during the brief gap between intercept and the deep-link landing. |
259
+ | `overlaySpinnerSize` | `number` (dp) | `32` | Spinner edge length (square). Only used when `overlayShowSpinner` is true. |
260
+ | `overlaySpinnerTopMargin` | `number` (dp) | `24` | Vertical gap between the body text and the spinner. |
261
+ | `overlaySpinnerColor` | `string` (hex) | system primary | Tints the spinner. Useful to match your brand color. |
262
+ | `notificationTitle` | `string` | `"App Blocked"` | Foreground-service notification title. |
263
+ | `notificationText` | `string` | `"{appName} is blocked. Tap to manage."` | Foreground-service notification body. |
264
+
265
+ Example (matches a Hebrew RTL app with brand colors + a logo above the title):
266
+
267
+ ```ts
268
+ import * as ExpoAppBlocker from 'expo-app-blocker';
269
+
270
+ ExpoAppBlocker.setAndroidConfig({
271
+ overlayTitle: 'האפליקציה חסומה',
272
+ overlayText: 'ענה על כמה שאלות כדי להשתמש בה',
273
+ overlayBackgroundColor: '#f6f6f6',
274
+ overlayTitleColor: '#111111',
275
+ overlayTextColor: '#888888',
276
+ overlayTitleFontSize: 26,
277
+ overlayTextFontSize: 16,
278
+ overlayTitleBold: true,
279
+ overlayPadding: 32,
280
+ overlayIconSize: 112,
281
+ overlayIconBottomMargin: 20,
282
+ overlayTitleBottomMargin: 12,
283
+ notificationTitle: 'גרנדמייזר',
284
+ notificationText: 'ענה על השאלות כדי לפתוח את האפליקציה',
285
+ });
286
+ ```
287
+
288
+ **Why two layers?** `android.overlay.icon` is build-time because Android resolves drawable resources by ID, which requires the bitmap to be packed into the APK. Everything else (text, colors, sizes) lives in `SharedPreferences` and can be updated by your JS at any time — no rebuild required.
238
289
 
239
290
  ### Blur Styles
240
291
 
@@ -11,6 +11,17 @@ object AppBlockerPrefs {
11
11
  private const val KEY_OVERLAY_BG_COLOR = "overlay_bg_color"
12
12
  private const val KEY_OVERLAY_TITLE_COLOR = "overlay_title_color"
13
13
  private const val KEY_OVERLAY_TEXT_COLOR = "overlay_text_color"
14
+ private const val KEY_OVERLAY_TITLE_FONT_SIZE = "overlay_title_font_size"
15
+ private const val KEY_OVERLAY_TEXT_FONT_SIZE = "overlay_text_font_size"
16
+ private const val KEY_OVERLAY_TITLE_BOLD = "overlay_title_bold"
17
+ private const val KEY_OVERLAY_PADDING = "overlay_padding"
18
+ private const val KEY_OVERLAY_ICON_SIZE = "overlay_icon_size"
19
+ private const val KEY_OVERLAY_ICON_GAP = "overlay_icon_gap"
20
+ private const val KEY_OVERLAY_TITLE_GAP = "overlay_title_gap"
21
+ private const val KEY_OVERLAY_SHOW_SPINNER = "overlay_show_spinner"
22
+ private const val KEY_OVERLAY_SPINNER_SIZE = "overlay_spinner_size"
23
+ private const val KEY_OVERLAY_SPINNER_GAP = "overlay_spinner_gap"
24
+ private const val KEY_OVERLAY_SPINNER_COLOR = "overlay_spinner_color"
14
25
  private const val KEY_NOTIFICATION_TITLE = "notification_title"
15
26
  private const val KEY_NOTIFICATION_TEXT = "notification_text"
16
27
 
@@ -26,6 +37,13 @@ object AppBlockerPrefs {
26
37
  .apply()
27
38
  }
28
39
 
40
+ /**
41
+ * Push the overlay + notification config from JS into native prefs.
42
+ *
43
+ * Numeric `Float?` knobs (font sizes, paddings, icon size) are stored as
44
+ * floats and read back through [getOverlayFloat]. Pass `null` to keep the
45
+ * baked-in default; pass an explicit value (e.g. `28f`) to override.
46
+ */
29
47
  fun setAndroidConfig(
30
48
  context: Context,
31
49
  overlayTitle: String?,
@@ -33,18 +51,48 @@ object AppBlockerPrefs {
33
51
  overlayBackgroundColor: String?,
34
52
  overlayTitleColor: String?,
35
53
  overlayTextColor: String?,
54
+ overlayTitleFontSize: Float?,
55
+ overlayTextFontSize: Float?,
56
+ overlayTitleBold: Boolean?,
57
+ overlayPadding: Float?,
58
+ overlayIconSize: Float?,
59
+ overlayIconBottomMargin: Float?,
60
+ overlayTitleBottomMargin: Float?,
61
+ overlayShowSpinner: Boolean?,
62
+ overlaySpinnerSize: Float?,
63
+ overlaySpinnerTopMargin: Float?,
64
+ overlaySpinnerColor: String?,
36
65
  notificationTitle: String?,
37
66
  notificationText: String?,
38
67
  ) {
39
- get(context).edit()
68
+ val editor = get(context).edit()
40
69
  .putString(KEY_OVERLAY_TITLE, overlayTitle)
41
70
  .putString(KEY_OVERLAY_TEXT, overlayText)
42
71
  .putString(KEY_OVERLAY_BG_COLOR, overlayBackgroundColor)
43
72
  .putString(KEY_OVERLAY_TITLE_COLOR, overlayTitleColor)
44
73
  .putString(KEY_OVERLAY_TEXT_COLOR, overlayTextColor)
74
+ .putString(KEY_OVERLAY_SPINNER_COLOR, overlaySpinnerColor)
45
75
  .putString(KEY_NOTIFICATION_TITLE, notificationTitle)
46
76
  .putString(KEY_NOTIFICATION_TEXT, notificationText)
47
- .apply()
77
+ putNullableFloat(editor, KEY_OVERLAY_TITLE_FONT_SIZE, overlayTitleFontSize)
78
+ putNullableFloat(editor, KEY_OVERLAY_TEXT_FONT_SIZE, overlayTextFontSize)
79
+ putNullableFloat(editor, KEY_OVERLAY_PADDING, overlayPadding)
80
+ putNullableFloat(editor, KEY_OVERLAY_ICON_SIZE, overlayIconSize)
81
+ putNullableFloat(editor, KEY_OVERLAY_ICON_GAP, overlayIconBottomMargin)
82
+ putNullableFloat(editor, KEY_OVERLAY_TITLE_GAP, overlayTitleBottomMargin)
83
+ putNullableFloat(editor, KEY_OVERLAY_SPINNER_SIZE, overlaySpinnerSize)
84
+ putNullableFloat(editor, KEY_OVERLAY_SPINNER_GAP, overlaySpinnerTopMargin)
85
+ if (overlayTitleBold != null) {
86
+ editor.putBoolean(KEY_OVERLAY_TITLE_BOLD, overlayTitleBold)
87
+ } else {
88
+ editor.remove(KEY_OVERLAY_TITLE_BOLD)
89
+ }
90
+ if (overlayShowSpinner != null) {
91
+ editor.putBoolean(KEY_OVERLAY_SHOW_SPINNER, overlayShowSpinner)
92
+ } else {
93
+ editor.remove(KEY_OVERLAY_SHOW_SPINNER)
94
+ }
95
+ editor.apply()
48
96
  }
49
97
 
50
98
  fun getOverlayTitle(context: Context): String =
@@ -62,9 +110,49 @@ object AppBlockerPrefs {
62
110
  fun getOverlayTextColor(context: Context): String =
63
111
  get(context).getString(KEY_OVERLAY_TEXT_COLOR, null) ?: "#737373"
64
112
 
113
+ fun getOverlayTitleFontSize(context: Context): Float =
114
+ getOverlayFloat(context, KEY_OVERLAY_TITLE_FONT_SIZE, 24f)
115
+
116
+ fun getOverlayTextFontSize(context: Context): Float =
117
+ getOverlayFloat(context, KEY_OVERLAY_TEXT_FONT_SIZE, 16f)
118
+
119
+ fun getOverlayTitleBold(context: Context): Boolean =
120
+ get(context).getBoolean(KEY_OVERLAY_TITLE_BOLD, true)
121
+
122
+ fun getOverlayPadding(context: Context): Float =
123
+ getOverlayFloat(context, KEY_OVERLAY_PADDING, 32f)
124
+
125
+ fun getOverlayIconSize(context: Context): Float =
126
+ getOverlayFloat(context, KEY_OVERLAY_ICON_SIZE, 96f)
127
+
128
+ fun getOverlayIconBottomMargin(context: Context): Float =
129
+ getOverlayFloat(context, KEY_OVERLAY_ICON_GAP, 20f)
130
+
131
+ fun getOverlayTitleBottomMargin(context: Context): Float =
132
+ getOverlayFloat(context, KEY_OVERLAY_TITLE_GAP, 12f)
133
+
134
+ fun getOverlayShowSpinner(context: Context): Boolean =
135
+ get(context).getBoolean(KEY_OVERLAY_SHOW_SPINNER, false)
136
+
137
+ fun getOverlaySpinnerSize(context: Context): Float =
138
+ getOverlayFloat(context, KEY_OVERLAY_SPINNER_SIZE, 32f)
139
+
140
+ fun getOverlaySpinnerTopMargin(context: Context): Float =
141
+ getOverlayFloat(context, KEY_OVERLAY_SPINNER_GAP, 24f)
142
+
143
+ fun getOverlaySpinnerColor(context: Context): String? =
144
+ get(context).getString(KEY_OVERLAY_SPINNER_COLOR, null)
145
+
65
146
  fun getNotificationTitle(context: Context): String =
66
147
  get(context).getString(KEY_NOTIFICATION_TITLE, null) ?: "App Blocked"
67
148
 
68
149
  fun getNotificationText(context: Context): String =
69
150
  get(context).getString(KEY_NOTIFICATION_TEXT, null) ?: "{appName} is blocked. Tap to manage."
151
+
152
+ private fun putNullableFloat(editor: SharedPreferences.Editor, key: String, value: Float?) {
153
+ if (value != null) editor.putFloat(key, value) else editor.remove(key)
154
+ }
155
+
156
+ private fun getOverlayFloat(context: Context, key: String, fallback: Float): Float =
157
+ if (get(context).contains(key)) get(context).getFloat(key, fallback) else fallback
70
158
  }
@@ -66,6 +66,10 @@ class ExpoAppBlockerModule : Module() {
66
66
  }
67
67
 
68
68
  Function("setAndroidConfig") { config: Map<String, Any?> ->
69
+ // JS sends numbers as Double over the bridge — coerce to Float so prefs
70
+ // keep a consistent type. Booleans are forwarded as-is.
71
+ fun numberOrNull(key: String): Float? = (config[key] as? Number)?.toFloat()
72
+
69
73
  AppBlockerPrefs.setAndroidConfig(
70
74
  context,
71
75
  overlayTitle = config["overlayTitle"] as? String,
@@ -73,6 +77,17 @@ class ExpoAppBlockerModule : Module() {
73
77
  overlayBackgroundColor = config["overlayBackgroundColor"] as? String,
74
78
  overlayTitleColor = config["overlayTitleColor"] as? String,
75
79
  overlayTextColor = config["overlayTextColor"] as? String,
80
+ overlayTitleFontSize = numberOrNull("overlayTitleFontSize"),
81
+ overlayTextFontSize = numberOrNull("overlayTextFontSize"),
82
+ overlayTitleBold = config["overlayTitleBold"] as? Boolean,
83
+ overlayPadding = numberOrNull("overlayPadding"),
84
+ overlayIconSize = numberOrNull("overlayIconSize"),
85
+ overlayIconBottomMargin = numberOrNull("overlayIconBottomMargin"),
86
+ overlayTitleBottomMargin = numberOrNull("overlayTitleBottomMargin"),
87
+ overlayShowSpinner = config["overlayShowSpinner"] as? Boolean,
88
+ overlaySpinnerSize = numberOrNull("overlaySpinnerSize"),
89
+ overlaySpinnerTopMargin = numberOrNull("overlaySpinnerTopMargin"),
90
+ overlaySpinnerColor = config["overlaySpinnerColor"] as? String,
76
91
  notificationTitle = config["notificationTitle"] as? String,
77
92
  notificationText = config["notificationText"] as? String,
78
93
  )
@@ -15,6 +15,7 @@ import android.view.View
15
15
  import android.view.WindowManager
16
16
  import android.widget.ImageView
17
17
  import android.widget.LinearLayout
18
+ import android.widget.ProgressBar
18
19
  import android.widget.TextView
19
20
 
20
21
  class OverlayManager(private val context: Context) {
@@ -117,7 +118,7 @@ class OverlayManager(private val context: Context) {
117
118
 
118
119
  private fun buildOverlayView(appName: String): View {
119
120
  val density = context.resources.displayMetrics.density
120
- fun dp(value: Int) = (value * density).toInt()
121
+ fun dp(value: Float) = (value * density).toInt()
121
122
 
122
123
  val overlayTitle = AppBlockerPrefs.getOverlayTitle(context)
123
124
  .replace("{appName}", appName)
@@ -135,12 +136,19 @@ class OverlayManager(private val context: Context) {
135
136
  AppBlockerPrefs.getOverlayTextColor(context),
136
137
  Color.parseColor("#737373"),
137
138
  )
139
+ val titleFontSize = AppBlockerPrefs.getOverlayTitleFontSize(context)
140
+ val textFontSize = AppBlockerPrefs.getOverlayTextFontSize(context)
141
+ val titleBold = AppBlockerPrefs.getOverlayTitleBold(context)
142
+ val padding = AppBlockerPrefs.getOverlayPadding(context)
143
+ val iconSize = AppBlockerPrefs.getOverlayIconSize(context)
144
+ val iconGap = AppBlockerPrefs.getOverlayIconBottomMargin(context)
145
+ val titleGap = AppBlockerPrefs.getOverlayTitleBottomMargin(context)
138
146
 
139
147
  return LinearLayout(context).apply {
140
148
  orientation = LinearLayout.VERTICAL
141
149
  gravity = Gravity.CENTER
142
150
  setBackgroundColor(backgroundColor)
143
- setPadding(dp(32), dp(32), dp(32), dp(32))
151
+ setPadding(dp(padding), dp(padding), dp(padding), dp(padding))
144
152
 
145
153
  // Optional brand icon — drawable named `expo_app_blocker_overlay_icon`
146
154
  // is copied by the config plugin from `pluginConfig.android.overlay.icon`.
@@ -154,9 +162,9 @@ class OverlayManager(private val context: Context) {
154
162
  addView(ImageView(context).apply {
155
163
  val bitmap = BitmapFactory.decodeResource(context.resources, iconResId)
156
164
  if (bitmap != null) setImageBitmap(bitmap)
157
- val size = dp(96)
165
+ val size = dp(iconSize)
158
166
  layoutParams = LinearLayout.LayoutParams(size, size).apply {
159
- bottomMargin = dp(20)
167
+ bottomMargin = dp(iconGap)
160
168
  }
161
169
  })
162
170
  }
@@ -164,18 +172,37 @@ class OverlayManager(private val context: Context) {
164
172
  addView(TextView(context).apply {
165
173
  text = overlayTitle
166
174
  setTextColor(titleColor)
167
- setTextSize(TypedValue.COMPLEX_UNIT_SP, 24f)
168
- setTypeface(typeface, Typeface.BOLD)
175
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, titleFontSize)
176
+ if (titleBold) setTypeface(typeface, Typeface.BOLD)
169
177
  gravity = Gravity.CENTER
170
- setPadding(0, 0, 0, dp(12))
178
+ setPadding(0, 0, 0, dp(titleGap))
171
179
  })
172
180
 
173
181
  addView(TextView(context).apply {
174
182
  text = overlayText
175
183
  setTextColor(textColor)
176
- setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
184
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, textFontSize)
177
185
  gravity = Gravity.CENTER
178
186
  })
187
+
188
+ // Optional indeterminate spinner — gives the user a visual cue that
189
+ // the app is launching during the ~150–300ms gap between intercept
190
+ // detection and the deep-link landing.
191
+ if (AppBlockerPrefs.getOverlayShowSpinner(context)) {
192
+ val spinnerSize = dp(AppBlockerPrefs.getOverlaySpinnerSize(context))
193
+ val spinnerGap = dp(AppBlockerPrefs.getOverlaySpinnerTopMargin(context))
194
+ addView(ProgressBar(context).apply {
195
+ isIndeterminate = true
196
+ val tint = AppBlockerPrefs.getOverlaySpinnerColor(context)
197
+ if (tint != null) {
198
+ val parsed = parseColorOrNull(tint)
199
+ if (parsed != null) indeterminateTintList = android.content.res.ColorStateList.valueOf(parsed)
200
+ }
201
+ layoutParams = LinearLayout.LayoutParams(spinnerSize, spinnerSize).apply {
202
+ topMargin = spinnerGap
203
+ }
204
+ })
205
+ }
179
206
  }
180
207
  }
181
208
 
@@ -185,6 +212,12 @@ class OverlayManager(private val context: Context) {
185
212
  fallback
186
213
  }
187
214
 
215
+ private fun parseColorOrNull(hex: String): Int? = try {
216
+ Color.parseColor(hex)
217
+ } catch (_: IllegalArgumentException) {
218
+ null
219
+ }
220
+
188
221
  private fun buildLayoutParams(): WindowManager.LayoutParams {
189
222
  @Suppress("DEPRECATION")
190
223
  val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-app-blocker",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
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",
@@ -159,6 +159,28 @@ export interface AndroidConfig {
159
159
  overlayTitleColor?: string;
160
160
  /** Hex color (e.g. "#737373") for the overlay body text. Default: "#737373". */
161
161
  overlayTextColor?: string;
162
+ /** Title font size in sp. Default: 24. */
163
+ overlayTitleFontSize?: number;
164
+ /** Body font size in sp. Default: 16. */
165
+ overlayTextFontSize?: number;
166
+ /** Render the title in bold. Default: true. */
167
+ overlayTitleBold?: boolean;
168
+ /** Inner padding (all sides) in dp. Default: 32. */
169
+ overlayPadding?: number;
170
+ /** Icon edge length in dp (square). Default: 96. Only used when an overlay icon is configured via the plugin. */
171
+ overlayIconSize?: number;
172
+ /** Vertical gap (dp) between the icon and the title. Default: 20. */
173
+ overlayIconBottomMargin?: number;
174
+ /** Vertical gap (dp) between the title and the body text. Default: 12. */
175
+ overlayTitleBottomMargin?: number;
176
+ /** Show an indeterminate circular spinner under the body text. Useful as a "launching…" cue during the brief gap between intercept and the deep-link landing. Default: false. */
177
+ overlayShowSpinner?: boolean;
178
+ /** Spinner edge length in dp (square). Default: 32. */
179
+ overlaySpinnerSize?: number;
180
+ /** Vertical gap (dp) between the body text and the spinner. Default: 24. */
181
+ overlaySpinnerTopMargin?: number;
182
+ /** Hex color (e.g. "#7cb518") tinting the spinner. Default: system primary. */
183
+ overlaySpinnerColor?: string;
162
184
  /** Notification title when app is blocked. Use {appName} as placeholder. Default: "App Blocked" */
163
185
  notificationTitle?: string;
164
186
  /** Notification text when app is blocked. Use {appName} as placeholder. */