Version 1.0.0 | Author: Listiananda Apriliawan
- Overview
- Architecture
- Module Breakdown
- NFC Scanning Flow
- EMV TLV Parser
- APDU Commands
- Card Data Extraction
- Error Handling
- Build & Distribution
- Testing Strategy
react-native-nfc-card-scanner is a React Native library that reads contactless payment card data (card number, expiry date, card scheme) via NFC using the EMV contactless protocol. It communicates with cards over ISO-DEP (ISO 14443-4) and parses EMV TLV-encoded responses.
- Scans contactless credit/debit cards via NFC
- Extracts PAN (Primary Account Number) and expiration date
- Auto-detects card scheme (Visa, Mastercard, JCB, Amex, UnionPay, Discover)
- Provides a standalone EMV TLV parser
- Does NOT read CVV, PIN, or cardholder name
- Does NOT perform transactions or modify card state
- Does NOT store or transmit card data
+------------------------------------------------------------------+
| Consumer Application |
| (React Native App) |
+------------------------------------------------------------------+
| | |
v v v
+----------------+ +----------------+ +------------------+
| scanNfc() | | stopNfc() | | isNfcSupported() |
| Core Scan | | Cancel Scan | | isNfcEnabled() |
+----------------+ +----------------+ +------------------+
|
v
+------------------------------------------------------------------+
| Scanner Module (scanner.ts) |
| |
| +-------------------+ +------------------+ +---------------+ |
| | PPSE Selection | | AID Detection | | PDOL Commands | |
| | & Communication | | & Scheme Mapping | | & Data Read | |
| +-------------------+ +------------------+ +---------------+ |
+------------------------------------------------------------------+
| |
v v
+-------------------+ +--------------------+
| react-native- | | EMV Module |
| nfc-manager | | (emv/) |
| (Peer Dependency) | | |
| | | +----------------+ |
| - NfcTech.IsoDep | | | TLV Parser | |
| - isoDepHandler | | +----------------+ |
| - transceive() | | | Tag Database | |
| | | | (7400+ entries)| |
+-------------------+ | +----------------+ |
| | Hex/Bin Utils | |
| +----------------+ |
+--------------------+
src/
|-- index.ts Public API exports
|-- scanner.ts NFC scanning logic, APDU commands, card data extraction
|-- types.ts TypeScript interfaces, error constants, type unions
|-- emv/
| |-- index.ts TLV parser, tag lookup, describe functions
| |-- tags.ts EMV tag database (7400+ lines, all known EMV tags)
| |-- utils.ts Hex/Binary/Decimal conversion utilities
|-- __tests__/
|-- scanner.test.ts Card scheme detection tests
|-- emv-parser.test.ts TLV parsing tests
|-- emv-utils.test.ts Utility function tests
|-- types.test.ts Type and error code tests
index.ts
|
+---> scanner.ts
| |
| +---> emv/index.ts
| | |
| | +---> emv/tags.ts
| | +---> emv/utils.ts
| |
| +---> types.ts
| +---> react-native-nfc-manager (external peer dep)
|
+---> emv/index.ts (re-exported as `emv`)
+---> types.ts (re-exported types & constants)
The main module responsible for the entire card reading flow.
Key Functions:
| Function | Visibility | Description |
|---|---|---|
scanNfc(options?) |
Public | Entry point. Validates NFC, starts scan with timeout |
stopNfc() |
Public | Cancels ongoing scan, releases NFC reader |
isNfcSupported() |
Public | Checks device NFC hardware |
isNfcEnabled() |
Public | Checks if NFC is enabled |
getCardSchemeFromAid(aid) |
Public | Maps AID prefix to card scheme |
readCardData() |
Private | Orchestrates the full EMV read flow |
extractRecord(type) |
Private | Sends PDOL commands, parses response |
flatTLVParser(responses) |
Private | Parses Tag 70 (flat) structure |
nestedTLVParser(responses) |
Private | Parses Tag 77 (nested) structure |
extractAidTags(hex) |
Private | Regex extraction of AIDs from PPSE response |
toByteArray(hex) |
Private | Hex string to byte array conversion |
toHexString(bytes) |
Private | Byte array to hex string conversion |
A standalone TLV (Tag-Length-Value) parser for EMV data.
Public API:
| Function | Description |
|---|---|
emv.parse(data, cb) |
Parse raw hex TLV into EmvObject[] |
emv.describe(data, cb) |
Parse + add human-readable tag descriptions |
emv.lookup(tag, cb) |
Look up a single tag name |
emv.getValue(tag, objects, cb) |
Extract value for a specific tag |
emv.getElement(tag, objects, cb) |
Extract full element for a specific tag |
emv.describeKernel(data, kernel, cb) |
Parse with kernel-specific lookup |
emv.lookupKernel(tag, kernel, cb) |
Look up tag in specific kernel |
Low-level hex/binary/decimal conversion functions used by the TLV parser.
A comprehensive database of 7400+ EMV tag definitions covering multiple kernels (Generic, VISA, MasterCard, JCB, etc.) with tag names, descriptions, formats, and sources.
All TypeScript interfaces, type unions, and error constants.
Consumer App scanNfc() NfcManager Payment Card
| | | |
|--- scanNfc() ----->| | |
| | | |
| [Validate NFC] | |
| |--- isSupported --->| |
| |<-- true/false -----| |
| |--- isEnabled ----->| |
| |<-- true/false -----| |
| | | |
| [Start NFC Session] | |
| |--- start() ------->| |
| |--- registerTag --->| |
| | (NFC-A, NFC-B, | |
| | skip NDEF, | |
| | no sounds) | |
| | | |
| [Begin Timeout Race] | |
| | | |
| [readCardData()] | |
| |--- requestTech --->| |
| | (IsoDep) |--- ISO-DEP ------>|
| | |<-- Connected -----|
| | | |
| [Step 1: SELECT PPSE] | |
| |--- transceive ---->|--- APDU --------->|
| | 00A40400... | SELECT |
| | "2PAY.SYS.DDF01"| PPSE |
| |<-- response -------|<-- AIDs list -----|
| | | |
| [Step 2: Extract AID] | |
| | regex: /4F(..)(hex)/ |
| | match AID prefix |
| | --> Card Scheme |
| | | |
| [Step 3: SELECT AID] | |
| |--- transceive ---->|--- APDU --------->|
| | 00A40400[aid] | SELECT AID |
| |<-- response -------|<-- ACK ----------|
| | | |
| [Step 4: GET PROCESSING OPTIONS] |
| |--- transceive ---->|--- APDU --------->|
| | 80A80000... | GPO |
| |<-- response -------|<-- EMV data ------|
| | | |
| [Step 5: Parse EMV] | |
| | TLV Parser | |
| | Extract PAN + EXP | |
| | | |
| [Cleanup] | |
| |--- cancelTech ---->|--- Disconnect --->|
| |--- unregister ---->| |
| | | |
|<-- {card,exp, | | |
| scheme} -------| | |
+-------------+
| IDLE |
+------+------+
|
scanNfc() called
|
v
+------+------+
+-----| VALIDATING |-----+
| +------+------+ |
| | |
NFC not NFC not NFC OK
supported enabled |
| | v
v v +------+------+
+----+----+ +---+---+ | REGISTERING |
| ERROR | | ERROR | | TAG EVENT |
+---------+ +-------+ +------+------+
|
v
+------+------+
+-----| SCANNING |-----+
| +------+------+ |
| | |
Timeout Card detected stopNfc()
| | |
v v v
+-----+----+ +----+-----+ +----+-----+
| ERROR | | READING | | CANCELLED|
| TIMEOUT | | CARD | +----------+
+----------+ +----+-----+
|
+-------+-------+
| |
Read OK Read Failed
| |
v v
+-----+----+ +------+-----+
| RESULT | | ERROR |
| {card, | | READ_FAILED|
| exp, | +------------+
| scheme} |
+----------+
EMV data is encoded in TLV (Tag-Length-Value) format:
+-------+--------+------------------+
| Tag | Length | Value |
+-------+--------+------------------+
| 1-3 B | 1-3 B | Variable length |
+-------+--------+------------------+
Tag First Byte Layout:
+---+---+---+---+---+---+---+---+
| 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
+---+---+---+---+---+---+---+---+
|Class |C/P| Tag Number |
+-------+---+-------------------+
Class (bits 7-8):
00 = Universal
01 = Application
10 = Context-specific
11 = Private
C/P (bit 6):
0 = Primitive (value is raw data)
1 = Constructed (value contains nested TLV)
Tag Number (bits 1-5):
If all 1s (11111) --> multi-byte tag, read next byte(s)
Next byte: bit 8 = 1 means more bytes follow
Short Form (1 byte):
0x00-0x7F --> Length = value directly
Example: 08 = 8 bytes
Long Form (2+ bytes):
First byte: 0x8N where N = number of length bytes that follow
Example: 81 FF = 255 bytes
Example: 82 01 00 = 256 bytes
Input: "6F10840E325041592E5359532E4444463031"
Step 1: Read Tag
First byte: 6F (binary: 01101111)
Class: 01 (Application)
C/P: 1 (Constructed --> value is nested TLV)
Number: 01111 (not all 1s --> single byte tag)
Tag = "6F"
Step 2: Read Length
Byte: 10 (hex) = 16 (dec)
Short form (< 0x80)
Length = 16 bytes = 32 hex chars
Step 3: Read Value
Value = "840E325041592E5359532E4444463031"
Since tag is Constructed, recursively parse:
Inner Step 1: Tag = "84"
Inner Step 2: Length = 0E = 14 bytes
Inner Step 3: Value = "325041592E5359532E4444463031"
= "2PAY.SYS.DDF01" (ASCII)
Result:
{
tag: "6F",
length: "10",
value: [
{ tag: "84", length: "0E", value: "325041592E5359532E4444463031" }
]
}
APDU Command Format:
+-----+-----+----+----+----+---------+----+
| CLA | INS | P1 | P2 | Lc | Data | Le |
+-----+-----+----+----+----+---------+----+
| 1B | 1B | 1B | 1B | 1B | Lc bytes| 1B |
+-----+-----+----+----+----+---------+----+
CLA = Class byte (00 = standard, 80 = proprietary)
INS = Instruction byte
P1 = Parameter 1
P2 = Parameter 2
Lc = Length of data field
Data = Command data
Le = Expected response length
00 A4 04 00 0E 325041592E5359532E444446303100
| | | | | |
| | | | | +-- Data: "2PAY.SYS.DDF01" + NULL (ASCII hex-encoded)
| | | | +-- Lc: 14 bytes of data
| | | +-- P2: 00 (first occurrence)
| | +-- P1: 04 (select by DF name)
| +-- INS: A4 (SELECT)
+-- CLA: 00 (standard)
Purpose: Select the Payment System Environment to discover available AIDs
00 A4 04 00 [len] [aid_bytes]
Example for Visa:
00 A4 04 00 07 A0000000031010
Purpose: Select the specific payment application on the card
80 A8 00 00 23 83 21 28 00 00 00 00 00 00 00 00 00 00 00 25 00 00 00 00 00 00 09 78 20 05 26 00 E8 DA 93 52 00
| | | | | |
| | | | | +-- PDOL data (Processing Options Data Object List)
| | | | +-- Lc: 35 bytes
| | | +-- P2: 00
| | +-- P1: 00
| +-- INS: A8 (GET PROCESSING OPTIONS)
+-- CLA: 80 (proprietary)
Purpose: Request card data using full PDOL for nested (77) template cards
Command 1: 80 A8 00 00 02 83 00 00
Purpose: Basic GPO with minimal PDOL
Command 2: 00 B2 01 14 00
| | | | |
| | | | +-- Le: 00 (read all)
| | | +-- P2: 14 (SFI 2, record)
| | +-- P1: 01 (record number 1)
| +-- INS: B2 (READ RECORD)
+-- CLA: 00 (standard)
Purpose: Request card data for flat (70) template cards
The library handles two EMV response template structures:
Template Type Comparison:
+-----------------------------+-----------------------------+
| FLAT (Tag 70) | NESTED (Tag 77) |
+-----------------------------+-----------------------------+
| | |
| 70 [len] | 77 [len] |
| |-- 5A [len] [PAN] | |-- 57 [len] [Track2] |
| |-- 5F24 [len] [EXP] | | | |
| |-- ...other tags | | +-- PAN + D + |
| | | YYMM + ... |
| | |-- ...other tags |
| | |
+-----------------------------+-----------------------------+
| | |
| PAN Source: | PAN Source: |
| Tag 5A directly | Tag 57, split on 'D', |
| | take first part |
| Expiry Source: | |
| Tag 5F24 directly | Expiry Source: |
| Format: YYMMDD | Tag 57, after 'D', |
| | first 4 chars (YYMM) |
+-----------------------------+-----------------------------+
extractRecord()
|
v
Send PDOL Commands
|
v
Parse Response with EMV TLV Parser
|
v
Check first tag of parsed result
|
+--- Tag == "70" (Flat)
| |
| v
| flatTLVParser()
| Find Tag 5A --> card number
| Find Tag 5F24 --> expiry (YYMMDD)
| Return { card, exp: "MM/YY" }
|
+--- Tag == "77" (Nested)
|
v
nestedTLVParser()
Find Tag 57 --> Track 2 Equivalent Data
Split value on 'D' separator
Left side = PAN
Right side first 4 chars = YYMM
Return { card, exp: "MM/YY" }
Priority: Flat result preferred if both PAN and EXP are valid,
otherwise fall back to Nested result.
AID Prefix Mapping:
AID (hex) Scheme
+-----------+ +------------+
|A000000003 | --> | VISA |
|A000000004 | --> | MASTERCARD |
|A000000065 | --> | JCB |
|A000000025 | --> | AMEX |
|A000000333 | --> | UNIONPAY |
|A000000152 | --> | DISCOVER | (Discover Global Network)
|A000000324 | --> | DISCOVER | (Diners Club International)
|A000000444 | --> | DISCOVER | (Older Diners AID)
| other | --> | null |
+-----------+ +------------+
Process:
1. PPSE response contains Tag 4F entries (AIDs)
2. Extract AIDs using regex: /4F(..)([A-Fa-f0-9]+)/gi
3. Take first AID found
4. Match prefix against table above
5. Return CardScheme or null
scanNfc()
|
+-- isSupported() == false
| --> throw NFC_NOT_SUPPORTED
|
+-- isEnabled() == false
| --> throw NFC_NOT_ENABLED
|
+-- Timeout exceeded
| --> throw SCAN_TIMEOUT
|
+-- readCardData()
|
+-- No AID found in PPSE response
| --> throw AID_NOT_FOUND
|
+-- AID doesn't match any known scheme
| --> throw UNSUPPORTED_CARD_SCHEME
|
+-- Card data missing (no PAN or EXP)
| --> throw CARD_READ_FAILED
|
+-- NFC communication error
--> throw native error (tag lost, etc.)
| Error Code | When | Recovery |
|---|---|---|
NFC_NOT_SUPPORTED |
Device has no NFC hardware | Cannot recover; inform user |
NFC_NOT_ENABLED |
NFC toggle is off | Prompt user to enable in Settings |
SCAN_TIMEOUT |
No card detected within timeout | Retry scan |
AID_NOT_FOUND |
PPSE response has no AID tags | Card may not be a payment card |
UNSUPPORTED_CARD_SCHEME |
AID prefix not recognized | Card network not supported |
CARD_READ_FAILED |
EMV data missing PAN or expiry | Retry, card may be damaged |
try {
// ... scan operations
} finally {
NfcManager.cancelTechnologyRequest() // Release IsoDep
NfcManager.unregisterTagEvent() // Unregister listener
}
The finally block ensures NFC resources are always released, even when errors occur or timeout triggers. Both cleanup calls use .catch(() => {}) to silently handle cases where the resource was already released.
Source (TypeScript) Build (tsup) Output
+------------------+ +-----------------+ +------------------+
| src/index.ts | | | | dist/index.js | CommonJS
| src/scanner.ts | --> | tsup | --> | dist/index.mjs | ESM
| src/types.ts | | (esbuild-based) | | dist/index.d.ts | Types
| src/emv/ | | | | dist/index.d.mts | Types (ESM)
+------------------+ +-----------------+ | dist/*.map | Sourcemaps
+------------------+
Config:
- Target: ES2020
- No code splitting (single bundle)
- react-native-nfc-manager externalized
- Source maps enabled
Published to npm: NOT published:
+--------------------+ +--------------------+
| dist/index.js | | src/ |
| dist/index.mjs | | example/ |
| dist/index.d.ts | | node_modules/ |
| dist/index.d.mts | | __tests__/ |
| dist/*.map | | tsconfig.json |
| README.md | | tsup.config.ts |
| LICENSE | | .gitignore |
| package.json | | vitest.config.* |
+--------------------+ +--------------------+
Controlled by "files" field in package.json:
["dist", "README.md", "LICENSE"]
{
"main": "dist/index.js", // CommonJS entry (require)
"module": "dist/index.mjs", // ESM entry (import)
"types": "dist/index.d.ts", // TypeScript definitions
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
}
}Test Suites:
+---------------------------+--------------------------------+
| File | What it tests |
+---------------------------+--------------------------------+
| scanner.test.ts | Card scheme detection from AID |
| | - All 6 card schemes |
| | - Unknown AID handling |
| | - Case insensitivity |
+---------------------------+--------------------------------+
| emv-parser.test.ts | TLV parser correctness |
| | - Simple TLV parsing |
| | - Multiple TLV entries |
| | - Nested/constructed tags |
| | - Tag lookup |
| | - Value/element extraction |
+---------------------------+--------------------------------+
| emv-utils.test.ts | Conversion utilities |
| | - Hex/Binary/Decimal |
| | - Padding functions |
| | - Hex to ASCII |
+---------------------------+--------------------------------+
| types.test.ts | Type & constant validation |
| | - NfcError constant values |
+---------------------------+--------------------------------+
Mocking:
react-native-nfc-manager is mocked in scanner tests
since it requires native modules not available in test env.
yarn test # vitest run
yarn typecheck # tsc --noEmit