npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-blocker

v0.1.7

Published

App blocker for React Native/Expo with scheduling, app usage tracking, and customizable overlay UI

Readme

expo-blocker

Note: Currently Android only. iOS support coming soon (requires Apple Family Controls entitlement).

A production-ready app blocker for React Native / Expo with scheduling, app usage tracking, and customizable UI.

Features

  • Block specific apps or all non-system apps
  • Schedule blocking at specific times (HH:mm format) or date+time (yyyy-MM-dd HH:mm)
  • Exclude apps from being blocked
  • App usage statistics - track how much time you spend on apps
  • Customizable overlay UI - change title, message, colors, button styling
  • Button callback - handle button clicks on the overlay
  • Foreground service for reliable background operation
  • Boot persistence - auto-restart after device reboot
  • Get app icons and names for nice UI display

Installation

npx expo install expo-blocker

Or with npm:

npm install expo-blocker

Required Permissions

This module requires two special permissions that users must grant manually:

1. Usage Access (PACKAGE_USAGE_STATS)

Required to detect which app is currently in the foreground.

// Check permission
const hasUsagePermission = await AppBlocker.hasUsageStatsPermission();

// Request permission (opens system settings)
await AppBlocker.requestUsageStatsPermission();

2. Overlay Permission (SYSTEM_ALERT_WINDOW)

Required to display the blocking overlay on top of other apps.

// Check permission
const hasOverlayPermission = await AppBlocker.hasOverlayPermission();

// Request permission (opens system settings)
await AppBlocker.requestOverlayPermission();

Usage

Check Permissions

import AppBlocker from 'expo-blocker';

const permissions = await AppBlocker.checkPermissions();
console.log(permissions);
// { usageStats: true, overlay: false }

Block Specific Apps

// Block Instagram and Facebook
await AppBlocker.block(['com.instagram.android', 'com.facebook.katana']);

Block All Non-System Apps

await AppBlocker.block();

Exclude Apps (Whitelist)

// Apps in this list will never be blocked
await AppBlocker.block(null, ['com.yourapp.package', 'com.android.settings']);

Schedule Blocking

// Block apps starting at 9 PM
await AppBlocker.schedule('21:00');

// With excluded apps
await AppBlocker.schedule('21:00', ['com.yourapp.package']);

Schedule Blocking at Specific Date and Time

// Block apps starting at 9 AM tomorrow
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(9, 0, 0, 0); // 9:00 AM

const year = tomorrow.getFullYear();
const month = String(tomorrow.getMonth() + 1).padStart(2, '0');
const day = String(tomorrow.getDate()).padStart(2, '0');
const hours = String(tomorrow.getHours()).padStart(2, '0');
const minutes = String(tomorrow.getMinutes()).padStart(2, '0');

const dateTime = `${year}-${month}-${day} ${hours}:${minutes}`;
await AppBlocker.scheduleAt(dateTime);

// With excluded apps
await AppBlocker.scheduleAt(dateTime, ['com.yourapp.package']);

Clear Blocking

await AppBlocker.clear();

Get App Usage Statistics

// Get today's usage for all apps
const stats = await AppBlocker.getUsageStats();
console.log(stats);
// [
//   { packageName: 'com.instagram.android', appName: 'Instagram', usageTime: 3600000, usageTimeFormatted: '1h 0m', ... },
//   { packageName: 'com.facebook.katana', appName: 'Facebook', usageTime: 1800000, usageTimeFormatted: '30m', ... },
// ]

// Get usage for specific app
const usage = await AppBlocker.getAppUsageTime('com.instagram.android');
console.log(usage);
// { usageTime: 3600000, usageTimeFormatted: '1h 0m' }

Get App Info

// Get app name
const name = await AppBlocker.getAppName('com.instagram.android');
// 'Instagram'

// Get app icon as base64
const icon = await AppBlocker.getAppIcon('com.instagram.android');
// 'iVBORw0KGgoAAAANSUhEUgAAAAEAAA...'

Button Click Callback

Listen for button clicks on the overlay when users tap the custom button:

import { useButtonClickListener } from 'expo-blocker';

// In your component (hook-based)
useButtonClickListener((event) => {
  console.log('Button clicked!', event);
  // event = { packageName: 'com.instagram.android', action: 'open' }
  
  // Do something - open settings, log data, etc.
});

Or use the imperative API directly:

import { addButtonClickListener } from 'expo-blocker';

const subscription = addButtonClickListener((event) => {
  console.log('Button clicked for:', event.packageName);
});

// When done listening (e.g., component unmount)
subscription.remove();

Customize Overlay UI

await AppBlocker.updateOverlayConfig({
  title: 'App Blocked 🔒',
  message: 'Time to focus on something else!',
  description: 'You can customize this description.',
  backgroundColor: '#1A1A1A',
  textColor: '#FFFFFF',
  showAppIcon: true,
  showAppName: true,
  showTodayUsage: true,
});

Customize Button

await AppBlocker.updateOverlayConfig({
  buttonText: 'Open Settings',
  buttonLink: 'expo://home',        // Optional: Navigate to URL/scheme
  buttonColor: '#4CAF50',           // Hex color string
  buttonTextColor: '#FFFFFF',      // Hex color string
  buttonBorderRadius: 25,
  buttonWidth: 200,
  buttonHeight: 50,
  buttonMarginTop: 40,
});

Get/Set Excluded Apps

// Get current excluded apps
const excluded = await AppBlocker.getExcludeApps();

// Set excluded apps
await AppBlocker.setExcludeApps(['com.yourapp.package']);

Get Blocking State

const state = await AppBlocker.getState();
console.log(state);
// {
//   isBlocking: true,
//   blockedApps: ['com.instagram.android'],
//   blockAll: false,
//   scheduledTime: null,
//   scheduleActivated: false,
//   excludeApps: ['com.yourapp.package']
// }

Complete Example

import React, { useEffect, useState } from 'react';
import { Alert, Button, Text, View } from 'react-native';
import AppBlocker, { useButtonClickListener } from 'expo-blocker';

export default function App() {
  const [permissions, setPermissions] = useState(null);

  useEffect(() => {
    checkPermissions();
  }, []);

  useButtonClickListener((event) => {
    Alert.alert(
      'Button Clicked',
      `User tapped button for: ${event.packageName}`,
      [{ text: 'OK' }]
    );
  });

  const checkPermissions = async () => {
    const perms = await AppBlocker.checkPermissions();
    setPermissions(perms);
  };

  const startBlocking = async () => {
    await AppBlocker.updateOverlayConfig({
      title: 'Blocked 🔒',
      message: 'Time to focus!',
      backgroundColor: '#1A1A1A',
      textColor: '#FFFFFF',
      showAppIcon: true,
      showAppName: true,
      showTodayUsage: true,
      buttonText: 'Open Settings',
      buttonLink: 'expo://home',
      buttonColor: '#4CAF50',
      buttonTextColor: '#FFFFFF',
      buttonBorderRadius: 25,
    });

    await AppBlocker.block(['com.instagram.android'], ['com.yourapp.package']);
  };

  if (!permissions?.usageStats || !permissions?.overlay) {
    return (
      <View>
        <Text>Please grant permissions first!</Text>
        <Button title="Grant Usage Stats" onPress={() => AppBlocker.requestUsageStatsPermission()} />
        <Button title="Grant Overlay" onPress={() => AppBlocker.requestOverlayPermission()} />
      </View>
    );
  }

  return (
    <View>
      <Button title="Start Blocking" onPress={startBlocking} />
      <Button title="Clear" onPress={() => AppBlocker.clear()} />
    </View>
  );
}

API Reference

| Method | Parameters | Description | |--------|------------|-------------| | block(apps?, excludeApps?) | string[], string[] | Block apps. Pass null for all non-system apps | | blockAll(excludeApps?) | string[] | Block all non-system apps | | clear() | - | Stop all blocking | | schedule(time, excludeApps?) | string (HH:mm), string[] | Schedule blocking at time (today only) | | scheduleAt(dateTime, excludeApps?) | string (yyyy-MM-dd HH:mm), string[] | Schedule blocking at specific date and time | | getState() | - | Get current blocking state | | isBlocking() | - | Check if blocking is active | | checkPermissions() | - | Check all permissions | | hasUsageStatsPermission() | - | Check usage access permission | | hasOverlayPermission() | - | Check overlay permission | | requestUsageStatsPermission() | - | Open usage access settings | | requestOverlayPermission() | - | Open overlay settings | | getInstalledApps() | - | Get list of non-system apps | | getAppName(package) | string | Get app name from package | | getAppIcon(package) | string | Get app icon as base64 | | getUsageStats() | - | Get today's app usage stats | | getAppUsageTime(package) | string | Get usage for specific app | | setExcludeApps(apps) | string[] | Set apps to exclude | | getExcludeApps() | - | Get excluded apps list | | updateOverlayConfig(config) | OverlayConfig | Customize overlay UI | | getOverlayConfig() | - | Get current overlay config |

Event Listeners

| Function | Parameters | Description | |----------|------------|-------------| | useButtonClickListener(callback) | function | React hook to listen for button clicks | | addButtonClickListener(callback) | function | Imperative API - returns subscription with .remove() |

ButtonClickedEvent

{
  packageName: string,  // Package name of the blocked app
  action: string        // Action type (e.g., 'open')
}

OverlayConfig Options

Content Options

{
  title?: string,              // Default: "App Blocked"
  message?: string,            // Short message (optional)
  description?: string,       // Longer description (optional)
  backgroundColor?: string,   // Default: "#1A1A1A" (dark gray)
  textColor?: string,         // Default: "#FFFFFF" (white)
  titleTextSize?: number,     // Default: 32
  messageTextSize?: number,   // Default: 18
  descriptionTextSize?: number, // Default: 16
}

Display Options

{
  showAppIcon?: boolean,      // Show app icon (default: true)
  showAppName?: boolean,      // Show "AppName is blocked" (default: true)
  showTodayUsage?: boolean,   // Show today's usage time (default: false)
  showUsageStats?: boolean,   // Show usage stats (default: false)
}

Button Options

{
  buttonText?: string,           // Button label (no button if empty)
  buttonLink?: string,           // Optional URL/scheme to navigate to (e.g., 'expo://home', 'https://google.com')
  buttonColor?: string,          // Button background hex color (default: "#4CAF50")
  buttonTextColor?: string,      // Button text hex color (default: "#FFFFFF")
  buttonBorderRadius?: number,   // Border radius in pixels (default: 50)
  buttonWidth?: number,          // Button width in pixels (default: 280)
  buttonHeight?: number,         // Button height in pixels (default: 60)
  buttonMarginTop?: number,      // Space above button (default: 40)
}

Close Button Options

{
  showCloseButton?: boolean,     // Show close button (default: false)
  closeButtonColor?: string,     // Close button hex color (default: "#666666")
}

## AppUsageStat Object

```typescript
{
  packageName: string,           // Package name (e.g., 'com.instagram.android')
  appName: string,               // Display name (e.g., 'Instagram')
  iconBase64: string | null,     // Base64 encoded PNG icon
  usageTime: number,             // Time in milliseconds
  usageTimeFormatted: string,     // Formatted (e.g., '1h 30m')
  lastTimeUsed: number           // Timestamp of last use
}

Platform Support

  • Android: Fully supported
  • iOS: Coming soon (contact us if you need it)

Permissions Required

| Permission | Purpose | How to Grant | |------------|---------|--------------| | PACKAGE_USAGE_STATS | Detect foreground apps | Settings → Apps → Special access → Usage access | | SYSTEM_ALERT_WINDOW | Show blocking overlay | Settings → Apps → Special access → Display over other apps |

Quick Testing with ADB

If you're testing on an emulator or device connected via ADB, you can grant permissions programmatically:

# Get your app's package name (default: expo.modules.appblockerengine.example)
adb shell pm list packages | grep blocker

# Grant Usage Access permission
adb shell appops set expo.modules.appblockerengine.example PACKAGE_USAGE_STATS allow

# Grant Overlay permission
adb shell appops set expo.modules.appblockerengine.example SYSTEM_ALERT_WINDOW allow

# Restart the app to apply permissions
adb shell am force-stop expo.modules.appblockerengine.example
adb shell am start -n expo.modules.appblockerengine.example/.MainActivity

For your own app, replace expo.modules.appblockerengine.example with your package name.

License

MIT