Improve NFC history readers and prepare production build
This commit is contained in:
@ -21,6 +21,7 @@ extension Dictionary where Key == String, Value == Any {
|
||||
@available(iOS 13.0, *)
|
||||
public class UnifiedNfcApi {
|
||||
var stationMap: [Int: Station] = [:]
|
||||
var felicaHistoryRetryCount = 0
|
||||
|
||||
func parseData() {
|
||||
stationMap = [
|
||||
@ -87,7 +88,7 @@ public class UnifiedNfcApi {
|
||||
var apduRunner = ApduRunner()
|
||||
|
||||
public init() {
|
||||
langCode = Locale.current.languageCode
|
||||
langCode = Locale.current.languageCode ?? "en"
|
||||
parseData()
|
||||
}
|
||||
|
||||
@ -124,9 +125,16 @@ public class UnifiedNfcApi {
|
||||
public func checkFelicaCard(tag: NFCFeliCaTag){
|
||||
readFelicaCard(tag: tag)
|
||||
}
|
||||
|
||||
private func logFelica(_ message: String) {
|
||||
_ = message
|
||||
// print("[FeliCa] \(message)")
|
||||
// debugLog("[FeliCa] \(message)")
|
||||
}
|
||||
|
||||
func readFelicaCard(tag: NFCFeliCaTag){
|
||||
let kmt = Emoney()
|
||||
logFelica("Start read | currentIDm=\(tag.currentIDm.map { String(format: "%02X", $0) }.joined()) | currentSystemCode=\(tag.currentSystemCode.map { String(format: "%02X", $0) }.joined())")
|
||||
|
||||
let serviceCode = Data([0x0B, 0x30])
|
||||
let blockList = Data([0x80, 0x00])
|
||||
@ -137,21 +145,27 @@ public class UnifiedNfcApi {
|
||||
) { (status1, status2, blockData, error) in
|
||||
|
||||
if let error = error {
|
||||
self.logFelica("Card info read failed | error=\(error.localizedDescription)")
|
||||
self.apduRunner.nfcApi?.stopCheckCard(message: "Gagal membaca: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
// Cek status keberhasilan dari kartu (0x00 0x00 berarti sukses)
|
||||
guard status1 == 0x00 && status2 == 0x00 else {
|
||||
self.logFelica("Card info read failed | status1=\(status1) | status2=\(status2)")
|
||||
self.apduRunner.nfcApi?.stopCheckCard(message: "Error Status: \(status1) \(status2)")
|
||||
return
|
||||
}
|
||||
for (index, data) in blockData.enumerated() {
|
||||
debugLog("Data Blok \(index): \(data.map { String(format: "%02X", $0) }.joined())")
|
||||
let hex = data.map { String(format: "%02X", $0) }.joined()
|
||||
self.logFelica("Card info block \(index) raw=\(hex)")
|
||||
if let cardNumberString = String(data: data, encoding: .utf8) {
|
||||
self.logFelica("Card number parsed=\(cardNumberString)")
|
||||
kmt.setCardLabel("KMT")
|
||||
kmt.setCardNumber(cardNumberString)
|
||||
self.readFelicaBalance(tag: tag, kmt: kmt)
|
||||
} else {
|
||||
self.logFelica("Card number parse failed for block \(index)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,12 +180,18 @@ public class UnifiedNfcApi {
|
||||
blockList: [blockList]
|
||||
) { (status1, status2, blockData, error) in
|
||||
if let error = error {
|
||||
self.logFelica("Balance read failed | error=\(error.localizedDescription)")
|
||||
self.apduRunner.nfcApi?.stopCheckCard(message: "Gagal membaca: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
if status1 == 0x00 && status2 == 0x00 {
|
||||
let cardBalance = [UInt8](blockData[0])
|
||||
guard let firstBlock = blockData.first, firstBlock.count >= 4 else {
|
||||
self.logFelica("Balance block missing or too short")
|
||||
return
|
||||
}
|
||||
self.logFelica("Balance raw=\(firstBlock.map { String(format: "%02X", $0) }.joined())")
|
||||
let cardBalance = [UInt8](firstBlock)
|
||||
var y: Int = 0
|
||||
for x in 0..<4 {
|
||||
y += Int(cardBalance[x]) << (x * 8)
|
||||
@ -180,12 +200,14 @@ public class UnifiedNfcApi {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = Locale(identifier: "id_ID")
|
||||
formatter.numberStyle = .decimal
|
||||
debugLog("balance")
|
||||
kmt.setBalance(y)
|
||||
if let balance = formatter.string(from: NSNumber(value: y)) {
|
||||
debugLog("Saldo: \(balance)") // Hasil contoh: "67.305.985"
|
||||
self.logFelica("Balance parsed=\(y) | formatted=\(balance)")
|
||||
}
|
||||
self.felicaHistoryRetryCount = 0
|
||||
self.readFelicaCardHistory(tag: tag, kmt: kmt)
|
||||
} else {
|
||||
self.logFelica("Balance read failed | status1=\(status1) | status2=\(status2)")
|
||||
// kmt.setTampilRiwayat(false)
|
||||
// self.apduRunner.callback?.complete(emoney: kmt)
|
||||
// self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
@ -210,34 +232,38 @@ public class UnifiedNfcApi {
|
||||
) { (status1, status2, blockData, error) in
|
||||
var riwayatList: [RiwayatCard] = []
|
||||
if let error = error {
|
||||
debugLog("Error: \(error.localizedDescription)")
|
||||
self.logFelica("History read failed | error=\(error.localizedDescription)")
|
||||
self.retryFelicaHistoryRead(tag: tag, kmt: kmt, reason: "error \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
if status1 == 0x00 && status2 == 0x00 {
|
||||
debugLog("Berhasil membaca 15 blok!")
|
||||
self.logFelica("History read success | blocks=\(blockData.count)")
|
||||
// blockData akan berisi array of Data, masing-masing 16 byte
|
||||
for (index, data) in blockData.enumerated() {
|
||||
let riwayat = RiwayatCard()
|
||||
var normal = true
|
||||
let rawHex = data.map { String(format: "%02X", $0) }.joined()
|
||||
self.logFelica("History block \(index) raw=\(rawHex)")
|
||||
guard data.count >= 12 else {
|
||||
self.logFelica("History block \(index) skipped | reason=too_short | count=\(data.count)")
|
||||
continue
|
||||
}
|
||||
let subId = data.subdata(in: 8..<10)
|
||||
|
||||
let uid = self.convert(bytes: [UInt8](subId))
|
||||
debugLog("station: \(uid)")
|
||||
|
||||
debugLog("Blok \(index): \(data.map { String(format: "%02X", $0) }.joined())")
|
||||
self.logFelica("History block \(index) stationId=\(uid)")
|
||||
if (uid == 0){
|
||||
normal = false
|
||||
}
|
||||
if data.count >= 13 {
|
||||
let transactionKind = self.felicaTransactionKind(for: [UInt8](data))
|
||||
let transactionKind = self.felicaTransactionKind(for: [UInt8](data), stationId: uid)
|
||||
riwayat.setProsesTipe(transactionKind.prosesTipe)
|
||||
riwayat.setTitle(transactionKind.title.localizeString(string: self.langCode!))
|
||||
debugLog("signature: \(transactionKind.signature)")
|
||||
debugLog(transactionKind.logLabel)
|
||||
self.logFelica("History block \(index) signature=\(transactionKind.signature) | kind=\(transactionKind.logLabel)")
|
||||
}
|
||||
if let station = self.stationMap[uid]{
|
||||
debugLog("station", station.name)
|
||||
self.logFelica("History block \(index) stationName=\(station.name)")
|
||||
riwayat.setLocationName(station.name.uppercased(with: .autoupdatingCurrent))
|
||||
}
|
||||
// let station = self.stationMap[uid]
|
||||
@ -247,14 +273,17 @@ public class UnifiedNfcApi {
|
||||
|
||||
let subData = data.subdata(in: 0..<4)
|
||||
|
||||
let date = self.getDate(data: [UInt8](subData))
|
||||
riwayat.setTransactionTime(date!)
|
||||
if let date = self.getDate(data: [UInt8](subData)) {
|
||||
riwayat.setTransactionTime(date)
|
||||
}
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "id_ID") // Format Indonesia
|
||||
formatter.dateFormat = "dd MMMM yyyy, HH:mm"
|
||||
|
||||
let dateString = formatter.string(from: date!)
|
||||
debugLog("Hasil Konversi: \(dateString)")
|
||||
if let date = riwayat.getTransationTime() {
|
||||
let dateString = formatter.string(from: date)
|
||||
self.logFelica("History block \(index) timestamp=\(dateString)")
|
||||
}
|
||||
|
||||
let amn = data.subdata(in: 4..<8)
|
||||
let amount = amn.withUnsafeBytes { $0.load(as: Int32.self).bigEndian }
|
||||
@ -283,11 +312,10 @@ public class UnifiedNfcApi {
|
||||
nformatter.locale = Locale(identifier: "id_ID")
|
||||
nformatter.numberStyle = .decimal
|
||||
if let balance = nformatter.string(from: NSNumber(value: amount)) {
|
||||
debugLog("amount: \(balance)") // Hasil contoh: "67.305.985"
|
||||
self.logFelica("History block \(index) amount=\(amount) | formatted=\(balance)")
|
||||
}
|
||||
debugLog("")
|
||||
} else {
|
||||
debugLog("RESKA PARKIR")
|
||||
self.logFelica("History block \(index) mode=RESKA_PARKIR")
|
||||
|
||||
let stringData = data.map { String(format: "%02X", $0) }.joined()
|
||||
|
||||
@ -304,9 +332,9 @@ public class UnifiedNfcApi {
|
||||
|
||||
let result = outputFormatter.string(from: date)
|
||||
riwayat.setTransactionTime(date)
|
||||
debugLog(result) // Hasil: 29-01-2026 16:00:44
|
||||
self.logFelica("History block \(index) timestamp=\(result)")
|
||||
} else {
|
||||
debugLog("Format string tidak cocok")
|
||||
self.logFelica("History block \(index) timestamp_parse_failed")
|
||||
}
|
||||
let amn = data.subdata(in: 8..<12)
|
||||
let amount = amn.withUnsafeBytes { $0.load(as: Int32.self).bigEndian }
|
||||
@ -318,26 +346,42 @@ public class UnifiedNfcApi {
|
||||
nformatter.locale = Locale(identifier: "id_ID")
|
||||
nformatter.numberStyle = .decimal
|
||||
if let balance = nformatter.string(from: NSNumber(value: amount)) {
|
||||
debugLog("amount: \(balance)") // Hasil contoh: "67.305.985"
|
||||
self.logFelica("History block \(index) amount=\(amount) | formatted=\(balance)")
|
||||
}
|
||||
}
|
||||
debugLog("")
|
||||
let title = riwayat.getTitle() ?? "-"
|
||||
let location = riwayat.getLocationName() ?? "-"
|
||||
self.logFelica("History block \(index) summary | title=\(title) | prosesTipe=\(riwayat.getProsesTipe()) | location=\(location)")
|
||||
riwayatList.append(riwayat)
|
||||
}
|
||||
kmt.setRiwayatList(riwayatList)
|
||||
kmt.setTampilRiwayat(true)
|
||||
self.logFelica("History parsed total=\(riwayatList.count)")
|
||||
self.apduRunner.callback?.complete(emoney: kmt)
|
||||
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
self.apduRunner.invalidateSession()
|
||||
} else {
|
||||
debugLog("Gagal. Status: \(status1), \(status2)")
|
||||
self.logFelica("History read failed | status1=\(status1) | status2=\(status2)")
|
||||
self.retryFelicaHistoryRead(tag: tag, kmt: kmt, reason: "status \(status1)-\(status2)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func retryFelicaHistoryRead(tag: NFCFeliCaTag, kmt: Emoney, reason: String) {
|
||||
if felicaHistoryRetryCount < 1 {
|
||||
felicaHistoryRetryCount += 1
|
||||
logFelica("Retrying history read | reason=\(reason)")
|
||||
readFelicaCardHistory(tag: tag, kmt: kmt)
|
||||
} else {
|
||||
apduRunner.callback?.complete(emoney: kmt)
|
||||
apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
|
||||
apduRunner.invalidateSession()
|
||||
}
|
||||
}
|
||||
|
||||
func getDate(data: [UInt8]) -> Date? {
|
||||
// 1. Tentukan TimeZone Jakarta
|
||||
let timeZone = TimeZone(identifier: "Asia/Jakarta")!
|
||||
let timeZone = TimeZone(identifier: "Asia/Jakarta") ?? .current
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = timeZone
|
||||
|
||||
@ -364,7 +408,7 @@ public class UnifiedNfcApi {
|
||||
func convert(bytes: [UInt8]) -> Int {
|
||||
switch bytes.count {
|
||||
case 0:
|
||||
fatalError("Data kosong")
|
||||
return 0
|
||||
case 1:
|
||||
return Int(bytes[0])
|
||||
case 2:
|
||||
@ -379,12 +423,17 @@ public class UnifiedNfcApi {
|
||||
}
|
||||
}
|
||||
|
||||
private func felicaTransactionKind(for bytes: [UInt8]) -> (prosesTipe: Int, title: String, logLabel: String, signature: String) {
|
||||
private func felicaTransactionKind(for bytes: [UInt8], stationId: Int) -> (prosesTipe: Int, title: String, logLabel: String, signature: String) {
|
||||
let signatureBytes = Array(bytes[10...12])
|
||||
let signature = signatureBytes.map { String(format: "%02X", $0) }.joined(separator: " ")
|
||||
|
||||
if stationId == 0 {
|
||||
return (1, "payment", "Parkir", signature)
|
||||
}
|
||||
|
||||
switch signatureBytes {
|
||||
case [0x00, 0x02, 0x00],
|
||||
[0x02, 0x01, 0x00],
|
||||
[0x03, 0x01, 0x00]:
|
||||
return (0, "topup", "Topup", signature)
|
||||
case [0x01, 0x01, 0x01],
|
||||
@ -392,14 +441,12 @@ public class UnifiedNfcApi {
|
||||
[0x03, 0x61, 0x01]:
|
||||
return (1, "payment", "Pembayaran", signature)
|
||||
default:
|
||||
let type = bytes[10]
|
||||
switch type {
|
||||
let terminalMarker = bytes[12]
|
||||
switch terminalMarker {
|
||||
case 0x00:
|
||||
return (0, "topup", "Topup (fallback)", signature)
|
||||
return (0, "topup", "Topup (fallback sig[2])", signature)
|
||||
case 0x01:
|
||||
return (1, "payment", "Pembayaran (fallback)", signature)
|
||||
case 0x03:
|
||||
return (1, "payment", "Pembayaran (fallback 0x03)", signature)
|
||||
return (1, "payment", "Pembayaran (fallback sig[2])", signature)
|
||||
default:
|
||||
return (1, "payment", "Other", signature)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user