credit_card_nfc_reader

Flutter plugin for reading contactless EMV payment cards on Android. Wraps devnied/EMV-NFC-Paycard-Enrollment using the published Maven Central dependency com.github.devnied.emvnfccard:library:3.2.0.

Requirements

  • Flutter 3.44+, Dart 3.8+, Java 17, Android SDK 36.
  • Android 7.0+ (API 24) with NFC enabled and a physical ISO-DEP card.
  • Android only; other platforms throw UnsupportedError.

The generated Android project uses Android Gradle Plugin 9.0.1 with the generated Flutter Kotlin configuration. Older Flutter/Gradle toolchains are not supported by this setup.

Installation

Add the package to your Flutter app:

dependencies:
  credit_card_nfc_reader: ^0.2.0

Run flutter pub get. The plugin manifest supplies the NFC permission and an optional NFC hardware feature. No runtime permission dialog is required. Set minSdk to at least 24 in your application's Android configuration.

Usage

import 'package:credit_card_nfc_reader/credit_card_nfc_reader.dart';
import 'package:flutter/services.dart';

final reader = EmvNfcReader();

Future<EmvCard?> scan() async {
  if (await reader.getStatus() != NfcStatus.available) return null;
  try {
    return await reader.readCard(
      timeout: const Duration(seconds: 30),
      readAllAids: true,
      readTransactions: false,
      readAllRecords: false,
    );
  } on PlatformException catch (error) {
    // Show error.code/error.message in your UI; allow retry.
    return null;
  }
}

// A separate button can cancel the pending read:
// await reader.cancel();

Invoke reading from a foreground screen, then hold the card at the NFC antenna until the future completes. One session is allowed across all reader instances. A session ends after a result, error, timeout, cancellation, activity detachment, or the app leaving the foreground. Rotation cancels the session; start a new read afterward. The timeout covers both waiting for a card and parsing it (1 ms to 5 minutes). Each ISO-DEP exchange defaults to a 5-second timeout, configurable up to 60 seconds. Parsing runs on a worker thread and closing the connection aborts outstanding communication.

Java configuration troubleshooting

If Flutter reports an AGP / SourceCompatibility error, inspect the underlying failure and run flutter doctor -v to check the Java runtime. This project already uses AGP 9.0.1. Java 26 caused JdkImageTransform / jlink to fail with this setup; the verified runtime is Java 17.

Select an installed JDK 17 (use its actual home directory):

flutter config --jdk-dir=/path/to/jdk-17

This is a machine-wide Flutter setting. Restart an open IDE after changing it, then run flutter build apk --debug from example.

Full inspection

final card = await reader.readCard(options: EmvReadOptions.full);
for (final app in card.applications) {
  final cda = app.interchangeProfile?.cdaSupported;
  final international = app.usageControl?.internationalUsageAllowed;
  final balance = app.offlineBalance; // Exact decimal string, never double.
  final tokenized = app.tokenized;
  final par = app.paymentAccountReference;
  final rawObjects = app.rawDataObjects; // Tag -> uppercase hex.
  final records = app.records;
  final log = app.transactionLog;
}
final completeResult = card.toJson(); // Nested JSON-safe immutable map.

options overrides the legacy readAllAids/readTransactions/readAllRecords arguments when provided. Existing common getters remain available, including expirationDate, label, transactions, and maskedCardNumber.

All 24 upstream result types have Dart models, including all 120 application getters: identity, directory selection, kernel resolution, AIP/AUC/CVM, issuer information, offline authentication credentials, mag-stripe profiles, tokenization, records, DOLs, transaction logs, counters, exact balances, CPLC, and GeldKarte. See the feature guide for test steps and API examples.

Byte arrays are uppercase hexadecimal strings. Enum values use the upstream constant names. Dates are yyyy-MM-dd; transaction times are HH:mm:ss. BigDecimal amounts are strings to preserve precision. Upstream float amounts remain doubles. Missing collection getters return empty collections; data and toJson() preserve the original nulls. Unknown integer values remain -1, except the backward-compatible transactionCounter getter returns null for unknown. Every model is immutable and its toString() omits card data.

The reader returns partial and empty EMV models for diagnostic inspection when no application was readable. Check state, applications, status words and raw records to distinguish that from a usable card. Non-ISO-DEP tags still raise unsupported_card.

Developer console

cd example
flutter run
  • Reader: 18 feature presets, every reader setting, parser mode, terminal country, custom DOL values, timeouts, replay JSON and native extension examples.
  • Results: feature groups, field-path filtering, raw/parsed values, reveal control and explicit full-JSON clipboard copy.
  • Tools: 16 standalone decoder examples with editable inputs.
  • Catalogs: upstream countries, currencies, card schemes and AIDs.

Run synthetic replay exercises the actual Android parser with the bundled fake Visa APDU script. Load all-fields UI fixture loads independent synthetic field examples to inspect the UI; it is deliberately not a coherent card profile or proof that a card supports all features. Both are labeled in the result view.

The example retains results in memory and does not automatically store or send them. Enabling trace/reveal or copying JSON can expose full card and APDU data. It does not perform payments, verify a PIN, or obtain the printed CVV. Decoded security capabilities and certificates do not mean authentication was performed.

Errors

PlatformException.code can be:

  • nfc_unavailable: device has no NFC adapter.
  • nfc_disabled: enable NFC in system settings.
  • no_activity: no Android activity is attached.
  • busy: a read is already active.
  • timeout: session deadline expired.
  • cancelled: cancelled explicitly or by the activity lifecycle.
  • unsupported_card: no ISO-DEP support.
  • communication_error: NFC connection or exchange failed.
  • read_error: parser failed.
  • nfc_error: reader mode could not be enabled.
  • invalid_argument: invalid native timeout; Dart validates it as ArgumentError.

The upstream parser may absorb some communication errors and return partial data; a transport failure therefore does not always surface as communication_error.

Example and validation

flutter analyze
flutter test
cd example
flutter test
flutter run

Use a physical NFC Android device for end-to-end validation. Automated tests cover the channel contract, result decoding, screen workflows, all native model getters, standalone decoders, terminal overrides, and real parser execution against synthetic APDUs. They cannot verify antenna behavior or issuer/card compatibility.

License

Apache-2.0. See LICENSE and NOTICE for upstream attribution.