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

react-native-floating-bubble-turbo

v0.3.0

Published

Android-only floating bubble overlay (Messenger chat-head style) exposed as a TurboModule for the New Architecture.

Readme

react-native-floating-bubble-turbo

Android-only floating bubble overlay (Messenger chat-head style) for React Native, exposed as a TurboModule. Requires the New Architecture and React Native 0.82+.

  • Solo Android (iOS devuelve no-ops / false).
  • Burbuja arrastrable sobre cualquier app, con notificación en primer plano.
  • Dos modos: al tocarla puede reabrir la app (reopen) o abrir un popup flotante con la app montada dentro (popup), navegando a un deep link.
  • Autocontenida: permisos, <service>, <activity>, estilos e ícono se inyectan solos por manifest merging. No hay que tocar el AndroidManifest.xml ni el res/values/styles.xml de la app que la consume.

Instalación

npm install react-native-floating-bubble-turbo

Funciona con la New Architecture (TurboModule). Asegúrate de que tu proyecto la tenga activada (newArchEnabled: true).

No hay pasos manuales

Con autolinking (lo normal desde RN 0.60, y siempre en Expo Dev Client):

  • El paquete se registra solo. No edites MainApplication ni MainActivity.
  • Los permisos SYSTEM_ALERT_WINDOW, FOREGROUND_SERVICE y FOREGROUND_SERVICE_SPECIAL_USE, el <service> y el <activity> del popup se fusionan desde el manifest de la librería.
  • FloatingPopupTheme y el ícono vienen como recursos de la librería.

Solo queda recompilar, porque es código nativo:

npx react-native run-android

Con Expo Dev Client:

npx expo prebuild
npx expo run:android

Si usas Expo sin dev client y con EAS Build, prebuild genera el proyecto nativo en el servidor y la librería se autolinkea igual. No hace falta config plugin.

Uso

import {
  type BubbleMode,
  checkOverlayPermission,
  hideBubble,
  isBubbleVisible,
  onBubblePress,
  onBubbleRemoved,
  requestOverlayPermission,
  sendAppToBackground,
  showBubble,
} from 'react-native-floating-bubble-turbo';

// 1) Pide el permiso (te manda a la pantalla de "Dibujar sobre otras apps")
if (!(await checkOverlayPermission())) {
  await requestOverlayPermission();
}

// 2) Muestra la burbuja en (x, y) con el modo por defecto: reopen
await showBubble(20, 200);

// 2b) Modo popup: abre una ventana flotante y navega al deep link
await showBubble(20, 200, 'popup', 'myapp://assistant-chat');

// 3) Escucha eventos
onBubblePress(() => {
  console.log('Burbuja tocada');
});
onBubbleRemoved(() => {
  console.log('Burbuja cerrada');
});

// 4) Ocúltala
await hideBubble();

Los dos modos

| Modo | Qué pasa al tocar la burbuja | Deep link | | --- | --- | --- | | reopen (default) | Reabre la app trayéndola a primer plano con FLAG_ACTIVITY_REORDER_TO_FRONT | se ignora | | popup | Abre BubblePopupActivity: una ventana flotante con la app montada dentro | navega a popupDeepLink |

El deep link se pasa a la Activity como extra del Intent y se reinyecta como android.intent.extra.INITIAL_URI, así que lo procesa el Linking de React Native. La librería no tiene ninguna ruta hardcodeada: es el deep link que le pases.

const mode: BubbleMode = 'popup';

Mostrar la burbuja al mandar la app a segundo plano

El caso típico es mostrarla cuando el usuario sale de la app. showBubble() debe llamarse después de que la app quede en background, porque el overlay necesita SYSTEM_ALERT_WINDOW y Android no concede permisos en background.

Para eso está useFloatingBubbleLifecycle. Móntalo una sola vez, en el layout del área principal de tu app:

// app/(main)/_layout.tsx
import { useFloatingBubbleLifecycle } from 'react-native-floating-bubble-turbo';
import MainDrawer from '@/features/core/components/layout/main/MainDrawer';

export default function MainLayout() {
  useFloatingBubbleLifecycle();
  return <MainDrawer />;
}

Opciones:

useFloatingBubbleLifecycle({
  x: 20,                                   // default 20
  y: 200,                                  // default 200
  mode: 'popup',                           // default 'reopen'
  popupDeepLink: 'myapp://assistant-chat', // default ''
  requestPermissionOnMount: true,          // default true
  enabled: isFeatureEnabled,               // default true
});

El hook pide el permiso de overlay al montarse, muestra la burbuja en active → background, la oculta en background → active, y también al desmontar el componente (para que no quede flotando al hacer logout).

No lo gatees por autenticación

Esto es la trampa más común al integrar esta librería, y no es específica de ella. Un gate así rompe la feature entera sin dar ningún error:

// MAL
const { isAuthenticated } = useAuth();
useEffect(() => {
  if (!isAuthenticated) return; // <-- mata el listener de AppState
  const sub = AppState.addEventListener('change', /* ... */);
  return () => sub.remove();
}, [isAuthenticated]);

Ese return temprano hace dos cosas:

  1. Nunca registra el listener de AppState, así que showBubble() no se llama jamás. La app compila, el módulo nativo funciona, y la burbuja no aparece nunca.
  2. Nunca pide el permiso de overlay, así que ni siquiera sale el diálogo de "Dibujar sobre otras apps", y no hay pista de qué falló.

El problema es que isAuthenticated es false en desarrollo mucho más de lo que uno espera. Mientras el login no valide contra backend, la app suele navegar al área principal con router.push sin autenticar, así que llegas ahí con isAuthenticated === false y la feature queda muerta en silencio.

Si lo que quieres es "solo para quien puede iniciar sesión", exprésalo con enabled, no gateando el montaje:

useFloatingBubbleLifecycle({ enabled: isFeatureEnabledForThisUser });

Y montalo en el layout del área autenticada, no dentro de una pantalla condicional: el hook se encarga solo de limpiarse al desmontarse.

Si prefieres hacerlo a mano

import { AppState, showBubble, hideBubble } from 'react-native-floating-bubble-turbo';

useEffect(() => {
  const sub = AppState.addEventListener('change', (next) => {
    if (AppState.currentState === 'active' && next === 'background') {
      showBubble(20, 200, 'reopen');
    }
    if (next === 'active') hideBubble();
  });
  return () => {
    sub.remove();
    hideBubble();
  };
}, []);

Registra el listener fuera de toda guarda. Si necesitas condicionar, usa enabled de useFloatingBubbleLifecycle.

API

| Función | Tipo | Descripción | | --- | --- | --- | | checkOverlayPermission() | Promise<boolean> | ¿Se puede dibujar sobre otras apps? | | requestOverlayPermission() | Promise<boolean> | Abre los ajustes del permiso. Resuelve true si ya estaba otorgado. | | showBubble(x?, y?, mode?, popupDeepLink?) | Promise<void> | Muestra la burbuja. Default x=0, y=100, mode='reopen', popupDeepLink=''. Rechaza con NO_PERMISSION si falta el permiso. | | hideBubble() | Promise<void> | Oculta la burbuja y emite onBubbleRemoved. | | isBubbleVisible() | Promise<boolean> | ¿Hay burbuja en pantalla? | | sendAppToBackground() | Promise<void> | Manda la app al inicio (botón Home), sin matarla. Rechaza con NO_ACTIVITY o NO_HOME. | | onBubblePress(cb) | EmitterSubscription \| undefined | Tap en la burbuja. | | onBubbleRemoved(cb) | EmitterSubscription \| undefined | Burbuja cerrada. |

BubbleMode es 'reopen' | 'popup'.

useFloatingBubbleLifecycle(options?) devuelve void. Las opciones son enabled, x, y, mode, popupDeepLink y requestPermissionOnMount, todas opcionales.

Cómo mandar la app al inicio sin matarla

Si querés un botón "volver al inicio" para disparar la burbuja a mano (típico para probar), usá sendAppToBackground():

import { sendAppToBackground } from 'react-native-floating-bubble-turbo';

await sendAppToBackground(); // el launcher queda al frente

No lo implementes con Linking.openURL. El truco intent://#Intent;action=android.intent.action.MAIN;category=...HOME;end no funciona en RN 0.86.3: IntentModule.openURL construye Intent(ACTION_VIEW, Uri.parse(url)) y nunca llama a Intent.parseUri (IntentModule.kt:124), así que el fragmento #Intent;... se ignora y Android falla con No Activity found to handle Intent { act=VIEW dat=intent:// }. Linking.sendIntent tampoco sirve: hace Intent(action) sin categoría (IntentModule.kt:207) y un intent con ACTION_MAIN y cero categorías no matchea un filtro {HOME, DEFAULT}.

Tampoco uses BackHandler.exitApp(): destruye la Activity y el foreground service de la burbuja muere con ella, porque vive en el proceso de la app.

requestOverlayPermission() lanza la Activity de ajustes con startActivityForResult. El resultado llega por el onActivityResult que RN reenvía automáticamente a los ActivityEventListener registrados en el ReactContext, así que no hace falta tocar MainActivity. Si el usuario vuelve sin cambiar nada, checkOverlayPermission() tras volver a la app sigue siendo la fuente de verdad.

Example

Dentro del repo:

yarn
yarn example android

La app de ejemplo tiene un selector para probar los dos modos y un log de los eventos. (Necesitas dispositivo o emulador; example/ no es Expo.)

Estado y límites

  • Solo Android; en iOS las funciones son no-ops / false.
  • minSdkVersion 24, compileSdkVersion 36.
  • El Service es un Foreground Service con foregroundServiceType="specialUse". Android permite que el sistema lo mate, y en Xiaomi/Huawei la optimización de batería es especialmente agresiva. Si la burbuja desaparece sola, eso es el sistema, no la librería.
  • El ícono de la burbuja es res/drawable-nodpi/bubble_icon.png y el layout es res/layout/bubble_layout.xml. Para cambiarlo en tu app, sobreescribe el recurso con el mismo nombre en tu módulo, o haz un fork.
  • BubblePopupActivity usa "main" como nombre del componente raíz, que es el default de Expo con expo-router. Intenta leer getMainComponentName() del Application del host por reflection, pero en la práctica eso casi nunca funciona: el método lo declara el Activity, no el Application, así que la reflexión cae al default. Si tu app raíz no se llama "main", sobreescribí getMainComponentName() en una subclase.
  • El popup del modo popup monta una segunda instancia de la app. Eso implica más memoria y un cold start de React. Es el modo más caro; para la mayoría de los casos reopen es suficiente.
  • getMainComponentName() debe devolver el mismo nombre que usás para registrar la app, o el popup del modo popup monta una pantalla vacía. Con expo-router es "main", que es el default, así que normalmente funciona.

Con expo-router: no pongas el host dentro de app/

Todo archivo dentro de app/ es una route para expo-router. Si creás un componente tipo "host" dentro de ese directorio, expo-router lo registra como screen navegable y, si no tiene export default, se queja al arrancar:

WARN  Route "./(main)/FloatingBubbleHost.tsx" is missing the required default
export. Ensure a React component is exported as default.

Mountalo desde src/ y dejá que el _layout.tsx lo use:

// src/features/…/FloatingBubbleHost.tsx  (fuera de app/)
import { useFloatingBubbleLifecycle } from 'react-native-floating-bubble-turbo';

export default function FloatingBubbleHost() {
  useFloatingBubbleLifecycle();
  return <MainDrawer />;
}

// app/(main)/_layout.tsx
import FloatingBubbleHost from '@/features/…/FloatingBubbleHost';
export default FloatingBubbleHost;

Contribución

License

MIT — ver LICENSE.


Made with create-react-native-library