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

@voova/capacitor-nfc-adapter

v0.4.0

Published

Dual-platform NFC card UID scanner (Android USB Host/CCID readers + iOS wired CCID readers via CryptoTokenKit, with CoreNFC fallback) delivering the strict uid_dec_reversed format

Readme

:nfc-adapter — Dual-Platform NFC Scanning Module

Delivers student-card UIDs to the host app through one unified callback on both platforms:

| Platform | Engine | Hardware | |---|---|---| | Android | USB Host API (CCID class 11 / serial bridges / Brand A SDK hook) | External USB Type-C readers, plug & play | | iOS | CryptoTokenKit CCID slot (TKSmartCardSlotManager + PC/SC FF CA 00 00 00) | External wired reader (ACR1255U-J1 through a Lightning/USB adapter), plug & play | | iOS (fallback) | CoreNFC NFCTagReaderSession (ISO 14443) | Built-in antenna — card taps the iPhone |

The output contract — uid_dec_reversed

Every engine on every platform delivers the decimal representation of the reversed raw UID byte array, no padding:

raw bytes read from card :  FB 5D 82 29
reversed                 :  29 82 5D FB
onCardScanned() receives :  "696409595"

Pinned by shared-test-vectors/uid_vectors.json (values computed with an independent .NET BigInteger oracle) across three test lanes: Kotlin/JVM (src/test), Swift/XCTest (ios/Tests) and the toolchain-free Node harness (archive/verify.mjs, 69 checks — run with node archive/verify.mjs).

Layout

src/main/kotlin/com/school/nfcadapter/
  api/            NfcScannerPort + NfcScanListener (the ONLY public surface)
  core/           UidNormalizer, DeviceRouter, ConnectionManager (generation
                  tokens, attach debounce, storm/brownout detection), ListenerProxy
  usb/            attach trampoline, detach receiver, permission coordinator
                  (API 31+ FLAG_MUTABLE + API 34 explicit-intent compliant)
  transport/      UsbTransport seam + AndroidUsbTransport (bulkTransfer)
  handler/ccid/   pure CCID protocol + edge-triggered polling engine
  handler/serial/ pure frame assembler/profiles + usb-serial-for-android wrapper
  handler/branda/ Brand A SDK hook (integration contract in the file header)
ios/Sources/      Swift twin: NfcScannerPort, UidNormalizer, CoreNfcScannerPort
ios/Tests/        XCTest suites (same golden vectors)

Capacitor usage (@voova/capacitor-nfc-adapter)

The package is an autolinkable Capacitor 3 plugin: android/ (Gradle library + @CapacitorPlugin NfcAdapterPlugin), ios/Plugin/ (Swift CAPPlugin bridge registered via the CAP_PLUGIN macro, CapacitorNfcAdapter.podspec at package root), index.js entry point using registerPlugin('NfcAdapter') with a no-op web fallback for ionic serve.

import { NfcAdapter } from '@voova/capacitor-nfc-adapter';

const handle = NfcAdapter.addListener('onCardScanned', ({ uid }) => checkIn(uid)); // "696409595"
await NfcAdapter.startScanning();
// later: await NfcAdapter.stopScanning(); handle.remove();

Install in the host app, then npx cap sync to wire the native projects (capacitor.settings.gradle on Android, Podfile on iOS/macOS machines).

Android integration (native, non-RN)

val scanner: NfcScannerPort = NfcAdapterModule.create(context)  // Noop on non-OTG devices
scanner.setListener(object : NfcScanListener {
    override fun onCardScanned(uidDecReversed: String) { checkInGateway.submit(USB_NFC, uidDecReversed) }
    override fun onReaderStateChanged(state: ReaderState) { /* standby badge */ }
    override fun onReaderError(error: ReaderError) { /* driver-facing hint */ }
})
scanner.start()   // route screen enter / login
scanner.stop()    // route screen exit / logout

Gradle: add maven { url = uri("https://jitpack.io") } to repositories (for usb-serial-for-android). The library manifest already declares <uses-feature android:name="android.hardware.usb.host" android:required="false" /> — verify it survives manifest merge with aapt dump badging on the release APK.

iOS integration

Minimum iOS version: 16.0. The production route is the external wired reader, and iOS only publishes the reader's CCID slot to third-party apps from iOS 16 — so the pod's floor is 16.0 rather than a lower number the wired route could not honour.

Transport selection is automatic — the factory prefers a wired reader and only falls back to the built-in antenna when the platform publishes no CCID slot service:

let scanner = IosScannerPortFactory.make()            // .auto (default)
let scanner = IosScannerPortFactory.make(route: .wired)    // force wired CCID
let scanner = IosScannerPortFactory.make(route: .coreNfc)  // force built-in antenna
scanner.setListener(listener)
scanner.start()
scanner.stop()

From the Capacitor side the route is set with the iosScannerRoute plugin config value ("wired" | "coreNfc" | "auto", default "auto").

Wired route (external ACR1255U-J1) — the production path

CcidWiredScannerPort talks to the reader through the system CCID stack:

  • iOS exposes no general-purpose USB-host API, and this reader's cable interface is not an MFi/iAP accessory — EAAccessoryManager.connectedAccessories is empty with the reader attached (verified at runtime).
  • The system's CCID stack does enumerate it, and TKSmartCardSlotManager publishes that slot to third-party apps with no entitlement from iOS 16 onwards. Verified on iOS 16.7.12: slotNames == ["ACS ACR1255U-J1"], one card presentation returns SW 9000 with a 4-byte UID.
  • No NFCReaderUsageDescription and no NFC entitlement are required for this route — those apply only to the CoreNFC fallback.
  • Requires iOS 16+ for the slot service, which is exactly the pod's deployment target — no supported OS version is left without a working wired route.

Semantics match Android: health-gated Ready (two consecutive successful non-destructive probes — never a false Ready), deterministic DISCONNECTED on detach with automatic re-arm on reconnect, and exactly one onCardScanned per card presentation (the field must go free before the next tap counts).

CoreNFC fallback

Required target config for the fallback only: see ios/INFO_PLIST_ADDITIONS.md (NFCReaderUsageDescription + Tag Reading entitlement).

Honest platform limits (by Apple design): the CoreNFC route shows the system scan sheet whenever scanning is active — there is no silent background standby, and sessions are capped at ~60 s (the port auto-restarts them while start() is in effect). The wired route has neither limitation: no sheet, no session cap.

Testing without hardware

  • archive/verify.mjs — runs anywhere Node exists; exercises normalizers, CCID validation chain, engine state machines, serial assembly, router, and the vibration-storm/generation-token model.
  • Kotlin suite — pure classes have zero Android imports; runs as plain JVM unit tests in CI (ScriptedUsbTransport scripts taps/flicks/disconnects).
  • Swift suite — CoreNfcScannerPort.handleDiscoveredIdentifier(_:) is the pure seam; tests inject mock Data identifiers, valid and corrupted.

Open items before field rollout

  1. Brand A .aar: follow the contract in handler/branda/BrandAHandlerHook.kt; set brandAVendorId/brandAProductIds in NfcModuleConfig and add the VID/PID to res/xml/nfc_device_filter.xml.
  2. Confirm with the roster/backend owner that stored card numbers match uid_dec_reversed (this format was chosen to match keyboard-emulator output; verify endianness against a sample of real production cards).
  3. Tier-4 on-device smoke: one CCID reader + one CH340 reader on the two most common fleet phones (cold plug, hot plug, "Always open" permission path, 100-tap soak, mid-tap unplug).