Closed marchein closed 7 months ago
I'm not familiar with the project https://github.com/schorschii/MensaGuthaben-iOS but I just added support for Iso7816
on iOS.
Please test this dev version and let me know if it works:
npm i @capawesome-team/capacitor-nfc@5.0.2-dev.fb0564b.1704882185
You can now pass Iso7816
to the transceive(...)
method:
import { Nfc, NfcTagTechType } from '@capawesome-team/capacitor-nfc';
const result = await Nfc.transceive({ techType: NfcTagTechType.Iso7816, data: [...] });
Thank you very much.
Unfortunately I receive the message my tag would not support ISO7816, but it indeed does...
⚡️ [log] - Tag: {"isWritable":false,"techTypes":["NDEF","MIFARE_DESFIRE"],"historicalBytes":[128],"id":[4,138,90,178,62,80,128],"maxSize":0}
⚡️ [log] - techType: ISO_7816
⚡️ [log] - Request: {"0":144,"1":90,"2":0,"3":0,"4":3,"5":95,"6":132,"7":21,"8":0}
⚡️ [log] - Request Hex: 905a0000035f841500
⚡️ [log] - Request Int: 2.6628163300920766e+21
⚡️ [log] - Request Bytes: [144,90,0,0,3,95,132,21,0]
⚡️ To Native -> Nfc transceive 34592773
ERROR MESSAGE: {"message":"The NFC tag does not support the selected techType.","errorMessage":"The NFC tag does not support the selected techType."}
⚡️ [error] - {"message":"The NFC tag does not support the selected techType.","errorMessage":"The NFC tag does not support the selected techType."}
Using native swift code I can scan the card like this...
let nfcController = MensaNfcController()
let session = NFCTagReaderSession(pollingOption: .iso14443, delegate: nfcController!)
session?.alertMessage = NSLocalizedString("Please hold your Mensa card near the NFC sensor.", comment: "")
session?.begin()
...
import Foundation
import CoreNFC
class MensaNfcController : NSObject, NFCTagReaderSessionDelegate {
static var APP_ID : Int = 0x5F8415
static var FILE_ID : UInt8 = 1
func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
}
func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
print(error.localizedDescription)
}
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
print("Number of tags \(tags.count)")
for tag in tags {
communicate(session: session, tag: tag)
return
}
}
private func communicate(session: NFCTagReaderSession, tag: NFCTag) {
session.connect(to: tag) { (error: Error?) in
if(error != nil) {
print("CONNECTION ERROR: "+error!.localizedDescription)
session.invalidate(errorMessage: NSLocalizedString("Connection error:", comment: "") + " " + error!.localizedDescription)
return
}
print("CONNECTED TO CARD")
// read id based on card type
var idData = Data()
var idInt = 0
if case let NFCTag.iso7816(tag) = tag {
print("CARD TYPE: iso7816")
idData = tag.identifier
idInt = self.idDataToInt(idData)
print("Tag Identifier: \(tag.identifier)")
print("Tag identifier as NSData: \(tag.identifier as NSData)")
print("Tag Identifier as int: \(idInt)")
} else {
print("INVALID CARD TYPE: " + String(describing:tag))
session.invalidate(errorMessage: NSLocalizedString("Invalid card type:", comment: "") + " " + String(describing:tag))
return
}
// display card ID
print("CARD-ID hex: "+idData.hexEncodedString())
// 1st command : select app
let appIdByteArray = MensaNfcController.APP_ID.toByteArray()
print("App ID: \(MensaNfcController.APP_ID)")
print("MensaNfcController.APP_ID.toByteArray(): \(appIdByteArray)")
let readRequest = self.compileNfcRequest(
command: 0x5a, // command : select app
parameter: appIdByteArray
)
print("readRequest \(readRequest)")
print("readRequest as NSData: \(readRequest as NSData)")
self.send(
tag: tag,
data: readRequest,
completion: { (data1) -> () in
// 2nd command : read value (balance)
self.send(
tag: tag,
data: self.compileNfcRequest(
command: 0x6c, // command : read value
parameter: [MensaNfcController.FILE_ID] // file id : 1
),
completion: { (data2) -> () in
// parse and display balance response
var trimmedData = data2
trimmedData.removeLast()
trimmedData.removeLast()
trimmedData.reverse()
let currentBalanceRaw = [UInt8](trimmedData).toInt()
let currentBalanceValue : Double = self.intToEuro(value:currentBalanceRaw)
print("balance: \(currentBalanceValue), cardID: \(String(idInt))")
session.invalidate()
}
)
}
)
}
}
private func send(tag:NFCTag, data:Data, completion:@escaping (_ data: Data)->()) {
print("COMMAND TO CARD => "+data.hexEncodedString())
guard case let NFCTag.iso7816(tag) = tag else {
return
}
tag.sendCommand(apdu: NFCISO7816APDU(data: data)!, completionHandler: { (data:Data, sw1: UInt8, sw2: UInt8, error:Error?) in
if(error != nil) {
print("COMMAND ERROR: "+error!.localizedDescription)
return
}
print("CARD RESPONSE <= "+data.hexEncodedString())
completion(data)
})
}
private func compileNfcRequest(command: UInt8, parameter: [UInt8]?) -> Data {
var buff : [UInt8] = []
buff.append(0x90)
buff.append(command)
buff.append(0x00)
buff.append(0x00)
if(parameter != nil) {
buff.append(UInt8(parameter!.count))
for p in parameter! {
buff.append(p)
}
}
buff.append(0x00)
return Data(buff)
}
private func idDataToInt(_ d:Data) -> Int {
var idData = d
if(idData.count == 7) {
idData.append(UInt8(0))
}
return idData.withUnsafeBytes {
$0.load(as: Int.self)
}
}
private func intToEuro(value:Int) -> Double {
return (Double(value)/1000).rounded(toPlaces: 2)
}
}
extension [UInt8] {
func toInt() -> Int {
var rawValue : Int = 0
for byte in self {
rawValue = rawValue << 8
rawValue = rawValue | Int(byte)
}
return rawValue
}
}
extension Int {
func toByteArray() -> [UInt8] {
var buf : [UInt8] = [];
buf.append( UInt8((self & 0xFF0000) >> 16) )
buf.append( UInt8((self & 0xFF00) >> 8) )
buf.append( UInt8(self & 0xFF) )
return buf
}
}
extension Data {
struct HexEncodingOptions: OptionSet {
let rawValue: Int
static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
}
func hexEncodedString(options: HexEncodingOptions = []) -> String {
let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
return map { String(format: format, $0) }.joined()
}
}
extension Double {
// rounds the double to decimal places value
func rounded(toPlaces places:Int) -> Double {
let divisor = pow(10.0, Double(places))
return (self * divisor).rounded() / divisor
}
}
So the scanning gets initialized with iso14443 polling option. This means that the NFC reader session is configured to specifically look for and communicate with NFC tags that adhere to the ISO/IEC 14443 standard. Maybe this is missing here?
Thank you for providing alle the information. I will take a look at it on the weekend.
Unfortunately I receive the message my tag would not support ISO7816, but it indeed does...
That's strange. I basically do exactly the same thing in the plugin. Have a look at the source code of the CapawesomeTeamCapacitorNfc
pod in XCode. Maybe you will find the problem. I will try to order a card with this tech type to test it.
This means that the NFC reader session is configured to specifically look for and communicate with NFC tags that adhere to the ISO/IEC 14443 standard. Maybe this is missing here?
You can pass this option via the StartScanSessionOptions
:
import { Nfc, PollingOption } from '@capawesome-team/capacitor-nfc';
await Nfc.startScanSession({
pollingOptions: [PollingOption.iso14443]
});
Tried adding the polling option, which yielded the same result...
I will check the source code later, maybe I find something...
I tried debugging the plugin code, but my card is recognized as <NFCMiFareTag: 0x283f76240>, but it should be detected like this iso7816(<NFCISO7816Tag: 0x280567540>)...
Do you have any clue what could be causing this?
If you are having trouble finding a card, I can gladly provide you one of my development cards.
From the Core NFC documentation:
The NFCTagReaderSessionDelegate receives an object that conforms to the NFCMiFareTag protocol when the NFCTagReaderSession detects a compatible tag. However, if you include the application identifier D2760000850101—the identifier for the NDEF application on MIFARE® DESFire® tags (NFC Forum T4T tag platform)—in the com.apple.developer.nfc.readersession.iso7816.select-identifiers array of your Info.plist file, the reader session sends the delegate an NFCISO7816Tag object when it finds a tag matching the identifier.
It sounds like you should include this application identifier.
Wow, that including the changes you did in 5.0.2-dev.fb0564b.1704882185 did actually work.
Thank you very much!
I think, now the changes need to be done to android as well if I am correct?
Yay! 🙌 No, implementation is much easier on Android. Everything should already work there.
Btw: Would you be willing to share your implementation with me? I am working on the DHBW VS app and would love to implement this feature (read the current balance of your mensa card) there as well.
Once I cleaned the mess up, sure!
But sadly android does nothing really...
2024-02-28 15:25:35.192 31353-31353 Capacitor/Plugin com.jamsoftware.mensaplan V To native (Capacitor plugin): callbackId: 9729829, pluginId: Nfc, methodName: addListener 2024-02-28 15:25:35.193 31353-31353 Capacitor com.jamsoftware.mensaplan V callback: 9729829, pluginId: Nfc, methodName: addListener, methodData: {"eventName":"nfcTagScanned"} 2024-02-28 15:25:35.200 31353-31353 Capacitor/Plugin com.jamsoftware.mensaplan V To native (Capacitor plugin): callbackId: 9729830, pluginId: Nfc, methodName: startScanSession 2024-02-28 15:25:35.201 31353-31353 Capacitor com.jamsoftware.mensaplan V callback: 9729830, pluginId: Nfc, methodName: startScanSession, methodData: {"pollingOptions":["iso14443"]} 2024-02-28 15:25:35.229 31353-31353 Capacitor/Console com.jamsoftware.mensaplan I File: http://10.0.11.103:8100/overview - Line 284 - Msg: undefined
Edit: Wow, this is ugly... Check this screenshot of my logcat
What do you mean exactly? This looks like normal log entries.
Well, nothing happens once I trigger the scan function on android, but it works now perfectly fine on iOS.
Nothing happens visually under Android. You have to implement any UI yourself. It's not like iOS, where a modal opens.
Regardless of this, you must update the value in the data
property for Android.
On iOS, you have to use the iso15693RequestFlags
and iso15693CommandCode
properties.
On Android, you have to provide FLAGS, CMD and PARAMETER bytes via the data
property (see Android docs).
I plan to unify this in the future, but it's not that easy because the platforms simply handle it differently.
Apologies for any confusion; it appears there was a mix-up with the provided log output.
The current challenge I'm encountering is accurately reflected in this screenshot. For further context, I've updated my example project, which can be found at this GitHub repository.
Additionally, I've come to realize that Android does not inherently handle UI interactions in the same manner as iOS, which I previously overlooked. As a result, I'm delving into the Android documentation to ensure proper implementation of UI elements.
Furthermore, I must admit to some confusion regarding your mention of flags, CMD, and parameter bytes in the data property. I'm currently unclear on how these relate to the issue at hand and would appreciate further clarification.
Thank you ...
I changed the NfcTagTechType
to IsoDep
on Android because Iso7816
is only available on iOS, see docs.
const techType = Capacitor.getPlatform() === 'ios' ? NfcTagTechType.Iso7816 : NfcTagTechType.IsoDep;
I just found an old card from my university and was able to test it. But I still get different results on Android and iOS. 🤔
Android:
iOS:
I might have found an issue in my plugin. I will take another look and get back to you.
You are correct, thanks.
As far as NFC on android goes, I am not that experienced at all.
Maybe this helps with debugging your problem? https://github.com/astarub/campus_app/blob/da514b8ea02d369b34a46d69b6a4ddb42922a90b/android/app/src/main/kotlin/de/asta_bochum/campus_app/PopupActivity.kt
I discovered a problem with the plugin and fixed it but it was not relevant to this issue.
How do you know which bytes to send? Do you have a specification or something similar?
Okay, i have another idea. I get back to you.
Okay, i've fixed it for Android too. The issue was a duplicate of https://github.com/capawesome-team/capacitor-nfc/discussions/38#discussioncomment-7754303.
Please give this dev build a try:
npm i @capawesome-team/capacitor-nfc@5.1.0-dev.7d38c01.1709239536
There are some breaking changes. I updated the capacitor-welcome.js
file:
import { Capacitor } from '@capacitor/core';
import { SplashScreen } from '@capacitor/splash-screen';
import { NfcUtils } from '@capawesome-team/capacitor-nfc';
import { Nfc, NfcTagTechType, PollingOption } from '@capawesome-team/capacitor-nfc';
window.customElements.define(
"capacitor-welcome",
class extends HTMLElement {
constructor() {
super();
SplashScreen.hide();
const root = this.attachShadow({ mode: "open" });
root.innerHTML = `
<main>
<h1 style="margin-top: 64px;">Capacitor NFC Issue</h1>
<button id="scan-card">Scan Card</button>
</main>
`;
}
async scanCard(nfcTag) {
console.log("--------------- Output Card Info ---------------");
console.log(nfcTag);
if (nfcTag.id !== undefined) {
let id = this.getCardIdentifier(nfcTag.id);
let hexId = this.hexEncodedString(nfcTag.id);
console.log("ID:", id);
console.log("Hex ID:", hexId);
const nfcUtils = new NfcUtils();
let selectAppCommand = nfcUtils.convertHexToBytes({
hex: '0x905A0000035F841500'
}).bytes;
const techType = Capacitor.getPlatform() === 'ios' ? NfcTagTechType.Iso7816 : NfcTagTechType.IsoDep;
await Nfc.connect({
techType
});
await this.sendNfcRequest(nfcTag, selectAppCommand, techType).then(async (response) => {
let readValueRequest = nfcUtils.convertHexToBytes({
hex: '0x906C0000010100'
}).bytes;
await this.sendNfcRequest(nfcTag, readValueRequest, techType).then(async (response) => {
Nfc.close();
Nfc.stopScanSession();
let trimmedData = Array.from(response);
trimmedData.pop();
trimmedData.pop();
trimmedData.reverse();
let currentBalanceRaw = this.bytesToInt(trimmedData);
console.log("Current Balance Raw:", currentBalanceRaw);
let currentBalanceValue = this.intToEuro(currentBalanceRaw);
this.lastCredit = currentBalanceValue;
console.log("currentBalanceValue:", currentBalanceValue);
});
});
}
}
// Send an NFC request to the tag
async sendNfcRequest(tag, request, techType) {
console.log("-------------------- Send NFC Request --------------------");
console.log("Tag:", tag);
console.log("Request:", request);
console.log("techType:", techType);
const dataAsNumberArray = Array.from(request);
try {
const transceiveResult = await Nfc.transceive({
techType: techType,
data: dataAsNumberArray
});
console.log("Transceive Result:", transceiveResult);
// Extract the 'response' property from TransceiveResult
const response = transceiveResult.response || [];
console.log("Request response:", response);
return new Uint8Array(response);
} catch (error) {
console.log(error);
return new Uint8Array();
}
}
// Convert bytes to a hex-encoded string
hexEncodedString(bytes) {
return bytes.map(byte => byte.toString(16).padStart(2, '0')).join('');
}
// Convert bytes to an integer
bytesToInt(bytes) {
return parseInt(this.hexEncodedString(bytes), 16);
}
// Convert an integer to bytes
intToBytes(value) {
return [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
}
// Compile an NFC request command
compileNfcRequest(command, parameter) {
const buff = [0x90, command, 0x00, 0x00];
if (parameter != null) {
buff.push(parameter.length ?? 0);
buff.push(...parameter);
}
buff.push(0x00);
return new Uint8Array(buff);
}
// Get the card identifier from a byte array
getCardIdentifier(byteArray) {
let intValue = 0n;
for (let i = byteArray.length - 1; i >= 0; i--) {
intValue = (intValue << 8n) | BigInt(byteArray[i]);
}
return intValue.toString();
}
connectedCallback() {
const self = this;
const read = async () => {
return new Promise((resolve) => {
Nfc.addListener('nfcTagScanned', async (event) => {
resolve(event.nfcTag);
});
Nfc.startScanSession({
pollingOptions: [PollingOption.iso14443]
});
});
};
self.shadowRoot
.querySelector("#scan-card")
.addEventListener("click", async function (event) {
read().then(async (nfcTag) => {
await self.scanCard(nfcTag);
});
});
}
},
);
Let me know if it works!
Wow, thanks! This works for Android, but like you said, it has breaking changes, as it breaks iOS.
I look into it, if it is possible to use this for android and iOS at the same time.
I was able to update the code to also support for iOS:
import { Capacitor } from '@capacitor/core';
import { SplashScreen } from '@capacitor/splash-screen';
import { NfcUtils } from '@capawesome-team/capacitor-nfc';
import { Nfc, NfcTagTechType, PollingOption } from '@capawesome-team/capacitor-nfc';
window.customElements.define(
"capacitor-welcome",
class extends HTMLElement {
constructor() {
super();
SplashScreen.hide();
const root = this.attachShadow({ mode: "open" });
root.innerHTML = `
<main>
<h1 style="margin-top: 64px;">Capacitor NFC Issue</h1>
<button id="scan-card">Scan Card</button>
</main>
`;
}
async scanCard(nfcTag) {
if (nfcTag.id !== undefined) {
let id = this.getCardIdentifier(nfcTag.id);
let hexId = this.hexEncodedString(nfcTag.id);
console.log("ID:", id);
console.log("Hex ID:", hexId);
var readValueRequest = new Uint8Array();
const techType = Capacitor.getPlatform() === 'ios' ? NfcTagTechType.Iso7816 : NfcTagTechType.IsoDep;
if (Capacitor.getPlatform() === 'ios') {
let appId = 0x5F8415;
let appIdByteArray = this.intToBytes(appId);
let selectAppCommand = this.compileNfcRequest(0x5a, appIdByteArray);
await this.sendNfcRequest(nfcTag, selectAppCommand, techType);
let readValueCommand = 0x6c;
readValueRequest = this.compileNfcRequest(readValueCommand, [0x01]);
} else {
const nfcUtils = new NfcUtils();
let selectAppCommand = nfcUtils.convertHexToBytes({
hex: '0x905A0000035F841500'
}).bytes;
await Nfc.connect({
techType
});
await this.sendNfcRequest(nfcTag, selectAppCommand, techType);
readValueRequest = nfcUtils.convertHexToBytes({
hex: '0x906C0000010100'
}).bytes;
}
const readValueResponse = await this.sendNfcRequest(nfcTag, readValueRequest, techType);
if (Capacitor.getPlatform() === 'android') {
Nfc.close();
}
Nfc.stopScanSession();
let trimmedData = Array.from(readValueResponse);
trimmedData.pop();
trimmedData.pop();
trimmedData.reverse();
let currentBalanceRaw = this.bytesToInt(trimmedData);
let currentBalanceValue = this.intToEuro(currentBalanceRaw);
console.log("currentBalanceValue:", currentBalanceValue);
}
}
// Send an NFC request to the tag
async sendNfcRequest(tag, request, techType) {
console.log("Tag:", tag);
console.log("Request:", request);
console.log("techType:", techType);
const dataAsNumberArray = Array.from(request);
try {
const transceiveResult = await Nfc.transceive({
techType: techType,
data: dataAsNumberArray
});
// Extract the 'response' property from TransceiveResult
const response = transceiveResult.response || [];
return new Uint8Array(response);
} catch (error) {
console.log(error);
return new Uint8Array();
}
}
// Convert bytes to a hex-encoded string
hexEncodedString(bytes) {
return bytes.map(byte => byte.toString(16).padStart(2, '0')).join('');
}
// Convert bytes to an integer
bytesToInt(bytes) {
return parseInt(this.hexEncodedString(bytes), 16);
}
// Convert an integer to bytes
intToBytes(value) {
return [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
}
intToEuro(value) {
return +(value / 1000).toFixed(2);
}
// Compile an NFC request command
compileNfcRequest(command, parameter) {
const buff = [0x90, command, 0x00, 0x00];
if (parameter != null) {
buff.push(parameter.length ?? 0);
buff.push(...parameter);
}
buff.push(0x00);
return new Uint8Array(buff);
}
// Get the card identifier from a byte array
getCardIdentifier(byteArray) {
let intValue = 0n;
for (let i = byteArray.length - 1; i >= 0; i--) {
intValue = (intValue << 8n) | BigInt(byteArray[i]);
}
return intValue.toString();
}
connectedCallback() {
const self = this;
const read = async () => {
return new Promise((resolve) => {
Nfc.addListener('nfcTagScanned', async (event) => {
resolve(event.nfcTag);
});
Nfc.startScanSession({
pollingOptions: [PollingOption.iso14443]
});
});
};
self.shadowRoot.querySelector("#scan-card").addEventListener("click", async function () {
const nfcTag = await read();
await self.scanCard(nfcTag);
});
}
},
);
Wow, thanks! This works for Android, but like you said, it has breaking changes, as it breaks iOS.
You just need to skip the Nfc.connect(...)
and Nfc.close(...)
calls on iOS. That's it.
In this case i will close this issue. Thank you for reporting it. The bug fix will be officially released with version 6.0.0
.
Plugin version:
"@capawesome-team/capacitor-nfc": "^5.0.2"
Platform(s):
"@capacitor/ios": "^5.6.0",
Current behavior:
I have a iso7816 NFC card, which I need to scan. Tried tech type MifareDesfire and Ndef, but no luck, see capawesome-team/capacitor-nfc#43...
Expected behavior:
Using a similar project to what I am doing I am able to scan the card correctly https://github.com/schorschii/MensaGuthaben-iOS
Log output from Xcode using MensaGuthaben-iOS app while scanning:
Steps to reproduce:
Related code:
See minimal, reproducible example: https://github.com/marchein/capacitor-nfc-techtype-issue
Other information:
Log output from Xcode using Ndef:
Log output from Xcode using MifareDesfire:
Capacitor doctor: