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-flic2

v0.3.2

Published

Expo module for Flic2 Bluetooth buttons

Readme

expo-flic2

Expo module for integrating Flic2 Bluetooth buttons into your React Native / Expo app.

Supports iOS and Android. Provides a typed API for scanning, connecting, and reacting to button events (click, double-click, hold, and press/release).

Installation

npm install expo-flic2

iOS

Run npx pod-install after installing.

Add Bluetooth usage descriptions to your app.json (or app.config.js) — the config plugin handles this automatically when using Expo managed workflow:

{
  "expo": {
    "plugins": ["expo-flic2"]
  }
}

For bare React Native projects, add to ios/MyApp/Info.plist:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to connect to Flic2 buttons.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app uses Bluetooth to connect to Flic2 buttons.</string>

Android

The module requires the following permissions in AndroidManifest.xml:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

The config plugin adds these automatically for managed Expo projects.

Quick Start

import { useEffect } from "react";
import {
  initialize,
  startScan,
  stopScan,
  addOnScanListener,
  addOnClickListener,
  addOnConnectionListener,
  connectButton,
} from "expo-flic2";

export default function App() {
  useEffect(() => {
    // 1. Initialize the Flic2 Bluetooth manager
    initialize();

    // 2. Listen for button events
    const clickSub = addOnClickListener(({ uuid, queued, age }) => {
      console.log(`Button ${uuid} clicked (age: ${age}ms)`);
    });

    const connSub = addOnConnectionListener(({ uuid, state, error }) => {
      console.log(`Button ${uuid} connection state: ${state}`);
      if (error) console.error("Connection error:", error);
    });

    // 3. Scan for nearby buttons
    const scanSub = addOnScanListener(({ isScanning, button, error }) => {
      if (error) {
        console.error("Scan error:", error);
        return;
      }
      if (button) {
        console.log("Discovered button:", button.name, button.uuid);
        // Connect as soon as it's found
        connectButton(button.uuid);
      }
    });

    startScan();

    return () => {
      clickSub.remove();
      connSub.remove();
      scanSub.remove();
      stopScan();
    };
  }, []);

  return <YourAppUI />;
}

API

Initialization

initialize()

Initialize the Flic2 Bluetooth manager. Call this once before using any other API, typically on app startup.

import { initialize } from "expo-flic2";

initialize();

Scanning

startScan()

Start scanning for nearby Flic2 buttons. Fires onFlic2Scan events as buttons are discovered.

stopScan()

Stop the active scan.

import { startScan, stopScan, addOnScanListener } from "expo-flic2";

const sub = addOnScanListener(({ isScanning, button, error, scanEvent }) => {
  if (button) {
    console.log("Found:", button.name, button.serialNumber);
  }
  if (!isScanning) {
    console.log("Scan stopped");
  }
});

startScan();

// Later...
stopScan();
sub.remove();

Button Management

getButtons(): Flic2Button[]

Returns all known (previously paired or discovered) buttons.

import { getButtons } from "expo-flic2";

const buttons = getButtons();
buttons.forEach((btn) => {
  console.log(btn.name, btn.connectionState, btn.batteryLevel);
});

connectButton(uuid: string)

Connect to a button by its UUID.

disconnectButton(uuid: string)

Disconnect from a button without removing it from the known list.

forgetButton(uuid: string)

Disconnect and remove a button from the known list.

import { connectButton, disconnectButton, forgetButton } from "expo-flic2";

connectButton("some-uuid");
disconnectButton("some-uuid");
forgetButton("some-uuid"); // removes it entirely

setButtonTriggerMode(uuid: string, mode: Flic2TriggerMode)

Configure which gesture events a button fires. Use this to reduce latency — for example, if you only need single clicks, set Click mode so the button doesn't wait to rule out a double-click.

import { setButtonTriggerMode, Flic2TriggerMode } from "expo-flic2";

// Only fire click events
setButtonTriggerMode(uuid, Flic2TriggerMode.Click);

// Fire click and double-click events
setButtonTriggerMode(uuid, Flic2TriggerMode.ClickAndDoubleClick);

// Fire click and hold events
setButtonTriggerMode(uuid, Flic2TriggerMode.ClickAndHold);

// Fire all event types
setButtonTriggerMode(uuid, Flic2TriggerMode.ClickAndDoubleClickAndHold);

Event Listeners

All listeners return an EventSubscription — call .remove() to unsubscribe.

addOnClickListener(listener)

Fires when a button is single-clicked.

const sub = addOnClickListener(({ uuid, queued, age }) => {
  // queued: true if the event was stored locally while disconnected
  // age: how many ms ago the event occurred
  console.log(`Clicked: ${uuid}`);
});

addOnDoubleClickListener(listener)

Fires when a button is double-clicked. Requires Flic2TriggerMode.ClickAndDoubleClick or ClickAndDoubleClickAndHold.

const sub = addOnDoubleClickListener(({ uuid, queued, age }) => {
  console.log(`Double-clicked: ${uuid}`);
});

addOnHoldListener(listener)

Fires when a button is held. Requires Flic2TriggerMode.ClickAndHold or ClickAndDoubleClickAndHold.

const sub = addOnHoldListener(({ uuid, queued, age }) => {
  console.log(`Held: ${uuid}`);
});

addOnUpOrDownListener(listener)

Fires on every press and release, regardless of trigger mode.

const sub = addOnUpOrDownListener(({ uuid, isDown, queued, age }) => {
  console.log(`Button ${uuid} ${isDown ? "pressed" : "released"}`);
});

addOnConnectionListener(listener)

Fires when a button's connection state changes.

import { addOnConnectionListener, Flic2ConnectionState } from "expo-flic2";

const sub = addOnConnectionListener(({ uuid, state, error }) => {
  if (state === Flic2ConnectionState.Ready) {
    console.log(`${uuid} is ready to use`);
  } else if (state === Flic2ConnectionState.Disconnected && error) {
    console.error(`${uuid} disconnected with error: ${error}`);
  }
});

addOnScanListener(listener)

Fires during scanning with discovery updates.

const sub = addOnScanListener(({ isScanning, button, error, scanEvent }) => {
  // scanEvent (iOS only): "discovered" | "connected" | "verified" | "verificationFailed"
  if (button) console.log("Discovered:", button.name);
  if (error) console.error("Scan error:", error);
});

addOnBatteryListener(listener)

Fires when a button reports a battery level update.

const sub = addOnBatteryListener(({ uuid, level }) => {
  console.log(`Battery for ${uuid}: ${level}%`);
  if (level < 20) {
    alert("Flic2 battery is low!");
  }
});

addOnManagerStateListener(listener)

Fires when the device's Bluetooth state changes.

const sub = addOnManagerStateListener(({ state }) => {
  if (state === "poweredOff") {
    alert("Please enable Bluetooth to use Flic2 buttons.");
  }
});

Types

type Flic2Button = {
  uuid: string;
  bluetoothAddress: string;
  serialNumber: string;
  name: string;
  connectionState: Flic2ConnectionState;
  firmwareVersion: number;
  batteryLevel: number;
  pressCount: number;
  triggerMode: Flic2TriggerMode;
  isReady: boolean;
};

enum Flic2ConnectionState {
  Disconnected = "disconnected",
  Connecting = "connecting",
  Connected = "connected",
  Ready = "ready",
}

enum Flic2TriggerMode {
  Click = "click",
  ClickAndHold = "clickAndHold",
  ClickAndDoubleClick = "clickAndDoubleClick",
  ClickAndDoubleClickAndHold = "clickAndDoubleClickAndHold",
}

Complete Example: Button Controller Hook

import { useEffect, useState } from "react";
import {
  initialize,
  startScan,
  stopScan,
  getButtons,
  connectButton,
  setButtonTriggerMode,
  addOnManagerStateListener,
  addOnScanListener,
  addOnConnectionListener,
  addOnClickListener,
  addOnDoubleClickListener,
  addOnHoldListener,
  Flic2Button,
  Flic2TriggerMode,
  Flic2ConnectionState,
} from "expo-flic2";

export function useFlic2() {
  const [buttons, setButtons] = useState<Flic2Button[]>([]);
  const [isScanning, setIsScanning] = useState(false);
  const [bluetoothReady, setBluetoothReady] = useState(false);

  useEffect(() => {
    initialize();

    const managerSub = addOnManagerStateListener(({ state }) => {
      setBluetoothReady(state === "poweredOn");
    });

    const scanSub = addOnScanListener(({ isScanning, button }) => {
      setIsScanning(isScanning);
      if (button) {
        connectButton(button.uuid);
        setButtonTriggerMode(button.uuid, Flic2TriggerMode.ClickAndDoubleClickAndHold);
      }
    });

    const connSub = addOnConnectionListener(({ state }) => {
      if (state === Flic2ConnectionState.Ready) {
        setButtons(getButtons());
      }
    });

    return () => {
      managerSub.remove();
      scanSub.remove();
      connSub.remove();
    };
  }, []);

  const scan = () => {
    setIsScanning(true);
    startScan();
    // Auto-stop after 10 seconds
    setTimeout(() => stopScan(), 10_000);
  };

  return { buttons, isScanning, bluetoothReady, scan };
}

// Usage in a component:
function FlicController() {
  const { buttons, isScanning, bluetoothReady, scan } = useFlic2();

  useEffect(() => {
    const clickSub = addOnClickListener(({ uuid }) => {
      console.log("Click from", uuid);
    });
    const doubleSub = addOnDoubleClickListener(({ uuid }) => {
      console.log("Double-click from", uuid);
    });
    const holdSub = addOnHoldListener(({ uuid }) => {
      console.log("Hold from", uuid);
    });

    return () => {
      clickSub.remove();
      doubleSub.remove();
      holdSub.remove();
    };
  }, []);

  // ...
}

Contributing

Contributions are welcome! Please refer to the contributing guide.