{"id":48918516,"url":"https://github.com/eylonshm/expo-app-blocker","last_synced_at":"2026-04-19T06:01:45.790Z","repository":{"id":350871554,"uuid":"1208595929","full_name":"eylonshm/expo-app-blocker","owner":"eylonshm","description":"Cross-platform Expo module for app blocking - IOS + Android.","archived":false,"fork":false,"pushed_at":"2026-04-13T22:40:08.000Z","size":1306,"stargazers_count":16,"open_issues_count":2,"forks_count":2,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-18T05:03:00.080Z","etag":null,"topics":["android","app-blocker","expo","ios","screen-time","screen-time-management"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/eylonshm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-12T13:56:17.000Z","updated_at":"2026-04-17T20:44:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/eylonshm/expo-app-blocker","commit_stats":null,"previous_names":["eylonshm/expo-app-blocker"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/eylonshm/expo-app-blocker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eylonshm%2Fexpo-app-blocker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eylonshm%2Fexpo-app-blocker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eylonshm%2Fexpo-app-blocker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eylonshm%2Fexpo-app-blocker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eylonshm","download_url":"https://codeload.github.com/eylonshm/expo-app-blocker/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eylonshm%2Fexpo-app-blocker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31996445,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T20:23:30.271Z","status":"online","status_checked_at":"2026-04-19T02:00:07.110Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","app-blocker","expo","ios","screen-time","screen-time-management"],"created_at":"2026-04-17T04:04:08.744Z","updated_at":"2026-04-19T06:01:45.737Z","avatar_url":"https://github.com/eylonshm.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# expo-app-blocker\n\nCross-platform app blocking module for Expo. Block other apps and redirect users to your app.\n\n**Android**: UsageStatsManager + Foreground Service + System Overlay\n**iOS**: Screen Time API (FamilyControls + ManagedSettings + DeviceActivity)\n\n## Demo\n\nhttps://github.com/user-attachments/assets/37f34797-6b92-40d5-911a-90c40e9ffaaa\n\n\n\u003e **iOS requires Apple Developer Portal setup before building.** See [Prerequisites](#prerequisites) for details.\n\n\u003e [!IMPORTANT]\n\u003e **Submit your Family Controls distribution approval request now.** App Store distribution requires Apple approval per bundle ID — it can take days to weeks and you can't ship without it. [Request here](https://developer.apple.com/contact/request/family-controls-distribution) (you'll need to submit once per bundle ID — 4 total).\n\u003e\n\u003e **While waiting for approval**, use the **Family Controls (Development)** capability in Xcode instead of the standard \"Family Controls\" — it's marked \"Development only\" in Xcode's Signing \u0026 Capabilities tab and works without Apple's approval. Development builds with this entitlement are fully functional on device but cannot be submitted to TestFlight or the App Store.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eTable of Contents\u003c/strong\u003e\u003c/summary\u003e\n\n- [Features](#features)\n- [Build Compatibility](#build-compatibility)\n- [Quick Start](#quick-start)\n- [Prerequisites](#prerequisites)\n- [Plugin Options](#plugin-options)\n  - [Blur Styles](#blur-styles)\n  - [EAS Build](#eas-build)\n- [API Reference](#api-reference)\n  - [Permissions](#permissions)\n  - [Android: Permission Settings](#android-permission-settings)\n  - [Android: App Blocking](#android-app-blocking)\n  - [Android: Monitoring](#android-monitoring)\n  - [iOS: App Selection (Inline Picker)](#inline-picker-recommended)\n  - [iOS: App Selection (Modal Picker)](#modal-picker)\n  - [iOS: Block Configuration](#ios-block-configuration)\n  - [iOS: Temporary Unlock](#ios-temporary-unlock)\n  - [iOS: Shield Button Events](#ios-shield-button-events)\n  - [iOS: Blocked Apps List](#ios-blocked-apps-list)\n- [Full Example: iOS App Blocker](#full-example-ios-app-blocker)\n- [Platform Notes](#platform-notes)\n- [How It Works](#how-it-works)\n- [Contributing](CONTRIBUTING.md)\n\n\u003c/details\u003e\n\n## Features\n\n- Block specific apps from being used\n- Inline app picker - embed the iOS system app picker directly in your UI (like Duolingo)\n- Modal app picker - present the system picker as a sheet\n- Customizable iOS shield overlay (icon, title, subtitle, button text, colors, blur style)\n- Native view for rendering blocked app names/icons (Apple's opaque tokens)\n- Temporary unlock with timer\n- Auto-relock when unlock period expires (iOS DeviceActivityMonitor extension)\n- Notification when blocked app is detected\n- Persist blocked apps across app restarts\n- Automatic iOS extension target creation via `@bacons/apple-targets`\n- Full Expo Config Plugin - no manual native setup required\n\n## Build Compatibility\n\n| Build type | Supported | Notes |\n|---|---|---|\n| **Expo Go** | ❌ | Requires custom native modules — not available in Expo Go |\n| **Development build** (`expo-dev-client`) | ✅ | Fully supported — same setup as production |\n| **Local build** (`expo run:ios` / `expo run:android`) | ✅ | Fully supported |\n| **EAS Build** | ✅ | Fully supported — see [EAS Build config](#eas-build) |\n| **Production / App Store** | ✅ | Fully supported — iOS requires Apple approval first |\n\n**This plugin requires a development build or a production build.** If you're using Expo Go, you'll need to [create a development build](https://docs.expo.dev/develop/development-builds/introduction/) first:\n\n```bash\nnpx expo install expo-dev-client\nnpx expo run:ios --device    # or: eas build --profile development\n```\n\n## Quick Start\n\n### 1. Install\n\n```bash\nnpx expo install expo-app-blocker\n```\n\n### 2. Configure `app.json`\n\n```json\n{\n  \"expo\": {\n    \"scheme\": \"myapp\",\n    \"ios\": {\n      \"bundleIdentifier\": \"com.yourapp.id\",\n      \"appleTeamId\": \"YOUR_TEAM_ID\"\n    },\n    \"plugins\": [\n      [\"expo-app-blocker\", {\n        \"ios\": {\n          \"appGroup\": \"group.com.yourapp.blocker\",\n          \"shield\": {\n            \"title\": \"Hold on!\",\n            \"subtitle\": \"{appName} is blocked.\",\n            \"primaryButtonLabel\": \"Earn Free Time\",\n            \"primaryButtonColor\": \"#fb6107\",\n            \"backgroundColor\": \"#f6f6f6\",\n            \"backgroundBlurStyle\": \"systemThickMaterialLight\"\n          }\n        }\n      }]\n    ]\n  }\n}\n```\n\n### 3. Use in your app\n\n```tsx\nimport {\n  requestPermissions,\n  setBlockConfiguration,\n  clearAllBlocks,\n  temporaryUnlock,\n  FamilyActivityPickerView,\n  type FamilyActivityPickerSelectionEvent,\n} from 'expo-app-blocker';\n\nfunction AppBlockerScreen() {\n  const [selectionData, setSelectionData] = useState('');\n\n  // 1. Request Screen Time permission (call once)\n  const handleAuth = async () =\u003e {\n    const { allGranted } = await requestPermissions();\n    if (!allGranted) console.log('User denied Screen Time access');\n  };\n\n  // 2. Handle selection changes from the inline picker\n  const handleSelectionChange = async (event: FamilyActivityPickerSelectionEvent) =\u003e {\n    setSelectionData(event.selectionData);\n\n    if (event.items.length \u003e 0) {\n      // Apply blocks — shields appear immediately on selected apps\n      await setBlockConfiguration({ blockedItems: event.items, isActive: true });\n    } else {\n      clearAllBlocks();\n    }\n  };\n\n  return (\n    \u003cView\u003e\n      {/* Inline app picker — renders the iOS system picker in your UI */}\n      \u003cFamilyActivityPickerView\n        initialSelection={selectionData}\n        onSelectionChange={handleSelectionChange}\n        theme=\"light\"\n        style={{ height: 500 }}\n      /\u003e\n\n      {/* Unlock apps temporarily (e.g. after completing a quiz) */}\n      \u003cButton\n        title=\"Unlock for 15 minutes\"\n        onPress={() =\u003e temporaryUnlock(15)}\n      /\u003e\n    \u003c/View\u003e\n  );\n}\n```\n\n### 4. Build and run\n\n```bash\nnpx expo prebuild --clean\nnpx expo run:ios --device    # physical device required for Screen Time APIs\nnpx expo run:android         # Android works on emulator\n```\n\n## Prerequisites\n\n### Apple Developer Portal (iOS)\n\n\u003e **Full step-by-step guide**: [docs/APPLE_DEVELOPER_SETUP.md](docs/APPLE_DEVELOPER_SETUP.md)\n\n1. Register **4 App IDs** with **Family Controls** and **App Groups** capabilities:\n   - `com.yourapp.id` (main app)\n   - `com.yourapp.id.DeviceActivityMonitor`\n   - `com.yourapp.id.ShieldAction`\n   - `com.yourapp.id.ShieldConfiguration`\n\n2. Create an **App Group**: `group.com.yourapp.blocker` (or your chosen identifier)\n\n3. Assign the App Group to all 4 App IDs\n\n4. Request **Family Controls** capability approval (required for App Store/TestFlight distribution)\n   - Submit the form **once per bundle ID** (4 total): [developer.apple.com/contact/request/family-controls-distribution](https://developer.apple.com/contact/request/family-controls-distribution)\n   - **While waiting for approval**: use **Family Controls (Development)** in Xcode's Signing \u0026 Capabilities tab — fully functional in dev builds, just not distributable\n   - Incomplete capability setup causes cryptic provisioning errors — make sure all 4 App IDs have Family Controls + App Groups enabled\n\n### Android\n\nNo special setup required beyond what the config plugin handles automatically.\n\n## Plugin Options\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `ios.appGroup` | `string` | Required | App Group identifier for shared data |\n| `ios.shield.title` | `string` | `\"Hold on!\"` | Shield overlay title |\n| `ios.shield.subtitle` | `string` | `\"{appName} is blocked.\"` | Shield subtitle. `{appName}` is replaced with the blocked app name |\n| `ios.shield.primaryButtonLabel` | `string` | `\"Earn Free Time\"` | Primary button text |\n| `ios.shield.secondaryButtonLabel` | `string\\|null` | `\"Not now\"` | Secondary button text. Set to `null` to hide |\n| `ios.shield.primaryButtonColor` | `string` | `\"#fb6107\"` | Primary button background color (hex) |\n| `ios.shield.titleColor` | `string` | `\"#111111\"` | Title text color (hex) |\n| `ios.shield.subtitleColor` | `string` | `\"#737373\"` | Subtitle text color (hex) |\n| `ios.shield.backgroundColor` | `string\\|null` | `null` | Solid background color (hex). e.g. `\"#f6f6f6\"` for light, `\"#1a1a2e\"` for dark |\n| `ios.shield.backgroundBlurStyle` | `string\\|null` | `\"systemThickMaterial\"` | Blur style. See [Blur Styles](#blur-styles) for all options |\n| `ios.shield.icon` | `string` | SF Symbol | Path to custom shield icon PNG (e.g. `\"./assets/shield-icon.png\"`) |\n| `android.notificationTitle` | `string` | `\"App Blocked\"` | Notification title |\n| `android.notificationText` | `string` | `\"{appName} is blocked.\"` | Notification text |\n\n### Blur Styles\n\n| Category | Values |\n|---|---|\n| Adaptive (auto light/dark) | `systemUltraThinMaterial`, `systemThinMaterial`, `systemMaterial`, `systemThickMaterial`, `systemChromeMaterial` |\n| Light only | `systemUltraThinMaterialLight`, `systemThinMaterialLight`, `systemMaterialLight`, `systemThickMaterialLight`, `systemChromeMaterialLight` |\n| Dark only | `systemUltraThinMaterialDark`, `systemThinMaterialDark`, `systemMaterialDark`, `systemThickMaterialDark`, `systemChromeMaterialDark` |\n| Legacy | `regular`, `prominent`, `light`, `dark`, `extraLight` |\n\nBoth `backgroundColor` and `backgroundBlurStyle` can be combined — the blur renders behind the color.\n\n### EAS Build\n\nFor EAS Build, declare extensions in `app.json` for credential management:\n\n```json\n{\n  \"extra\": {\n    \"eas\": {\n      \"build\": {\n        \"experimental\": {\n          \"ios\": {\n            \"appExtensions\": [\n              {\n                \"targetName\": \"DeviceActivityMonitor\",\n                \"bundleIdentifier\": \"com.yourapp.id.DeviceActivityMonitor\",\n                \"entitlements\": {\n                  \"com.apple.developer.family-controls\": true,\n                  \"com.apple.security.application-groups\": [\"group.com.yourapp.blocker\"]\n                }\n              },\n              {\n                \"targetName\": \"ShieldAction\",\n                \"bundleIdentifier\": \"com.yourapp.id.ShieldAction\",\n                \"entitlements\": {\n                  \"com.apple.developer.family-controls\": true,\n                  \"com.apple.security.application-groups\": [\"group.com.yourapp.blocker\"]\n                }\n              },\n              {\n                \"targetName\": \"ShieldConfiguration\",\n                \"bundleIdentifier\": \"com.yourapp.id.ShieldConfiguration\",\n                \"entitlements\": {\n                  \"com.apple.developer.family-controls\": true,\n                  \"com.apple.security.application-groups\": [\"group.com.yourapp.blocker\"]\n                }\n              }\n            ]\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n## API Reference\n\n### Permissions\n\n```typescript\nimport { getPermissionStatus, requestPermissions } from 'expo-app-blocker';\n\n// Check current status\nconst status = await getPermissionStatus();\n// { allGranted: boolean, details: AndroidPermissions | IOSPermissions }\n\n// Request permissions (iOS: Screen Time authorization, Android: no-op)\nconst result = await requestPermissions();\n```\n\n### Android: Permission Settings\n\n```typescript\nimport { openOverlaySettings, openUsageStatsSettings } from 'expo-app-blocker';\n\nopenOverlaySettings();     // \"Display over other apps\"\nopenUsageStatsSettings();  // \"Usage access\"\n```\n\n### Android: App Blocking\n\n```typescript\nimport { setBlockedApps, getBlockedApps, getInstalledApps } from 'expo-app-blocker';\n\nconst apps = await getInstalledApps();\n// [{ packageName: 'com.instagram.android', name: 'Instagram' }, ...]\n\nsetBlockedApps(['com.instagram.android', 'com.google.android.youtube']);\nconst blocked = getBlockedApps(); // ['com.instagram.android', ...]\n```\n\n### Android: Monitoring\n\n```typescript\nimport { startMonitoring, stopMonitoring } from 'expo-app-blocker';\n\nstartMonitoring();   // Start foreground service (auto-started on init)\nstopMonitoring();    // Stop monitoring\n```\n\n### iOS: App Selection\n\nTwo ways to let users pick which apps to block:\n\n#### Inline Picker (Recommended)\n\nEmbeds Apple's `FamilyActivityPicker` directly in your UI — the same approach Duolingo and other Screen Time apps use. The picker renders as a searchable native view with app and category lists.\n\n```tsx\nimport { FamilyActivityPickerView, setBlockConfiguration } from 'expo-app-blocker';\n\n\u003cFamilyActivityPickerView\n  initialSelection={selectionData}\n  onSelectionChange={async (event) =\u003e {\n    setSelectionData(event.selectionData); // save for next mount\n    await setBlockConfiguration({ blockedItems: event.items, isActive: true });\n  }}\n  theme=\"light\"\n  style={{ height: 500 }}\n/\u003e\n```\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|---|---|---|---|\n| `initialSelection` | `string` | — | Base64-encoded selection from a previous `selectionData`. Restores prior selection on mount |\n| `onSelectionChange` | `(event) =\u003e void` | — | Fires each time the user toggles an app or category |\n| `theme` | `\"light\" \\| \"dark\" \\| \"system\"` | `\"system\"` | Forces the picker's color scheme |\n| `style` | `ViewStyle` | `{ minHeight: 400 }` | Set an explicit `height` for best results |\n\n**`onSelectionChange` event:**\n\n| Field | Type | Description |\n|---|---|---|\n| `items` | `IOSBlockedItem[]` | Selected apps/categories — pass directly to `setBlockConfiguration()` |\n| `totalApps` | `number` | Number of individual apps selected |\n| `totalCategories` | `number` | Number of categories selected |\n| `selectionData` | `string` | Base64 string — save and pass back as `initialSelection` |\n\n#### Modal Picker\n\nOpens the system picker as a modal sheet. Returns items on \"Done\", rejects on cancel.\n\n```typescript\nimport { presentFamilyActivityPicker } from 'expo-app-blocker';\n\ntry {\n  const items = await presentFamilyActivityPicker();\n  await setBlockConfiguration({ blockedItems: items, isActive: true });\n} catch (e) {\n  // User cancelled\n}\n```\n\n### iOS: Block Configuration\n\n```typescript\nimport { setBlockConfiguration, getBlockConfiguration, clearAllBlocks } from 'expo-app-blocker';\n\n// Apply blocks (shields appear on selected apps)\nawait setBlockConfiguration({\n  blockedItems: items, // from picker\n  isActive: true,\n});\n\n// Get current configuration\nconst config = getBlockConfiguration();\n\n// Remove all blocks\nclearAllBlocks();\n```\n\n### iOS: Temporary Unlock\n\n```typescript\nimport {\n  temporaryUnlock,\n  isTemporarilyUnlocked,\n  getRemainingUnlockTime,\n  relockApps,\n} from 'expo-app-blocker';\n\n// Unlock for N minutes (removes shields temporarily)\nconst result = await temporaryUnlock(15);\n// { unlocked: boolean, expiresAt: number }\n\nconst unlocked = isTemporarilyUnlocked(); // boolean\nconst seconds = getRemainingUnlockTime(); // seconds remaining\nawait relockApps();                        // re-lock immediately\n```\n\n### iOS: Shield Button Events\n\nWhen a user taps the primary button on the shield overlay, your app receives an event:\n\n```typescript\nimport { addPendingUnlockListener, checkAndClearPendingUnlock } from 'expo-app-blocker';\n\n// Check if button was tapped while app was closed\nconst hasPending = checkAndClearPendingUnlock();\n\n// Listen for real-time taps\nconst subscription = addPendingUnlockListener(() =\u003e {\n  // Navigate to your unlock/quiz screen\n  router.push('/unlock');\n});\n\n// Clean up\nsubscription?.remove();\n```\n\n### iOS: Blocked Apps List\n\nRenders blocked app tokens with their real names and icons using Apple's native `Label` view. Since iOS tokens are opaque, this is the only way to display app names/icons outside the picker.\n\n```tsx\nimport { BlockedAppsNativeList } from 'expo-app-blocker';\n\n\u003cBlockedAppsNativeList\n  items={blockedItems}\n  selectionData={selectionBase64}\n  style={{ minHeight: 200 }}\n/\u003e\n```\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|---|---|---|---|\n| `items` | `IOSBlockedItem[]` | Required | Blocked items from picker |\n| `selectionData` | `string` | — | Base64 selection for accurate rendering |\n| `style` | `ViewStyle` | `{ minHeight: 50 }` | Standard style |\n\n## Full Example: iOS App Blocker\n\nA complete example showing permissions, inline picker, blocking, and temporary unlock:\n\n```tsx\nimport { useState, useEffect, useCallback } from 'react';\nimport { View, Text, TouchableOpacity, Platform, StyleSheet } from 'react-native';\nimport {\n  getPermissionStatus,\n  requestPermissions,\n  setBlockConfiguration,\n  getBlockConfiguration,\n  clearAllBlocks,\n  temporaryUnlock,\n  isTemporarilyUnlocked,\n  getRemainingUnlockTime,\n  relockApps,\n  addPendingUnlockListener,\n  checkAndClearPendingUnlock,\n  FamilyActivityPickerView,\n  type PermissionStatus,\n  type IOSBlockedItem,\n  type FamilyActivityPickerSelectionEvent,\n} from 'expo-app-blocker';\n\nexport default function BlockerScreen() {\n  const [permissions, setPermissions] = useState\u003cPermissionStatus | null\u003e(null);\n  const [blockedApps, setBlockedApps] = useState\u003cIOSBlockedItem[]\u003e([]);\n  const [selectionData, setSelectionData] = useState('');\n  const [unlocked, setUnlocked] = useState(false);\n\n  // Load permissions and existing blocks on mount\n  useEffect(() =\u003e {\n    getPermissionStatus().then(setPermissions);\n    const config = getBlockConfiguration();\n    if (config?.blockedItems?.length) {\n      setBlockedApps(config.blockedItems);\n    }\n  }, []);\n\n  // Listen for shield button taps\n  useEffect(() =\u003e {\n    if (checkAndClearPendingUnlock()) {\n      // User tapped shield button while app was closed\n    }\n    const sub = addPendingUnlockListener(() =\u003e {\n      // User tapped shield button — show your unlock UI\n    });\n    return () =\u003e sub?.remove();\n  }, []);\n\n  // Handle inline picker selection\n  const handleSelectionChange = async (event: FamilyActivityPickerSelectionEvent) =\u003e {\n    const items = event.items.filter(i =\u003e i.type !== 'summary');\n    setBlockedApps(items);\n    setSelectionData(event.selectionData);\n\n    if (items.length \u003e 0) {\n      await setBlockConfiguration({ blockedItems: items, isActive: true });\n    } else {\n      clearAllBlocks();\n    }\n  };\n\n  if (Platform.OS !== 'ios') return null;\n\n  return (\n    \u003cView style={styles.container}\u003e\n      {/* Permission request */}\n      {!permissions?.allGranted \u0026\u0026 (\n        \u003cTouchableOpacity\n          style={styles.button}\n          onPress={async () =\u003e {\n            const result = await requestPermissions();\n            setPermissions(result);\n          }}\n        \u003e\n          \u003cText style={styles.buttonText}\u003eEnable Screen Time\u003c/Text\u003e\n        \u003c/TouchableOpacity\u003e\n      )}\n\n      {/* Inline app picker */}\n      {permissions?.allGranted \u0026\u0026 (\n        \u003cView style={styles.pickerContainer}\u003e\n          \u003cFamilyActivityPickerView\n            initialSelection={selectionData}\n            onSelectionChange={handleSelectionChange}\n            theme=\"light\"\n            style={{ height: 500 }}\n          /\u003e\n        \u003c/View\u003e\n      )}\n\n      {/* Actions */}\n      {blockedApps.length \u003e 0 \u0026\u0026 (\n        \u003cView style={styles.actions}\u003e\n          \u003cText\u003e{blockedApps.length} apps blocked\u003c/Text\u003e\n\n          \u003cTouchableOpacity\n            style={styles.button}\n            onPress={async () =\u003e {\n              await temporaryUnlock(15);\n              setUnlocked(true);\n            }}\n          \u003e\n            \u003cText style={styles.buttonText}\u003eUnlock 15 min\u003c/Text\u003e\n          \u003c/TouchableOpacity\u003e\n\n          \u003cTouchableOpacity\n            style={styles.button}\n            onPress={() =\u003e { clearAllBlocks(); setBlockedApps([]); }}\n          \u003e\n            \u003cText style={styles.buttonText}\u003eClear All\u003c/Text\u003e\n          \u003c/TouchableOpacity\u003e\n        \u003c/View\u003e\n      )}\n    \u003c/View\u003e\n  );\n}\n\nconst styles = StyleSheet.create({\n  container: { flex: 1, padding: 16 },\n  pickerContainer: { borderRadius: 16, overflow: 'hidden', borderWidth: 1, borderColor: '#e8e8e8' },\n  actions: { marginTop: 16, gap: 12 },\n  button: { backgroundColor: '#fb6107', padding: 16, borderRadius: 12, alignItems: 'center' },\n  buttonText: { color: '#fff', fontWeight: '700', fontSize: 16 },\n});\n```\n\n## Platform Notes\n\n### iOS Limitations\n\n- **Physical device required** - Screen Time APIs don't work in the simulator\n- **App tokens are opaque** - You cannot extract app names/bundle IDs from tokens. Use `BlockedAppsNativeList` or `FamilyActivityPickerView` to display them\n- **FamilyActivityPicker is required** - No API to enumerate installed apps on iOS\n- **Shield customization is limited** - Only icon, title, subtitle, button labels, and colors can be changed. No custom views, fonts, or animations\n- **Cannot open apps from shield** - Use notifications as a workaround to redirect users to your app\n- **Permission status may lag** - After a user grants or revokes Screen Time access outside your app, the status may not update until the app is restarted. Re-check on app foreground\n- **Picker may crash on large categories** - The native `FamilyActivityPicker` can crash when scrolling through very large app categories. Consider providing fallback UI (e.g. a retry button) if this affects your users\n\n### Android Limitations\n\n- **~500ms detection delay** - The foreground polling interval means a blocked app is briefly visible before the overlay appears\n- **Overlay permission requires manual grant** - Users must enable \"Display over other apps\" in system settings\n- **Usage access permission requires manual grant** - Users must enable in system settings\n- **OEM battery optimizations** - Some manufacturers (Xiaomi, Samsung, etc.) may kill the foreground service. Users may need to disable battery optimization for your app\n\n### Android Permissions (auto-added by config plugin)\n\n| Permission | Purpose |\n|---|---|\n| `SYSTEM_ALERT_WINDOW` | Display blocking overlay |\n| `FOREGROUND_SERVICE` | Run monitoring service |\n| `FOREGROUND_SERVICE_SPECIAL_USE` | Required for Android 14+ |\n| `PACKAGE_USAGE_STATS` | Detect foreground app |\n| `RECEIVE_BOOT_COMPLETED` | Auto-start service on boot |\n| `POST_NOTIFICATIONS` | Show blocked app notifications |\n\n## How It Works\n\n### Android Flow\n\n1. `ExpoAppBlockerModule` starts `AppBlockerService` as a foreground service\n2. Service polls `UsageStatsManager` every 500ms to detect the foreground app\n3. If the foreground app is in the blocked list:\n   - A full-screen overlay covers the screen\n   - A notification is sent with a deep link to your app\n   - Your app is brought to the foreground\n4. Blocked apps are persisted in SharedPreferences\n\n### iOS Flow\n\n1. User authorizes Screen Time via `requestPermissions()`\n2. User selects apps to block — inline via `\u003cFamilyActivityPickerView\u003e` or modal via `presentFamilyActivityPicker()`\n3. `setBlockConfiguration()` applies shields via `ManagedSettingsStore`\n4. When a blocked app is opened, iOS shows the shield overlay (customized via config plugin)\n5. When the user taps the shield button, `ShieldActionExtension` sends a notification\n6. Your app receives the event via `addPendingUnlockListener()` and can navigate to an unlock flow\n7. `temporaryUnlock()` removes shields for a duration\n8. `DeviceActivityMonitor` extension re-applies shields when the unlock period expires\n\n## Contributing\n\nContributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions, project structure, and guidelines.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feylonshm%2Fexpo-app-blocker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feylonshm%2Fexpo-app-blocker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feylonshm%2Fexpo-app-blocker/lists"}