Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 97 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# react-native-ble-nfc-reader

Expo native module for ACS BLE NFC Readers.
[![npm version](https://img.shields.io/npm/v/@countertek/react-native-ble-nfc-reader)](https://www.npmjs.com/package/@countertek/react-native-ble-nfc-reader)

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](https://docs.expo.dev/develop/development-builds/introduction/) 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.

## Installation

Expand All @@ -12,17 +17,6 @@ npm install @countertek/react-native-ble-nfc-reader

npm is the public distribution path for this native module. See [docs/release.md](docs/release.md) for maintainer release steps.

## Requirements

- Android 6.0+ (API 23+)
- iOS 15.0+
- Native development build or prebuilt app; Expo Go and web are not supported.
- ACS BLE Reader supported by the bundled ACS SDK binaries.
- MIFARE Classic Card for the MIFARE Classic helpers.

This package contains native Android and iOS code. After installing it, build a
native development build or run prebuild before using the module.

Add the config plugin to your app config:

```json
Expand All @@ -33,82 +27,116 @@ Add the config plugin to your app config:
}
```

The plugin adds Android BLE permissions and iOS Bluetooth usage descriptions.
The plugin adds Android BLE permissions and iOS Bluetooth usage descriptions. Rebuild the native app after adding or updating the plugin.

## Requirements

| | |
| --- | --- |
| **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/`](example/) app for a minimal Expo development-build setup.

## Reader flow

Typical integration order:

1. **Permissions** — check and request Reader permission before scanning.
2. **Scan** — discover nearby Readers for a bounded window.
3. **Connect** — connect one Reader at a time.
4. **Card** — listen for card presence, read UIDs, transmit APDUs, or use MIFARE Classic helpers.

### Reader permissions

## Reader permissions
Call `getReaderPermissionStatus()` before scanning. Call `requestReaderPermissions()` when the app needs to prompt.

Use `getReaderPermissionStatus()` before scanning and `requestReaderPermissions()`
when the app needs to prompt. Android reports missing runtime Bluetooth/location
permission as `denied` even before the first request; call
`requestReaderPermissions()` to ask for access. On iOS, calling `scanReaders()`
before Bluetooth permission has been requested rejects with
`READER_PERMISSION_UNDETERMINED`; check status and request Reader permission
before scanning. `READER_PERMISSION_DENIED` means runtime access was denied,
while `READER_PERMISSION_MISSING` means the native permission declaration is
missing.
| 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 |

## Reader scanning
On Android, missing Bluetooth/location permission may report as `denied` before the first request; call `requestReaderPermissions()` to prompt.

Use `scanReaders({ timeoutMs })` for bounded scans and
`addReaderDiscoveredListener()` to receive Readers during the scan window. Call
`stopReaderScan()` to end the active scan early. Starting a new `scanReaders()`
while one is active supersedes the prior bounded scan: the prior promise resolves
with partial Reader results collected so far, and discovery events for that scan
stop once it ends.
### Reader scanning

## Reader connection
- `scanReaders({ timeoutMs })` — bounded scan; resolves with discovered Readers.
- `addReaderDiscoveredListener()` — receive Readers during the scan window.
- `stopReaderScan()` — end the active scan early.

Use `connectReader(readerId)` with a discovered Reader ID to connect one Reader
per app process. It returns the connected `Reader`; `metadata` may include model,
firmware version, serial number, or battery level when the ACS Reader provides
those fields. Call `disconnectReader(readerId)` to release the native Reader
connection before connecting another Reader. If `disconnectReader()` rejects,
treat the Reader as still active and retry disconnect before connecting another
Reader.
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.

## Card and APDU
### Reader connection

Use `addCardPresentListener()` and `addCardRemovedListener()` after connecting a
Reader. `readCardUid(readerId)` returns the presented card UID as a Hex String.
`transmit(readerId, apdu)` sends a raw APDU Hex String and resolves with
`responseData` and `status`; non-`9000` APDU statuses are returned, not thrown.
- `connectReader(readerId)` — connect one Reader per app process; returns the connected `Reader`. `metadata` may 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.

## MIFARE Classic
### Card and APDU

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 unless
`allowTrailerWrite` is set.
After connecting a Reader:

- `addCardPresentListener()` / `addCardRemovedListener()` — card presence events.
- `readCardUid(readerId)` — card UID as a Hex String.
- `transmit(readerId, apdu)` — send a raw APDU Hex String; resolves with `responseData` (APDU Response Data) and `status` (APDU Status). Non-`9000` statuses are returned, not thrown.

### MIFARE Classic

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 unless `allowTrailerWrite` is set.

## Manual hardware checklist

- Android: grant Reader permission, start a 5 second scan, confirm an ACS BLE
Reader appears during the scan, then confirm scanning stops at timeout and
after `stopReaderScan()`. Connect the discovered Reader, confirm optional
metadata appears when available, present and remove a card, confirm the card
presence events update, read the card UID, 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 it, then confirm a second connect works only after disconnect.
- iOS: grant Bluetooth permission, start a 5 second scan, confirm an ACS BLE
Reader appears during the scan, then confirm scanning stops at timeout and
after `stopReaderScan()`. Connect the discovered Reader, confirm optional
metadata appears when available, present and remove a card, confirm the card
presence events update, read the card UID, 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 it, then confirm a second connect works only after disconnect.
Run these checks on real hardware when changing Reader, card, or MIFARE behavior. PRs that touch those flows should note what was tested.

**Android**

1. Grant Reader permission.
2. Start a 5 second scan; confirm an ACS BLE Reader appears.
3. Confirm scanning stops at timeout and after `stopReaderScan()`.
4. Connect the discovered Reader; confirm optional metadata when available.
5. Present and remove a card; confirm presence events update.
6. Read the card UID.
7. Transmit `FFCA000000`; confirm APDU Response Data and APDU Status are shown separately.
8. Authenticate a MIFARE Classic block with a caller-owned key, read the block, write 16 bytes, read it back.
9. Disconnect; confirm a second connect works only after disconnect.

**iOS**

1. Grant Bluetooth permission.
2. Start a 5 second scan; confirm an ACS BLE Reader appears.
3. Confirm scanning stops at timeout and after `stopReaderScan()`.
4. Connect the discovered Reader; confirm optional metadata when available.
5. Present and remove a card; confirm presence events update.
6. Read the card UID.
7. Transmit `FFCA000000`; confirm APDU Response Data and APDU Status are shown separately.
8. Authenticate a MIFARE Classic block with a caller-owned key, read the block, write 16 bytes, read it back.
9. Disconnect; confirm a second connect works only after disconnect.

## ACS SDK

ACS SDK binaries are bundled in the native package setup:
ACS SDK binaries are bundled in the native package:

- `android/libs/acssmcio-*.aar`
- `android/libs/smartcardio-*.aar`
- `ios/Frameworks/ACSSmartCardIO.xcframework`
- `ios/Frameworks/SmartCardIO.xcframework`

See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for bundled ACS and
SmartCardIO/OpenJDK notices.
See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for bundled ACS and SmartCardIO/OpenJDK notices.

## Development

From the repo root:

```sh
pnpm install
pnpm run lint
pnpm run build
```

Changes to Reader or card flows need the manual hardware checklist on physical devices.
Loading