From 1dc293c697a3bd14e6deb75c4ddab7ea3cb474cd Mon Sep 17 00:00:00 2001 From: Wira Irawan Date: Fri, 1 May 2026 21:44:59 +0700 Subject: [PATCH] Update Flazz card history parsing --- Emoney Info/Classes/api/BcaFlazzApi.swift | 608 ++++++++---------- Emoney Info/Classes/api/nfc/RiwayatCard.swift | 5 +- .../Classes/smartCard/CommonConstants.swift | 7 +- Emoney Info/HistoryView.swift | 23 +- Emoney Info/HomeView.swift | 4 +- 5 files changed, 285 insertions(+), 362 deletions(-) mode change 100755 => 100644 Emoney Info/Classes/api/BcaFlazzApi.swift diff --git a/Emoney Info/Classes/api/BcaFlazzApi.swift b/Emoney Info/Classes/api/BcaFlazzApi.swift old mode 100755 new mode 100644 index b569a02..7c67fab --- a/Emoney Info/Classes/api/BcaFlazzApi.swift +++ b/Emoney Info/Classes/api/BcaFlazzApi.swift @@ -9,382 +9,286 @@ import Foundation import CoreNFC public class BcaFlazzApi : UnifiedNfcApi { - var emoney : Emoney = Emoney() - var riwayatList: [RiwayatCard] = [] - var start = 0 - var finishV2 = 5 - var finish2V202 = 256 - var finishV1 = 16 - var mapv1 : String = "" - var mapv2 : String = "" - + private let emoney: Emoney = Emoney() + private var ef84Records: [RiwayatCard] = [] + private var extendedRecords: [RiwayatCard] = [] + + private let ef84MaxSlots = 10 + private let extendedMaxRecords = 256 + public override init() {} - - public func checkFlazzCard(){ - apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU01, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ + + public func checkFlazzCard() { + selectDirectoryFile(useFallbackLe: false) + } + + private func selectDirectoryFile(useFallbackLe: Bool) { + let command = NFCISO7816APDU( + instructionClass: 0x00, + instructionCode: 0xA4, + p1Parameter: 0x01, + p2Parameter: 0x00, + data: Data([0x02, 0x00]), + expectedResponseLength: useFallbackLe ? 256 : -1 + ) + + apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in + if response.sw1 == 0x90 && response.sw2 == 0x00 { self.emoney.setCardLabel("BCA Flazz") self.getCardNumber() + } else if !useFallbackLe { + self.selectDirectoryFile(useFallbackLe: true) } else { self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!)) } }) } - - private func getCardNumber(){ - apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU02, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ - let raw = String(data: response.getData(), encoding: .isoLatin1) - let start = raw!.firstIndex(of: ";")?.utf16Offset(in: raw!) - let end = raw!.firstIndex(of: "=")?.utf16Offset(in: raw!) - self.emoney.setCardNumber(String((raw?.subString(from: (start! + 1), to: end!))!)) - self.getBalance() - } else { - self.apduRunner.invalidateSession() - } - }) - } - - private func getBalance(){ - apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU03, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ - let raw = response.getData().hexEncodedString() - let balance = raw.subString(from: 2, to: 8) - self.emoney.setBalance(balance.hex2decimal()) - self.checkLog() + + private func getCardNumber() { + apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU02, completionHandler: { response in + if response.sw1 == 0x90 && response.sw2 == 0x00 { + guard let raw = String(data: response.getData(), encoding: .isoLatin1) else { + self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!)) + return + } + + if let cardNumber = self.extractPan(from: raw) { + self.emoney.setCardNumber(cardNumber) + self.getBalance() + } else { + self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!)) + } } else { self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!)) } }) } - - private func checkLog(){ - apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU04, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ - debugLog("log v2") -// self.updateScreen() - self.getLogV2step01(index: self.start) - } else { - debugLog("log v1") -// self.updateScreen() - self.getLogV1step01(index: self.start) - } - }) - } - - private func getLogV2step01(index : Int){ - debugLog("log getLogV2step01") - //00 B0 85 00 78 - let st = index*60 - let hex = String(st, radix: 16) - let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB0, p1Parameter : 0x85, p2Parameter : hex.hex2byte().bytes.first!, data : Data(), expectedResponseLength : 120) - apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ - debugLog("log data", response.getData().hexEncodedString()) - - - self.mapv1.append(response.getData().hexEncodedString()) - self.start+=1 - if (self.start < (self.finishV2)){ - self.getLogV2step01(index: self.start) - } else { - //mapping first - self.parseLogV201() - self.start = 0 - self.getLogV2step02(index: self.start) - } - } else { - self.parseLogV201() - self.start = 0 - self.getLogV2step02(index: self.start) - } - }) - } - - private func getLogV1step01(index : Int){ - debugLog("log getLogV1step01") - //00 b0 84 00 3c - let st = index*15 - let hex = String(st, radix: 16) - let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB0, p1Parameter : 0x84, p2Parameter : hex.hex2byte().bytes.first!, data : Data(), expectedResponseLength : 60) - apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ - self.mapv1.append(response.getData().hexEncodedString()) - self.start+=1 - if (self.start < (self.finishV1)){ - self.getLogV1step01(index: self.start) - } else { - self.start = 0 - self.getLogV1step02(index: self.start) - } - } else { - self.start = 0 - self.getLogV1step02(index: self.start) - } - }) - } - - private func getLogV1step02(index : Int){ - debugLog("log getLogV1step02") - //00b085003c - let st = index*15 - let hex = String(st, radix: 16) - let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB0, p1Parameter : 0x85, p2Parameter : hex.hex2byte().bytes.first!, data : Data(), expectedResponseLength : 60) - apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ - self.mapv1.append(response.getData().hexEncodedString()) - self.start+=1 - if (self.start < (self.finishV1)){ - self.getLogV1step02(index: self.start) - } else { - self.parseLogV101() - self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending }) - if (self.riwayatList.count > 0){ - self.emoney.setRiwayatList(self.riwayatList) - self.emoney.setTampilRiwayat(true) - } - self.updateScreen() - self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!) - self.apduRunner.invalidateSession() + private func getBalance() { + apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU03, completionHandler: { response in + if response.sw1 == 0x90 && response.sw2 == 0x00 { + let bytes = response.getData().bytes + guard bytes.count >= 4 else { + self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!)) + return } - } else { - self.parseLogV101() - self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending }) - if (self.riwayatList.count > 0){ - self.emoney.setRiwayatList(self.riwayatList) - self.emoney.setTampilRiwayat(true) - } - self.updateScreen() - self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!) - self.apduRunner.invalidateSession() + self.emoney.setBalance(self.uint32BE(bytes, offset: 0)) + self.readHistoryCheck() + } else { + self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!)) } }) } - - private func getLogV2step02(index : Int){ - debugLog("log getLogV2step02") - //00b08400f0 - let st = index*60 - let hex = String(st, radix: 16) - let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB0, p1Parameter : 0x84, p2Parameter : hex.hex2byte().bytes.first!, data : Data(), expectedResponseLength : 240) - apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ - self.mapv1.append(response.getData().hexEncodedString()) - self.start+=1 - if (self.start < (self.finishV2)){ - self.getLogV2step02(index: self.start) - } else { - self.start = 0 - self.getLogV2step03() - } - } else { - self.start = 0 - self.getLogV2step03() - } - }) - } - - private func getLogV2step03(){ - debugLog("log getLogV2step03") - //00 84 00 00 08 - let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0x84, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(), expectedResponseLength : 8) - apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ - self.getLogV2step04(data: response.getData()) - } else { - self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!) - self.apduRunner.invalidateSession() - } - }) - } - - private func getLogV2step04(data: Data){ - debugLog("log getLogV2step04") - //90 32 03 00 0A 0801 0000000000000000 29 - let send = "0801" + data.hexEncodedString() - let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x32, p1Parameter : 0x03, p2Parameter : 0x00, data : Data(_: send.hex2byte()), expectedResponseLength : 41) - apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ - self.getLogV2step05(index: self.start) - } else { - self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!) - self.apduRunner.invalidateSession() - } - }) - } - - private func getLogV2step05(index : Int){ - debugLog("log getLogV2step05") - //00 B0 89 00 40 - let st = index - let hex = String(st, radix: 16) - let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB0, p1Parameter : 0x89, p2Parameter : hex.hex2byte().bytes.first!, data : Data(), expectedResponseLength : 64) - apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ - self.mapv2.append(response.getData().hexEncodedString()) - self.start+=1 - if (self.start < (self.finish2V202)){ - self.getLogV2step05(index: self.start) - } else { - self.start = 0 - self.getLogV2step06(index: self.start) - } - } else { - self.start = 0 - self.getLogV2step06(index: self.start) - } - }) - } - - private func getLogV2step06(index : Int){ - debugLog("log getLogV2step06") - //90 32 03 00 01 00 20 - let st = index - let hex = String(st, radix: 16) - let BCAV2_LOG = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x32, p1Parameter : 0x03, p2Parameter : 0x00, data : Data(_: hex.hex2byte()), expectedResponseLength : 32) - apduRunner.exchangeApdu(apduCommand: BCAV2_LOG, completionHandler: {response in - if (response.sw1 == 0x90 && response.sw2 == 0x00){ - self.mapv2.append(response.getData().hexEncodedString()) - self.start+=1 - if (self.start < (self.finish2V202)){ - self.getLogV2step06(index: self.start) - } else { - //mapping the data - self.parseLogV202() - self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending }) - - if (self.riwayatList.count > 0){ - self.emoney.setRiwayatList(self.riwayatList) - self.emoney.setTampilRiwayat(true) - } - self.updateScreen() - self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!) - self.apduRunner.invalidateSession() - } - } else { - //mapping the data - self.parseLogV202() - self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending }) - if (self.riwayatList.count > 0){ - self.emoney.setRiwayatList(self.riwayatList) - self.emoney.setTampilRiwayat(true) - } - self.updateScreen() - self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!) - self.apduRunner.invalidateSession() - } + private func readHistoryCheck() { + apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU04, completionHandler: { response in + let shouldReadExtended = response.sw1 == 0x90 && response.sw2 == 0x00 + self.readEf84Record(slot: 0, shouldReadExtended: shouldReadExtended) }) } - - private func parseLogV101(){ - debugLog("log parseLogV101") - let logs = self.mapv1.trimmingCharacters(in: .whitespacesAndNewlines) - if (logs.count % 120 != 0){ + + private func readEf84Record(slot: Int, shouldReadExtended: Bool) { + guard slot < ef84MaxSlots else { + if shouldReadExtended { + getChallenge() + } else { + finishReading() + } return } - let total = logs.count/120 - for i in 0.. 0){ - riwayatList.append(riwayat) - } - } + }) } - - private func parseLogV201(){ - debugLog("log parseLogV201") - let logs = self.mapv1.trimmingCharacters(in: .whitespacesAndNewlines) - if (logs.count % 120 != 0){ + + private func getChallenge() { + let command = NFCISO7816APDU( + instructionClass: 0x00, + instructionCode: 0x84, + p1Parameter: 0x00, + p2Parameter: 0x00, + data: Data(), + expectedResponseLength: 8 + ) + + apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in + if response.sw1 == 0x90 && response.sw2 == 0x00 { + self.readExtendedRecord(index: 0) + } else { + self.finishReading() + } + }) + } + + private func readExtendedRecord(index: Int) { + guard index < extendedMaxRecords else { + finishReading() return } - let total = logs.count/120 - for i in 0.. 0){ - riwayatList.append(riwayat) - } - } + }) } - - private func parseLogV202(){ - debugLog("log parseLogV202") - let logs = self.mapv2.trimmingCharacters(in: .whitespacesAndNewlines) - if (logs.count % 64 != 0){ - return + + private func finishReading() { + var history = ef84Records + extendedRecords + history.sort { + ($0.getTransationTime() ?? Date.distantPast) > ($1.getTransationTime() ?? Date.distantPast) } - let total = logs.count/64 - for i in 0.. 0){ - riwayatList.append(riwayat) - } + + if !history.isEmpty { + emoney.setRiwayatList(history) + emoney.setTampilRiwayat(true) } + + updateScreen() + apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!) + apduRunner.invalidateSession() } - - private func formatDate(seconds : Int) -> Date{ - // Specify date components + + private func parseEf84Record(_ data: Data) -> RiwayatCard? { + let bytes = data.bytes + guard bytes.count >= 60 else { + return nil + } + + let item = RiwayatCard() + let typeRaw = (Int(bytes[0]) << 8) | Int(bytes[1]) + let amount = uint24BE(bytes, offset: 6) + let transactionTime = uint32BE(bytes, offset: 38) + let terminalId = asciiString(bytes, start: 30, length: 8) + let isPayment = typeRaw == 0x0400 + + item.setAmount(amount) + item.setLocationId(terminalId) + item.setLocationName(isPayment ? "payment".localizeString(string: self.langCode!) : "topup".localizeString(string: self.langCode!)) + item.setTransactionTime(formatFlazzTimestamp(seconds: transactionTime)) + + if isPayment { + item.setProsesTipe(1) + item.setTitle("payment".localizeString(string: self.langCode!)) + } else { + item.setProsesTipe(0) + item.setTitle("topup".localizeString(string: self.langCode!)) + } + + return transactionTime > 0 ? item : nil + } + + private func parseExtendedRecord(_ data: Data) -> RiwayatCard? { + let bytes = data.bytes + guard bytes.count >= 32 else { + return nil + } + + let item = RiwayatCard() + let type = bytes[0] + let rawTimestamp = Array(bytes[4..<8]).hexString().uppercased() + let timestampSeconds = uint32BE(bytes, offset: 4) + let rawAmount = uint24BE(bytes, offset: 1) + let terminalId = asciiString(bytes, start: 14, length: 8) + let isPayment = type == 0x04 + let amount = isPayment ? 0x1000000 - rawAmount : rawAmount + + item.setAmount(amount) + item.setLocationId(terminalId) + item.setLocationName(isPayment ? "payment".localizeString(string: self.langCode!) : "topup".localizeString(string: self.langCode!)) + item.setDesk("RAW TS: \(rawTimestamp)") + item.setTransactionTime(formatFlazzTimestamp(seconds: timestampSeconds)) + + if isPayment { + item.setProsesTipe(1) + item.setTitle("payment".localizeString(string: self.langCode!)) + } else { + item.setProsesTipe(0) + item.setTitle("topup".localizeString(string: self.langCode!)) + } + + return timestampSeconds > 0 ? item : nil + } + + private func extractPan(from raw: String) -> String? { + guard let start = raw.firstIndex(of: ";") else { + return nil + } + + let tail = raw[raw.index(after: start)...] + guard let end = tail.firstIndex(of: "=") else { + return nil + } + + return String(tail[.. Int { + guard bytes.count >= offset + 3 else { + return 0 + } + + return (Int(bytes[offset]) << 16) + | (Int(bytes[offset + 1]) << 8) + | Int(bytes[offset + 2]) + } + + private func uint32BE(_ bytes: [UInt8], offset: Int) -> Int { + guard bytes.count >= offset + 4 else { + return 0 + } + + return (Int(bytes[offset]) << 24) + | (Int(bytes[offset + 1]) << 16) + | (Int(bytes[offset + 2]) << 8) + | Int(bytes[offset + 3]) + } + + private func formatFlazzTimestamp(seconds: Int) -> Date { var dateComponents = DateComponents() dateComponents.year = 1980 dateComponents.month = 1 @@ -393,16 +297,22 @@ public class BcaFlazzApi : UnifiedNfcApi { dateComponents.hour = 0 dateComponents.minute = 0 dateComponents.second = seconds - // Create date from components - let userCalendar = Calendar.current // user calendar - let someDateTime = userCalendar.date(from: dateComponents) - return someDateTime! + return Calendar.current.date(from: dateComponents)! } - - private func updateScreen(){ - if (self.apduRunner.callback != nil){ + + private func asciiString(_ bytes: [UInt8], start: Int, length: Int) -> String { + guard bytes.count >= start + length else { + return "" + } + + let slice = Array(bytes[start..<(start + length)]) + return String(bytes: slice, encoding: .ascii)? + .trimmingCharacters(in: .whitespacesAndNewlines.union(.controlCharacters)) ?? "" + } + + private func updateScreen() { + if self.apduRunner.callback != nil { self.apduRunner.callback?.complete(emoney: self.emoney) } } - } diff --git a/Emoney Info/Classes/api/nfc/RiwayatCard.swift b/Emoney Info/Classes/api/nfc/RiwayatCard.swift index eb0f26f..dbdcbbd 100755 --- a/Emoney Info/Classes/api/nfc/RiwayatCard.swift +++ b/Emoney Info/Classes/api/nfc/RiwayatCard.swift @@ -47,6 +47,10 @@ class RiwayatCard return self.desk } + func setDesk(_ str: String) { + self.desk = str + } + func getJam() -> String? { return self.jam } @@ -121,4 +125,3 @@ class RiwayatCard // } } - diff --git a/Emoney Info/Classes/smartCard/CommonConstants.swift b/Emoney Info/Classes/smartCard/CommonConstants.swift index 4c223ce..07387c3 100755 --- a/Emoney Info/Classes/smartCard/CommonConstants.swift +++ b/Emoney Info/Classes/smartCard/CommonConstants.swift @@ -18,10 +18,9 @@ func debugLog( separator: String = " ", terminator: String = "\n" ) { -#if DEBUG - let output = items.map { String(describing: $0) }.joined(separator: separator) - Swift.print(output, terminator: terminator) -#endif + _ = items + _ = separator + _ = terminator } /** diff --git a/Emoney Info/HistoryView.swift b/Emoney Info/HistoryView.swift index ab52738..895d6f2 100644 --- a/Emoney Info/HistoryView.swift +++ b/Emoney Info/HistoryView.swift @@ -385,10 +385,15 @@ private struct TransactionRow: View { } private var dateText: String { - guard let date = riwayat.getTransationTime() else { return "–" } - let fmt = DateFormatter() - fmt.dateFormat = "MMM d, yyyy · HH:mm" - return fmt.string(from: date) + if let date = riwayat.getTransationTime() { + let fmt = DateFormatter() + fmt.dateFormat = "MMM d, yyyy · HH:mm" + return fmt.string(from: date) + } + if let raw = riwayat.getDesk(), !raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return raw + } + return "–" } private var statusText: String { isCredit ? L10n.topup : L10n.payment } @@ -660,8 +665,10 @@ final class HistoryHostingController: UIViewController { y += 10 // ── Table Header ────────────────────────────────────────── - // Cek apakah ada data lokasi di seluruh list - let hasLocation = list.contains { + let normalizedCardLabel = cardLabel.lowercased() + let hideLocationForPdf = normalizedCardLabel.contains("flazz") + || normalizedCardLabel.contains("mandiri e-money") + let hasLocation = !hideLocationForPdf && list.contains { let loc = $0.getLocationName()?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return !loc.isEmpty } @@ -707,7 +714,9 @@ final class HistoryHostingController: UIViewController { width: colW + 8, height: rowHeight)).fill() } - let dateStr = rw.getTransationTime().map { dateFmt.string(from: $0) } ?? "–" + let dateStr = rw.getTransationTime().map { dateFmt.string(from: $0) } + ?? rw.getDesk() + ?? "–" let typeStr = rw.getProsesTipe() == 0 ? "Top up" : "Payment" let amtStr = numFmt.string(for: rw.getAmount()) ?? "Rp 0" let amtColor: UIColor = rw.getProsesTipe() == 0 diff --git a/Emoney Info/HomeView.swift b/Emoney Info/HomeView.swift index 3c680aa..cc0be1e 100644 --- a/Emoney Info/HomeView.swift +++ b/Emoney Info/HomeView.swift @@ -672,7 +672,9 @@ extension HomeViewController: ApduCallback { if let first = riwayat.first { let dateFmt = DateFormatter() dateFmt.dateFormat = "dd MMM yyyy, HH:mm" - let dateStr = first.getTransationTime().map { dateFmt.string(from: $0) } ?? "–" + let dateStr = first.getTransationTime().map { dateFmt.string(from: $0) } + ?? first.getDesk() + ?? "–" let isCredit = first.getProsesTipe() == 0 let sign = isCredit ? "+" : "-" let place = first.getLocationName()?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""