expo-app-blocker 0.1.49 → 0.1.51
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 +5 -5
- package/package.json +1 -1
- package/plugin/src/index.js +71 -28
package/README.md
CHANGED
|
@@ -86,10 +86,9 @@ npx expo install expo-app-blocker
|
|
|
86
86
|
|
|
87
87
|
### 2. Configure `app.json`
|
|
88
88
|
|
|
89
|
-
> **
|
|
90
|
-
> 1. `ios.appleTeamId` — `@bacons/apple-targets` refuses to add the extension targets without it.
|
|
89
|
+
> **Two things are required on iOS** and skipping either produces a cryptic build failure:
|
|
90
|
+
> 1. `ios.appleTeamId` — `@bacons/apple-targets` (auto-registered by this plugin) refuses to add the extension targets without it.
|
|
91
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
92
|
|
|
94
93
|
```json
|
|
95
94
|
{
|
|
@@ -116,8 +115,7 @@ npx expo install expo-app-blocker
|
|
|
116
115
|
"backgroundBlurStyle": "systemThickMaterialLight"
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
|
-
}]
|
|
120
|
-
"@bacons/apple-targets"
|
|
118
|
+
}]
|
|
121
119
|
]
|
|
122
120
|
}
|
|
123
121
|
}
|
|
@@ -125,6 +123,8 @@ npx expo install expo-app-blocker
|
|
|
125
123
|
|
|
126
124
|
> 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
125
|
|
|
126
|
+
> **Monorepo note (pnpm / yarn workspaces):** `@bacons/apple-targets` is declared as a direct dependency of `expo-app-blocker` and is auto-registered by this plugin, so most monorepo setups work out of the box. If you ever see `Failed to load '@bacons/apple-targets'` during prebuild, add it as a direct dependency of your app (`pnpm add @bacons/apple-targets`) to defeat the workspace's resolver isolation.
|
|
127
|
+
|
|
128
128
|
### 3. Use in your app
|
|
129
129
|
|
|
130
130
|
```tsx
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-blocker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.51",
|
|
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
|
@@ -14,6 +14,32 @@ const {
|
|
|
14
14
|
const fs = require("fs");
|
|
15
15
|
const path = require("path");
|
|
16
16
|
|
|
17
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
// Privacy manifest helper
|
|
19
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
// Merges expo-app-blocker's required UserDefaults entry into config.ios.privacyManifests
|
|
22
|
+
// so Expo's built-in withPrivacyInfo plugin writes it to PrivacyInfo.xcprivacy on prebuild.
|
|
23
|
+
// Required for App Store submission: the blocker uses UserDefaults extensively for
|
|
24
|
+
// AppGroup state sharing between the main app and Shield/DeviceActivityMonitor extensions.
|
|
25
|
+
function mergeBlockerPrivacyManifest(config) {
|
|
26
|
+
const ios = config.ios ?? {};
|
|
27
|
+
const privacyManifests = ios.privacyManifests ?? {};
|
|
28
|
+
const apiTypes = [...(privacyManifests.NSPrivacyAccessedAPITypes ?? [])];
|
|
29
|
+
const TYPE = "NSPrivacyAccessedAPICategoryUserDefaults";
|
|
30
|
+
const REASON = "CA92.1";
|
|
31
|
+
const existing = apiTypes.find((t) => t.NSPrivacyAccessedAPIType === TYPE);
|
|
32
|
+
if (!existing) {
|
|
33
|
+
apiTypes.push({ NSPrivacyAccessedAPIType: TYPE, NSPrivacyAccessedAPITypeReasons: [REASON] });
|
|
34
|
+
} else if (!existing.NSPrivacyAccessedAPITypeReasons.includes(REASON)) {
|
|
35
|
+
existing.NSPrivacyAccessedAPITypeReasons = [...existing.NSPrivacyAccessedAPITypeReasons, REASON];
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
...config,
|
|
39
|
+
ios: { ...ios, privacyManifests: { ...privacyManifests, NSPrivacyAccessedAPITypes: apiTypes } },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
17
43
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
18
44
|
// Android
|
|
19
45
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -185,6 +211,8 @@ function withAppBlockerAndroid(config, pluginConfig) {
|
|
|
185
211
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
186
212
|
|
|
187
213
|
function withAppBlockerIOS(config, pluginConfig) {
|
|
214
|
+
config = mergeBlockerPrivacyManifest(config);
|
|
215
|
+
|
|
188
216
|
const bundleId = config.ios?.bundleIdentifier || "expo.app-blocker";
|
|
189
217
|
const appGroup = pluginConfig?.ios?.appGroup || `group.${bundleId}`;
|
|
190
218
|
|
|
@@ -201,6 +229,46 @@ function withAppBlockerIOS(config, pluginConfig) {
|
|
|
201
229
|
return config;
|
|
202
230
|
});
|
|
203
231
|
|
|
232
|
+
// Populate `targets/` synchronously at config-eval time so the
|
|
233
|
+
// `@bacons/apple-targets` plugin (registered just below) can glob the
|
|
234
|
+
// directory and register the Shield/DeviceActivityMonitor/ShieldConfiguration
|
|
235
|
+
// extensions on the first prebuild. Done synchronously because @bacons globs
|
|
236
|
+
// during config evaluation, before any withDangerousMod queued by this plugin
|
|
237
|
+
// would run.
|
|
238
|
+
const projectRoot = config._internal?.projectRoot;
|
|
239
|
+
if (projectRoot) {
|
|
240
|
+
const targetsDir = path.join(projectRoot, "targets");
|
|
241
|
+
const packageTargetsDir = path.resolve(__dirname, "..", "..", "targets");
|
|
242
|
+
if (fs.existsSync(packageTargetsDir)) {
|
|
243
|
+
for (const dir of fs.readdirSync(packageTargetsDir)) {
|
|
244
|
+
const srcDir = path.join(packageTargetsDir, dir);
|
|
245
|
+
const destDir = path.join(targetsDir, dir);
|
|
246
|
+
if (!fs.statSync(srcDir).isDirectory()) continue;
|
|
247
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
248
|
+
for (const file of fs.readdirSync(srcDir)) {
|
|
249
|
+
if (file.endsWith(".swift") || file === "expo-target.config.js") {
|
|
250
|
+
fs.copyFileSync(path.join(srcDir, file), path.join(destDir, file));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Auto-register `@bacons/apple-targets` so users don't have to add it to
|
|
258
|
+
// their app.json plugins array. Resolved from this package's own
|
|
259
|
+
// node_modules (declared dep), which also makes it work in pnpm/yarn
|
|
260
|
+
// workspaces where transitive plugins aren't hoisted into the app root.
|
|
261
|
+
try {
|
|
262
|
+
const withTargetsDir = resolve("@bacons/apple-targets/app.plugin");
|
|
263
|
+
config = withTargetsDir(config, {});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`[expo-app-blocker] Failed to load '@bacons/apple-targets'. In pnpm or ` +
|
|
267
|
+
`yarn-workspace monorepos, add it as a direct dependency of your app: ` +
|
|
268
|
+
`\`pnpm add @bacons/apple-targets\`. Original error: ${err.message}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
204
272
|
config = withDangerousMod(config, [
|
|
205
273
|
"ios",
|
|
206
274
|
(config) => {
|
|
@@ -243,35 +311,10 @@ function withAppBlockerIOS(config, pluginConfig) {
|
|
|
243
311
|
}
|
|
244
312
|
}
|
|
245
313
|
|
|
246
|
-
//
|
|
314
|
+
// Templates were copied to `targets/` at config-eval time (see
|
|
315
|
+
// withAppBlockerIOS). This block only resolves the directory for the
|
|
316
|
+
// placeholder-substitution and shield-icon-copy steps below.
|
|
247
317
|
const targetsDir = path.join(path.dirname(platformRoot), "targets");
|
|
248
|
-
const packageTargetsDir = path.resolve(__dirname, "..", "..", "targets");
|
|
249
|
-
if (fs.existsSync(packageTargetsDir)) {
|
|
250
|
-
function copyDirSync(src, dest) {
|
|
251
|
-
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
252
|
-
for (const entry of fs.readdirSync(src)) {
|
|
253
|
-
const srcPath = path.join(src, entry);
|
|
254
|
-
const destPath = path.join(dest, entry);
|
|
255
|
-
if (fs.statSync(srcPath).isDirectory()) {
|
|
256
|
-
copyDirSync(srcPath, destPath);
|
|
257
|
-
} else {
|
|
258
|
-
fs.copyFileSync(srcPath, destPath);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
// Only copy Swift files and config (preserve user's assets, generated entitlements, Info.plist)
|
|
263
|
-
for (const dir of fs.readdirSync(packageTargetsDir)) {
|
|
264
|
-
const srcDir = path.join(packageTargetsDir, dir);
|
|
265
|
-
const destDir = path.join(targetsDir, dir);
|
|
266
|
-
if (!fs.statSync(srcDir).isDirectory()) continue;
|
|
267
|
-
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
268
|
-
for (const file of fs.readdirSync(srcDir)) {
|
|
269
|
-
if (file.endsWith(".swift") || file === "expo-target.config.js") {
|
|
270
|
-
fs.copyFileSync(path.join(srcDir, file), path.join(destDir, file));
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
318
|
|
|
276
319
|
// Helper: hex to RGB floats
|
|
277
320
|
function hexToRgb(hex) {
|