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 CHANGED
@@ -86,10 +86,9 @@ 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.
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.49",
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",
@@ -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
- // Copy fresh target templates from node_modules before replacing placeholders
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) {