expo-app-blocker 0.1.43 → 0.1.45
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.
|
@@ -19,8 +19,13 @@ jobs:
|
|
|
19
19
|
|
|
20
20
|
- name: Determine version bump
|
|
21
21
|
id: version
|
|
22
|
+
# Never inline `${{ github.event.head_commit.message }}` into the
|
|
23
|
+
# script body — GitHub Actions substitutes it BEFORE the shell runs,
|
|
24
|
+
# so backticks / parens / `${...}` / `{vars}` get evaluated as
|
|
25
|
+
# commands. Always pass via env: so it lands as a quoted string.
|
|
26
|
+
env:
|
|
27
|
+
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
|
22
28
|
run: |
|
|
23
|
-
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
|
24
29
|
if echo "$COMMIT_MSG" | grep -qw "\[major\]"; then
|
|
25
30
|
echo "bump=major" >> $GITHUB_OUTPUT
|
|
26
31
|
elif echo "$COMMIT_MSG" | grep -qw "\[minor\]"; then
|
package/README.md
CHANGED
|
@@ -86,13 +86,22 @@ npx expo install expo-app-blocker
|
|
|
86
86
|
|
|
87
87
|
### 2. Configure `app.json`
|
|
88
88
|
|
|
89
|
+
> **Three things are required on iOS** and skipping any of them produces a cryptic build failure:
|
|
90
|
+
> 1. `ios.appleTeamId` — `@bacons/apple-targets` refuses to add the extension targets without it.
|
|
91
|
+
> 2. `ios.entitlements` with **Family Controls + the App Group** — the extension `expo-target.config.js` files read `ios.entitlements['com.apple.security.application-groups'][0]` to learn which App Group to embed. If it's missing they fall back to `group.expo.app-blocker` and the build fails with `An Application Group with Identifier 'group.expo.app-blocker' is not available`.
|
|
92
|
+
> 3. `@bacons/apple-targets` must appear in the `plugins` array **after** `expo-app-blocker`. The targets only get added to the Xcode project when this plugin runs.
|
|
93
|
+
|
|
89
94
|
```json
|
|
90
95
|
{
|
|
91
96
|
"expo": {
|
|
92
97
|
"scheme": "myapp",
|
|
93
98
|
"ios": {
|
|
94
99
|
"bundleIdentifier": "com.yourapp.id",
|
|
95
|
-
"appleTeamId": "YOUR_TEAM_ID"
|
|
100
|
+
"appleTeamId": "YOUR_TEAM_ID",
|
|
101
|
+
"entitlements": {
|
|
102
|
+
"com.apple.developer.family-controls": true,
|
|
103
|
+
"com.apple.security.application-groups": ["group.com.yourapp.blocker"]
|
|
104
|
+
}
|
|
96
105
|
},
|
|
97
106
|
"plugins": [
|
|
98
107
|
["expo-app-blocker", {
|
|
@@ -107,12 +116,15 @@ npx expo install expo-app-blocker
|
|
|
107
116
|
"backgroundBlurStyle": "systemThickMaterialLight"
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
|
-
}]
|
|
119
|
+
}],
|
|
120
|
+
"@bacons/apple-targets"
|
|
111
121
|
]
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
124
|
```
|
|
115
125
|
|
|
126
|
+
> The App Group identifier in `ios.entitlements` and `expo-app-blocker.ios.appGroup` **must match** — they describe the same shared-storage container for the main app and the three extensions.
|
|
127
|
+
|
|
116
128
|
### 3. Use in your app
|
|
117
129
|
|
|
118
130
|
```tsx
|
|
@@ -214,6 +226,13 @@ No special setup required beyond what the config plugin handles automatically.
|
|
|
214
226
|
| `ios.shield.backgroundColor` | `string\|null` | `null` | Solid background color (hex). e.g. `"#f6f6f6"` for light, `"#1a1a2e"` for dark |
|
|
215
227
|
| `ios.shield.backgroundBlurStyle` | `string\|null` | `"systemThickMaterial"` | Blur style. See [Blur Styles](#blur-styles) for all options |
|
|
216
228
|
| `ios.shield.icon` | `string` | SF Symbol | Path to custom shield icon PNG (e.g. `"./assets/shield-icon.png"`) |
|
|
229
|
+
| `ios.shield.tempUnlockTitle` | `string` | `"Almost there!"` | Title shown briefly while ManagedSettings clears after a successful unlock |
|
|
230
|
+
| `ios.shield.tempUnlockSubtitle` | `string` | `"Your free time is loading. Try again in a moment."` | Subtitle for the temporary-unlock state |
|
|
231
|
+
| `ios.shield.tempUnlockButtonLabel` | `string` | `"OK"` | Button label for the temporary-unlock state |
|
|
232
|
+
| `ios.shield.countSuffix` | `string` | `" You have {count} apps blocked."` | Appended to the subtitle when more than one app is blocked. `{count}` is replaced with the integer at runtime. Set to `""` to drop the suffix entirely. |
|
|
233
|
+
| `ios.notification.title` | `string` | `"App Blocker"` | Title of the local notification fired when the user taps the Shield primary button. Set to a Hebrew/Arabic/etc. string to localize. |
|
|
234
|
+
| `ios.notification.body` | `string` | `"Tap to return to the app and complete the unlock challenge."` | Body of the unlock notification |
|
|
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). |
|
|
217
236
|
| `android.notificationTitle` | `string` | `"App Blocked"` | Notification title |
|
|
218
237
|
| `android.notificationText` | `string` | `"{appName} is blocked."` | Notification text |
|
|
219
238
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-blocker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.45",
|
|
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
|
@@ -285,6 +285,49 @@ function withAppBlockerIOS(config, pluginConfig) {
|
|
|
285
285
|
const blurRaw = shield.backgroundBlurStyle || (bgColorHex ? null : "systemThickMaterial");
|
|
286
286
|
const blurSwift = blurRaw && blurStyleMap[blurRaw] ? blurStyleMap[blurRaw] : null;
|
|
287
287
|
|
|
288
|
+
// Notification config (shown when the user taps the Shield primary button).
|
|
289
|
+
// All copy is configurable so non-English apps can localize without forking.
|
|
290
|
+
const notification = pluginConfig?.ios?.notification || {};
|
|
291
|
+
const notificationTitle = notification.title || "App Blocker";
|
|
292
|
+
const notificationBody = notification.body || "Tap to return to the app and complete the unlock challenge.";
|
|
293
|
+
// attachIcon defaults to true to preserve current behavior; set to false
|
|
294
|
+
// to drop the duplicate icon attachment so only the system app icon shows.
|
|
295
|
+
const notificationAttachIcon = notification.attachIcon === false ? "false" : "true";
|
|
296
|
+
|
|
297
|
+
// Temporary-unlock state copy (shown when the user has just earned time
|
|
298
|
+
// and the Shield is briefly visible while ManagedSettings clears).
|
|
299
|
+
const tempUnlockTitle = shield.tempUnlockTitle || "Almost there!";
|
|
300
|
+
const tempUnlockSubtitle = shield.tempUnlockSubtitle || "Your free time is loading. Try again in a moment.";
|
|
301
|
+
const tempUnlockButtonLabel = shield.tempUnlockButtonLabel || "OK";
|
|
302
|
+
|
|
303
|
+
// "You have N apps blocked" suffix appended to the subtitle when more
|
|
304
|
+
// than one app is blocked. Set countSuffix to "" to drop it entirely,
|
|
305
|
+
// or to a localized template like " יש לך {count} אפליקציות חסומות.".
|
|
306
|
+
// Defaults preserve the legacy English suffix.
|
|
307
|
+
const countSuffixTemplate = shield.countSuffix !== undefined
|
|
308
|
+
? shield.countSuffix
|
|
309
|
+
: " You have {count} apps blocked.";
|
|
310
|
+
|
|
311
|
+
// Swift string-literal escaping. Plugin substitutions land inside `"..."`
|
|
312
|
+
// literals so backslashes, quotes, and the Swift interpolation escape
|
|
313
|
+
// `\(` MUST all be escaped or the extension fails to compile.
|
|
314
|
+
function escapeSwiftString(s) {
|
|
315
|
+
return String(s)
|
|
316
|
+
.replace(/\\/g, "\\\\")
|
|
317
|
+
.replace(/"/g, '\\"')
|
|
318
|
+
.replace(/\n/g, "\\n")
|
|
319
|
+
.replace(/\r/g, "\\r");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Render the count suffix template into a Swift expression. We use
|
|
323
|
+
// `\(count)` interpolation when the template includes `{count}` so the
|
|
324
|
+
// runtime value is substituted. Empty template → empty literal.
|
|
325
|
+
function renderCountSuffixSwift(template) {
|
|
326
|
+
if (!template) return '""';
|
|
327
|
+
const escaped = escapeSwiftString(template);
|
|
328
|
+
return `"${escaped.replace(/\{count\}/g, "\\(count)")}"`;
|
|
329
|
+
}
|
|
330
|
+
|
|
288
331
|
// All placeholder replacements
|
|
289
332
|
const replacements = {
|
|
290
333
|
"APP_GROUP_PLACEHOLDER": appGroup,
|
|
@@ -292,6 +335,13 @@ function withAppBlockerIOS(config, pluginConfig) {
|
|
|
292
335
|
"SHIELD_SUBTITLE_PLACEHOLDER": shield.subtitle || "{appName} is blocked.",
|
|
293
336
|
"SHIELD_PRIMARY_BUTTON_PLACEHOLDER": shield.primaryButtonLabel || "Earn Free Time",
|
|
294
337
|
"SHIELD_SECONDARY_BUTTON_PLACEHOLDER": shield.secondaryButtonLabel === null ? "none" : (shield.secondaryButtonLabel || "Not now"),
|
|
338
|
+
"SHIELD_TEMP_UNLOCK_TITLE_PLACEHOLDER": tempUnlockTitle,
|
|
339
|
+
"SHIELD_TEMP_UNLOCK_SUBTITLE_PLACEHOLDER": tempUnlockSubtitle,
|
|
340
|
+
"SHIELD_TEMP_UNLOCK_BUTTON_PLACEHOLDER": tempUnlockButtonLabel,
|
|
341
|
+
"SHIELD_COUNT_SUFFIX_SWIFT_PLACEHOLDER": renderCountSuffixSwift(countSuffixTemplate),
|
|
342
|
+
"NOTIFICATION_TITLE_PLACEHOLDER": notificationTitle,
|
|
343
|
+
"NOTIFICATION_BODY_PLACEHOLDER": notificationBody,
|
|
344
|
+
"NOTIFICATION_ATTACH_ICON_PLACEHOLDER": notificationAttachIcon,
|
|
295
345
|
"SHIELD_PRIMARY_R_PLACEHOLDER": primaryColor.r,
|
|
296
346
|
"SHIELD_PRIMARY_G_PLACEHOLDER": primaryColor.g,
|
|
297
347
|
"SHIELD_PRIMARY_B_PLACEHOLDER": primaryColor.b,
|
|
@@ -7,6 +7,12 @@ class ShieldActionExtension: ShieldActionDelegate {
|
|
|
7
7
|
private let appGroupIdentifier = "APP_GROUP_PLACEHOLDER"
|
|
8
8
|
private let pendingUnlockKey = "appBlocker.pendingUnlock.v1"
|
|
9
9
|
private let pendingUnlockNotificationIdentifier = "expo.appblocker.pendingUnlock.local"
|
|
10
|
+
// Notification copy + behavior — configurable via plugin options so apps
|
|
11
|
+
// can localize without forking. Defaults preserve the original English
|
|
12
|
+
// copy and the icon attachment.
|
|
13
|
+
private let notificationTitle = "NOTIFICATION_TITLE_PLACEHOLDER"
|
|
14
|
+
private let notificationBody = "NOTIFICATION_BODY_PLACEHOLDER"
|
|
15
|
+
private let notificationAttachIcon = NOTIFICATION_ATTACH_ICON_PLACEHOLDER
|
|
10
16
|
|
|
11
17
|
override func handle(action: ShieldAction, for application: ApplicationToken, completionHandler: @escaping (ShieldActionResponse) -> Void) {
|
|
12
18
|
handleAction(action, completionHandler: completionHandler)
|
|
@@ -67,13 +73,15 @@ class ShieldActionExtension: ShieldActionDelegate {
|
|
|
67
73
|
let center = UNUserNotificationCenter.current()
|
|
68
74
|
|
|
69
75
|
let content = UNMutableNotificationContent()
|
|
70
|
-
content.title =
|
|
71
|
-
content.body =
|
|
76
|
+
content.title = notificationTitle
|
|
77
|
+
content.body = notificationBody
|
|
72
78
|
content.sound = .default
|
|
73
79
|
content.userInfo = ["link": "/unlock"]
|
|
74
80
|
|
|
75
|
-
// Attach the app icon to the notification
|
|
76
|
-
|
|
81
|
+
// Attach the app icon to the notification only when the app opted in.
|
|
82
|
+
// When false the system app icon is the only icon shown — avoids the
|
|
83
|
+
// "duplicate icon" look on iOS notification banners.
|
|
84
|
+
if notificationAttachIcon, let iconURL = iconFileURL() {
|
|
77
85
|
if let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
|
|
78
86
|
content.attachments = [attachment]
|
|
79
87
|
}
|
|
@@ -11,6 +11,11 @@ class ShieldConfigurationExtension: ShieldConfigurationDataSource {
|
|
|
11
11
|
private let shieldSubtitle = "SHIELD_SUBTITLE_PLACEHOLDER"
|
|
12
12
|
private let shieldPrimaryButtonLabel = "SHIELD_PRIMARY_BUTTON_PLACEHOLDER"
|
|
13
13
|
private let shieldSecondaryButtonLabel = "SHIELD_SECONDARY_BUTTON_PLACEHOLDER"
|
|
14
|
+
// Temporary-unlock state copy — shown briefly while ManagedSettings clears
|
|
15
|
+
// after a successful unlock. Configurable via plugin options.
|
|
16
|
+
private let shieldTempUnlockTitle = "SHIELD_TEMP_UNLOCK_TITLE_PLACEHOLDER"
|
|
17
|
+
private let shieldTempUnlockSubtitle = "SHIELD_TEMP_UNLOCK_SUBTITLE_PLACEHOLDER"
|
|
18
|
+
private let shieldTempUnlockButtonLabel = "SHIELD_TEMP_UNLOCK_BUTTON_PLACEHOLDER"
|
|
14
19
|
private let shieldPrimaryButtonColor = UIColor(red: SHIELD_PRIMARY_R_PLACEHOLDER, green: SHIELD_PRIMARY_G_PLACEHOLDER, blue: SHIELD_PRIMARY_B_PLACEHOLDER, alpha: 1.0)
|
|
15
20
|
private let shieldBackgroundColor: UIColor? = SHIELD_BG_COLOR_PLACEHOLDER
|
|
16
21
|
private let shieldBlurStyle: UIBlurEffect.Style? = SHIELD_BLUR_STYLE_PLACEHOLDER
|
|
@@ -44,16 +49,18 @@ class ShieldConfigurationExtension: ShieldConfigurationDataSource {
|
|
|
44
49
|
backgroundBlurStyle: shieldBlurStyle,
|
|
45
50
|
backgroundColor: shieldBackgroundColor,
|
|
46
51
|
icon: mascotIcon,
|
|
47
|
-
title: ShieldConfiguration.Label(text:
|
|
48
|
-
subtitle: ShieldConfiguration.Label(text:
|
|
49
|
-
primaryButtonLabel: ShieldConfiguration.Label(text:
|
|
52
|
+
title: ShieldConfiguration.Label(text: shieldTempUnlockTitle, color: shieldTitleColor),
|
|
53
|
+
subtitle: ShieldConfiguration.Label(text: shieldTempUnlockSubtitle, color: shieldSubtitleColor),
|
|
54
|
+
primaryButtonLabel: ShieldConfiguration.Label(text: shieldTempUnlockButtonLabel, color: .white),
|
|
50
55
|
primaryButtonBackgroundColor: shieldPrimaryButtonColor,
|
|
51
56
|
secondaryButtonLabel: nil
|
|
52
57
|
)
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
let count = getBlockedAppCount()
|
|
56
|
-
|
|
61
|
+
// The plugin replaces this placeholder with a Swift string literal
|
|
62
|
+
// containing `\(count)` interpolation, or `""` when the user opted out.
|
|
63
|
+
let context = count > 1 ? SHIELD_COUNT_SUFFIX_SWIFT_PLACEHOLDER : ""
|
|
57
64
|
let subtitle = shieldSubtitle.replacingOccurrences(of: "{appName}", with: appName) + context
|
|
58
65
|
|
|
59
66
|
let hasSecondary = !shieldSecondaryButtonLabel.isEmpty && shieldSecondaryButtonLabel != "none"
|