Expo native module for ACS BLE NFC Readers — scan for a Reader, connect over Bluetooth, read card UIDs, send raw APDUs, and work with MIFARE Classic Cards.
Important
This package ships native Android and iOS code. Expo Go and web are not supported. Install it into an Expo app with a development build or a bare React Native app, then run expo prebuild / npx expo run:android or npx expo run:ios so the native module is compiled in.
Install the package from npmjs.com:
npm install @countertek/react-native-ble-nfc-readernpm is the public distribution path for this native module. See docs/release.md for maintainer release steps.
Add the config plugin to your app config:
{
"expo": {
"plugins": ["@countertek/react-native-ble-nfc-reader"]
}
}The plugin adds Android BLE permissions and iOS Bluetooth usage descriptions. Rebuild the native app after adding or updating the plugin.
| Platforms | Android 6.0+ (API 23+), iOS 15.0+ |
| App type | Expo development build or bare React Native — not Expo Go, not web |
| Hardware | ACS BLE Reader supported by the bundled ACS SDK binaries |
| Cards | MIFARE Classic Card for the mifare helpers |
See the example/ app for a minimal Expo development-build setup.
Detailed guides — installation troubleshooting, Reader lifecycle, card/APDU usage, MIFARE Classic, and release expectations — are on DeepWiki. The docs/deepwiki.md outline in this repo seeds that structure.
Typical integration order:
- Permissions — check and request Reader permission before scanning.
- Scan — discover nearby Readers for a bounded window.
- Connect — connect one Reader at a time.
- Card — start monitoring when you need card presence events, read UIDs, transmit APDUs, or use MIFARE Classic helpers.
Call getReaderPermissionStatus() before scanning. Call requestReaderPermissions() when the app needs to prompt.
| Status | Meaning |
|---|---|
READER_PERMISSION_UNDETERMINED |
Not asked yet — request before scanning (iOS rejects scanReaders() until permission is requested) |
READER_PERMISSION_DENIED |
User denied runtime access |
READER_PERMISSION_MISSING |
Native permission declaration is missing from the app |
On Android, missing Bluetooth/location permission may report as denied before the first request; call requestReaderPermissions() to prompt.
scanReaders({ timeoutMs })— bounded scan; resolves with discovered Readers.addReaderDiscoveredListener()— receive Readers during the scan window.stopReaderScan()— end the active scan early.
Starting a new scanReaders() while one is active supersedes the prior scan: the prior promise resolves with partial results collected so far, and discovery events for that scan stop once it ends.
connectReader(readerId)— connect one Reader per app process; returns the connectedReader.metadatamay include model, firmware version, serial number, or battery level when the ACS Reader provides those fields.disconnectReader(readerId)— release the native connection before connecting another Reader. If disconnect rejects, treat the Reader as still active and retry before connecting elsewhere.
After connecting a Reader:
addCardPresentListener()/addCardRemovedListener()— subscribe to card presence events.startCardMonitor(readerId, options?)— explicitly start Card Presence Event monitoring. Connecting a Reader no longer starts monitoring by default in v0.2.0.stopCardMonitor(readerId)— stop monitoring silently. A manual stop or auto-stop does not mean the card was physically removed and does not emit a card removal event.readCardUid(readerId)— card UID as a Hex String.transmit(readerId, apdu)— send a raw APDU Hex String; resolves withresponseData(APDU Response Data) andstatus(APDU Status). Non-9000statuses are returned, not thrown.
startCardMonitor() defaults to { pollingIntervalMs: 1000, autoStopAfterMs: null }. pollingIntervalMs must be an integer of at least 100. autoStopAfterMs, when provided, must be a positive integer. Repeated starts with the same options are idempotent; repeated starts with different options reject with CARD_MONITOR_ALREADY_ACTIVE.
Card commands do not require Card Presence Event monitoring. For example, readCardUid(readerId) works after connectReader(readerId) even when no monitor is running.
const present = addCardPresentListener(({ readerId }) => {
console.log('Card present', readerId);
});
const removed = addCardRemovedListener(({ readerId }) => {
console.log('Card removed', readerId);
});
const reader = await connectReader(readerId);
await startCardMonitor(reader.id, { pollingIntervalMs: 1000, autoStopAfterMs: 30000 });
await stopCardMonitor(reader.id);
await disconnectReader(reader.id);
present.remove();
removed.remove();Use mifare.authenticateBlock({ readerId, block, keyType, key }) with a caller-owned key before mifare.readBlock({ readerId, block }) or mifare.writeBlock({ readerId, block, data }).
- Keys are loaded into the Reader only for the operation and are not stored by this package.
readBlock()returns 16 bytes as a Hex String.writeBlock()rejects trailer blocks unlessallowTrailerWriteis set.
Run these checks on real hardware when changing Reader, card, or MIFARE behavior. PRs that touch those flows should note what was tested.
Android
- Grant Reader permission.
- Start a 5 second scan; confirm an ACS BLE Reader appears.
- Confirm scanning stops at timeout and after
stopReaderScan(). - Connect the discovered Reader; confirm optional metadata when available.
- Confirm no Card Presence Events arrive after
connectReader()alone. - Start Card Presence Event monitoring; present and remove a card; confirm presence events update.
- Start monitoring while a card is already present; confirm an immediate Card Presence Event.
- Stop monitoring; confirm stop is silent and later card changes do not emit events.
- Start monitoring with
autoStopAfterMs; confirm auto-stop is silent. - Read the card UID without monitoring running.
- Transmit
FFCA000000; confirm APDU Response Data and APDU Status are shown separately. - Authenticate a MIFARE Classic block with a caller-owned key, read the block, write 16 bytes, read it back.
- Disconnect; confirm a second connect works only after disconnect.
iOS
- Grant Bluetooth permission.
- Start a 5 second scan; confirm an ACS BLE Reader appears.
- Confirm scanning stops at timeout and after
stopReaderScan(). - Connect the discovered Reader; confirm optional metadata when available.
- Confirm no Card Presence Events arrive after
connectReader()alone. - Start Card Presence Event monitoring; present and remove a card; confirm presence events update.
- Start monitoring while a card is already present; confirm an immediate Card Presence Event.
- Stop monitoring; confirm stop is silent and later card changes do not emit events.
- Start monitoring with
autoStopAfterMs; confirm auto-stop is silent. - Read the card UID without monitoring running.
- Transmit
FFCA000000; confirm APDU Response Data and APDU Status are shown separately. - Authenticate a MIFARE Classic block with a caller-owned key, read the block, write 16 bytes, read it back.
- Disconnect; confirm a second connect works only after disconnect.
ACS SDK binaries are bundled in the native package:
android/libs/acssmcio-*.aarandroid/libs/smartcardio-*.aarios/Frameworks/ACSSmartCardIO.xcframeworkios/Frameworks/SmartCardIO.xcframework
See THIRD_PARTY_NOTICES.md for bundled ACS and SmartCardIO/OpenJDK notices.
From the repo root:
pnpm install
pnpm run lint
pnpm run buildChanges to Reader or card flows need the manual hardware checklist on physical devices.