capawesome-team / capacitor-plugins

⚡️ Community plugins for Capacitor. Supports Android, iOS and the Web.
https://capawesome.io/plugins/
233 stars 37 forks source link

bug(nfc): Cannot scan iso7816 Tag, need help... #124

Closed marchein closed 7 months ago

marchein commented 9 months ago

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:

Number of tags 1
iso7816(<NFCISO7816Tag: 0x2822bfc40>)
CONNECTED TO CARD
CARD TYPE: iso7816
Tag Identifier: 7 bytes
Tag identifier as NSData: {length = 7, bytes = 0x049c54b23e5080}
Tag Identifier as int: 36117027229047812
CARD-ID hex: 049c54b23e5080
App ID: 6259733
MensaNfcController.APP_ID.toByteArray(): [95, 132, 21]
readRequest 9 bytes
readRequest as NSData: {length = 9, bytes = 0x905a0000035f841500}
COMMAND TO CARD => 905a0000035f841500
CARD RESPONSE <= 
COMMAND TO CARD => 906c0000010100
CARD RESPONSE <= 1e000000
balance: 0.03, cardID: 36117027229047812

Session invalidated by user

Steps to reproduce:

  1. Execute the example
  2. Press scan card
  3. Scan card using a real device
  4. The issue appears

Related code:

See minimal, reproducible example: https://github.com/marchein/capacitor-nfc-techtype-issue

Other information:

Log output from Xcode using Ndef:

⚡️  To Native ->  Nfc addListener 122730965
⚡️  To Native ->  Nfc startScanSession 122730966
⚡️  TO JS undefined

⚡️  TO JS {"nfcTag":{"isWritable":false,"id":[4,156,84,178,62,80,128],"techTypes":["NDEF","MIFARE_DESFIRE"],"historicalBytes":[128],"maxSize":0}}
⚡️  To Native ->  Nfc stopScanSession 122730967
⚡️  TO JS undefined
⚡️  [log] - --------------- Output Card Info ---------------
⚡️  [log] - {"isWritable":false,"id":[4,156,84,178,62,80,128],"techTypes":["NDEF","MIFARE_DESFIRE"],"historicalBytes":[128],"maxSize":0}
⚡️  [log] - ATQA: undefined
⚡️  [log] - Application Data: undefined
⚡️  [log] - Barcode: undefined
⚡️  [log] - Can Make Read-Only: undefined
⚡️  [log] - DSF ID: undefined
⚡️  [log] - Hi Layer Response: undefined
⚡️  [log] - Historical Bytes: [128]
⚡️  [log] - ID: [4,156,84,178,62,80,128]
⚡️  [log] - Is Writable: false
⚡️  [log] - Manufacturer: undefined
⚡️  [log] - Max Size: 0
⚡️  [log] - Message: undefined
⚡️  [log] - Protocol Info: undefined
⚡️  [log] - Response Flags: undefined
⚡️  [log] - SAK: undefined
⚡️  [log] - System Code: undefined
⚡️  [log] - Tech Types: ["NDEF","MIFARE_DESFIRE"]
⚡️  [log] - Type: undefined
⚡️  [log] - --------------- Output Card Info END ---------------
⚡️  [log] - --------------- Card Identifier ---------------
⚡️  [log] - ID: 36117027229047812
⚡️  [log] - Hex ID: 049c54b23e5080
⚡️  [log] - --------------- Card Identifier END ---------------
⚡️  [log] - --------------- Send Command Select App --------------- 
⚡️  [log] - App ID: [95,132,21]
⚡️  [log] - App ID Bytes: [95,132,21]
⚡️  [log] - Select App Command: {"0":144,"1":90,"2":0,"3":0,"4":3,"5":95,"6":132,"7":21,"8":0}
⚡️  [log] - Select App Command Hex: 905a0000035f841500
⚡️  [log] - -------------------- Send NFC Request --------------------
⚡️  [log] - Tag: {"isWritable":false,"id":[4,156,84,178,62,80,128],"techTypes":["NDEF","MIFARE_DESFIRE"],"historicalBytes":[128],"maxSize":0}
⚡️  [log] - techType: NDEF
⚡️  [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 122730968
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."}
⚡️  [log] - {"errorMessage":"The NFC tag does not support the selected techType."}
⚡️  [log] - Response: {}
⚡️  [log] - Response Hex: 
⚡️  [log] - Response Int: NaN

Log output from Xcode using MifareDesfire:

⚡️  To Native ->  Nfc addListener 53278078
⚡️  To Native ->  Nfc startScanSession 53278079
⚡️  TO JS undefined
⚡️  TO JS {"nfcTag":{"historicalBytes":[128],"maxSize":0,"techTypes":["NDEF","MIFARE_DESFIRE"],"isWritable":false,"id":[4,156,84,178,62,80,128]}}
⚡️  To Native ->  Nfc stopScanSession 53278080
⚡️  TO JS undefined
⚡️  [log] - --------------- Output Card Info ---------------
⚡️  [log] - {"historicalBytes":[128],"maxSize":0,"techTypes":["NDEF","MIFARE_DESFIRE"],"isWritable":false,"id":[4,156,84,178,62,80,128]}
⚡️  [log] - ATQA: undefined
⚡️  [log] - Application Data: undefined
⚡️  [log] - Barcode: undefined
⚡️  [log] - Can Make Read-Only: undefined
⚡️  [log] - DSF ID: undefined
⚡️  [log] - Hi Layer Response: undefined
⚡️  [log] - Historical Bytes: [128]
⚡️  [log] - ID: [4,156,84,178,62,80,128]
⚡️  [log] - Is Writable: false
⚡️  [log] - Manufacturer: undefined
⚡️  [log] - Max Size: 0
⚡️  [log] - Message: undefined
⚡️  [log] - Protocol Info: undefined
⚡️  [log] - Response Flags: undefined
⚡️  [log] - SAK: undefined
⚡️  [log] - System Code: undefined
⚡️  [log] - Tech Types: ["NDEF","MIFARE_DESFIRE"]
⚡️  [log] - Type: undefined
⚡️  [log] - --------------- Output Card Info END ---------------
⚡️  [log] - --------------- Card Identifier ---------------
⚡️  [log] - ID: 36117027229047812
⚡️  [log] - Hex ID: 049c54b23e5080
⚡️  [log] - --------------- Card Identifier END ---------------
⚡️  [log] - --------------- Send Command Select App --------------- 
⚡️  [log] - App ID: [95,132,21]
⚡️  [log] - App ID Bytes: [95,132,21]
⚡️  [log] - Select App Command: {"0":144,"1":90,"2":0,"3":0,"4":3,"5":95,"6":132,"7":21,"8":0}
⚡️  [log] - Select App Command Hex: 905a0000035f841500
⚡️  [log] - -------------------- Send NFC Request --------------------
⚡️  [log] - Tag: {"historicalBytes":[128],"maxSize":0,"techTypes":["NDEF","MIFARE_DESFIRE"],"isWritable":false,"id":[4,156,84,178,62,80,128]}
⚡️  [log] - techType: MIFARE_DESFIRE
⚡️  [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 53278081
ERROR MESSAGE:  {"errorMessage":"Tag is not connected","message":"Tag is not connected"}
⚡️  [error] - {"errorMessage":"Tag is not connected","message":"Tag is not connected"}
⚡️  [log] - {"errorMessage":"Tag is not connected"}
⚡️  [log] - Response: {}
⚡️  [log] - Response Hex: 
⚡️  [log] - Response Int: NaN
⚡️  [log] - --------------- Send Command Read Value --------------- 
⚡️  [log] - -------------------- Send NFC Request --------------------
⚡️  [log] - Tag: {"historicalBytes":[128],"maxSize":0,"techTypes":["NDEF","MIFARE_DESFIRE"],"isWritable":false,"id":[4,156,84,178,62,80,128]}
⚡️  [log] - techType: MIFARE_DESFIRE
⚡️  [log] - Request: {"0":144,"1":108,"2":0,"3":0,"4":1,"5":1,"6":0}
⚡️  To Native ->  Nfc transceive 53278082
⚡️  [log] - Request Hex: 906c0000010100
⚡️  [log] - Request Int: 40651143902200060
⚡️  [log] - Request Bytes: [144,108,0,0,1,1,0]
ERROR MESSAGE:  {"errorMessage":"Tag is not connected","message":"Tag is not connected"}
⚡️  [error] - {"errorMessage":"Tag is not connected","message":"Tag is not connected"}
⚡️  [log] - {"errorMessage":"Tag is not connected"}
⚡️  [log] - Response: {}
⚡️  [log] - Response Hex: 
⚡️  [log] - Response Int: NaN
⚡️  [log] - --------------- Send Command Read Value END --------------- 
⚡️  [log] - --------------- Send Command Select App END --------------- 

Capacitor doctor:

💊   Capacitor Doctor  💊 

Latest Dependencies:

  @capacitor/cli: 5.6.0
  @capacitor/core: 5.6.0
  @capacitor/android: 5.6.0
  @capacitor/ios: 5.6.0

Installed Dependencies:

  @capacitor/android: not installed
  @capacitor/cli: 5.6.0
  @capacitor/ios: 5.6.0
  @capacitor/core: 5.6.0

[success] iOS looking great! 👌
robingenz commented 9 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: [...] });
marchein commented 9 months ago

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?

robingenz commented 9 months ago

Thank you for providing alle the information. I will take a look at it on the weekend.

robingenz commented 9 months ago

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]
});
marchein commented 9 months ago

Tried adding the polling option, which yielded the same result...

I will check the source code later, maybe I find something...

marchein commented 7 months ago

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.

robingenz commented 7 months ago

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.

marchein commented 7 months ago

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?

robingenz commented 7 months ago

Yay! 🙌 No, implementation is much easier on Android. Everything should already work there.

robingenz commented 7 months ago

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.

marchein commented 7 months ago

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 Screenshot 2024-02-28 um 15 26 26

robingenz commented 7 months ago

What do you mean exactly? This looks like normal log entries.

marchein commented 7 months ago

Well, nothing happens once I trigger the scan function on android, but it works now perfectly fine on iOS.

robingenz commented 7 months ago

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.

marchein commented 7 months ago

Apologies for any confusion; it appears there was a mix-up with the provided log output.

Screenshot 2024-02-28 um 16 04 30

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 ...

robingenz commented 7 months ago

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:

grafik

iOS:

grafik

robingenz commented 7 months ago

I might have found an issue in my plugin. I will take another look and get back to you.

marchein commented 7 months ago

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

robingenz commented 7 months ago

I discovered a problem with the plugin and fixed it but it was not relevant to this issue.

robingenz commented 7 months ago

How do you know which bytes to send? Do you have a specification or something similar?

robingenz commented 7 months ago

Okay, i have another idea. I get back to you.

robingenz commented 7 months ago

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!

marchein commented 7 months ago

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.

marchein commented 7 months ago

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);
      });
    }
  },
);
robingenz commented 7 months ago

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.