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

@notiflyio/capacitor-push

v0.1.2

Published

Capacitor plugin wiring Notifly push into PWA shell apps: FCM channel setup, injected window-event bridge, and same-origin notification deep links

Readme

@notiflyio/capacitor-push

Capacitor plugin wiring Notifly push into the fleet's PWA shell apps. It owns what every shell used to hand-copy (caly-pwa-android / marka-pwa-android#5, ~350 lines per app):

  • FCM default notification channel created at launch with a real name (config-driven id/name/description).
  • Injected window-event bridge (bridge/notifly-push-bridge.js) installed at document start into every document of the app origin, re-dispatching @capacitor/push-notifications activity as the SAME window CustomEvents the fleet's raw-WKWebView iOS shells emit — the app's web layer keeps one set of listeners, no per-platform fork.
  • Notification tap → same-origin deep link. Warm taps are handled by the plugin automatically; cold-start taps are a ~6-line MainActivity hook (below). Off-origin links from push payloads are rejected (phishing surface).

Event contract (fleet-locked; see pwa-notifications references/contracts.md)

One token event on both platforms, naming its own provider — a web layer keeps one listener and never forks per platform.

| Direction | Name | Detail | |---|---|---| | shell → web | push-apns-token | { token: string, provider: 'apns' \| 'fcm' } | | shell → web | push-apns-error | error message string | | shell → web | push-notification | foreground payload (title, body, link, data) | | shell → web | push-permission-request | 'granted' \| 'denied' | | shell → web | push-permission-state | 'authorized' \| 'denied' \| 'notDetermined' | | web → shell | window.NotiflyPush.requestPermission() / permissionState() / token() | control object (plus the jsGlobal alias) | | web → shell | window.webkit.messageHandlers['push-permission-request' \| 'push-permission-state' \| 'push-token'].postMessage('') | same three calls, under the names the fleet's raw-WKWebView shells register |

Deprecated aliases (since 0.1.1)

| Name | Detail | Status | |---|---|---| | push-fcm-token | device token string | Android only; emitted alongside push-apns-token. Deprecated — read the primary event's provider instead. | | push-fcm-error | error message string | Android only; emitted alongside push-apns-error. Deprecated. |

Both aliases keep 0.1.0's bare-string detail, so a consumer written against 0.1.0 needs no change. They will not be removed before 1.0. On iOS the legacy name and the primary name are the same event, so it is dispatched exactly once, with the object detail.

Migration for a 0.1.0 consumer: read the token from push-apns-token, accepting either shape —

const read = (detail) => (typeof detail === 'string' ? { token: detail, provider: 'apns' } : detail);
window.addEventListener('push-apns-token', (e) => { const { token, provider } = read(e.detail); ... });

— and drop the push-fcm-* listeners.

The control object appears asynchronously: the injected script retries for ~10s until Capacitor's runtime is callable (a server.url shell has not bootstrapped it at document start), then warns [notifly] gave up installing the push bridge if it never was. Probe for window.NotiflyPush when you need it, not once at mount.

Install (shell app)

npm install @notiflyio/capacitor-push @capacitor/push-notifications
npx cap sync

@capacitor/push-notifications is a peer dependency and LOAD-BEARING beyond gradle wiring: Capacitor's own JS export writes Capacitor.Plugins.PushNotifications (the probe web layers use to detect the native push leg) only when that plugin is installed.

capacitor.config.json

{
  "plugins": {
    "NotiflyPush": {
      "jsGlobal": "MarkaPush",
      "channelId": "marka_default",
      "channelName": "Marka notifications",
      "channelDescription": "General notifications",
      "allowedHost": "joinmarka.com"
    }
  }
}

All keys optional — channelId defaults to notifly_default, the canonical window.NotiflyPush global is always set, deep links always allow the app origin (server.url).

Android app manifest (stays app-side)

<meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="marka_default" />

plus the usual Firebase leg: google-services.json in android/app/ and the com.google.gms.google-services gradle plugin. Firebase project MUST be the app's existing OAuth GCP project (SenderId-mismatch class — see the pwa-notifications skill's portal-setup reference).

MainActivity cold-start hook (the one last mile a plugin cannot own)

The initial page load is already in flight during plugin load, so a cold-start tap must steer that load from the activity:

import io.notifly.capacitorpush.NotiflyPushBridge;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    String link = NotiflyPushBridge.deepLinkFrom(getIntent(), getBridge().getConfig().getServerUrl());
    if (link != null) {
        getBridge().getWebView().loadUrl(link);
    }
}

Warm taps (app already running) are handled by the plugin's handleOnNewIntent — no activity code. If your activity ALSO navigates on new intents, both navigations target the same URL (harmless).

The app-side token relay route (session-authed, forwards the device token to Notifly's per-device endpoints) deliberately stays app-side too — it needs the app's auth. Recipe: pwa-notifications references/wiring.md.

Development

bridge/notifly-push-bridge.js is the canonical script; pnpm build (or node scripts/embed-bridge.mjs) stamps it into the committed native copies (android/src/main/assets/ + generated BridgeScript.swift). node scripts/embed-bridge.mjs --check gates drift (runs in prepublish).

pnpm test runs test/bridge.test.mjs — the real script in a fake document (node:vm, fake clock), pinning the install retry and the event contract. Both also run on every PR (.github/workflows/ci.yml). Point NOTIFLY_BRIDGE_PATH at another copy of the script to run the suite against it: with 0.1.0's bridge, 9 of the 13 checks fail, starting with the install race (notifly#61).

Verification status

  • Android half: ported line-for-line from caly-pwa-android (device-proven on the fleet, 2026-07). This npm packaging device-proven 2026-07-31: marka-pwa-android migrated onto the packed plugin (app-local bridge deleted), gradle assembleDebug green, and on an API-36 emulator logcat showed plugin registration, the marka_default channel created from config, and the injected bridge's addListener → checkPermissions → register → FCM registration event with no registrationError. Remaining before 1.x: a real notification DELIVERY + tap deep-link pass on a physical device. 0.1.1 cures what the superbooks Android device proof found on 2026-08-01 (notifly#61): on that shell the bridge never installed on any page of the app origin, because at document start on a fresh server.url context Capacitor is not callable yet and 0.1.0 returned for good. The install is now self-retrying and test/bridge.test.mjs pins it.
  • iOS half: authored to the same design but COMPILE-GATED — no Mac toolchain has built it as of 2026-07-31. The fleet's shipped iOS shells are raw WKWebView apps (see the NotiflyShellPush Swift package), not Capacitor; this half exists for future Capacitor-built iOS shells. Do not ship it to a device without an Xcode build + the contract smoke run.