taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

commit 5a0f216537b9020dcd685c47210da014402b5c27
parent 723d02fa3d539c7b14bfc110698be8dd7863dde4
Author: Marc Stibane <marc@taler.net>
Date:   Tue, 24 Jun 2025 07:15:22 +0200

Fix emulation

Diffstat:
MTalerWallet1/Helper/Data+fromUInt8Array.swift | 12++++++++++++
MTalerWallet1/Helper/TagEmulation.swift | 19++++++++++++++++---
MTalerWallet1/Helper/apdu.swift | 79++++++++++++++++++++++++++-----------------------------------------------------
MTalerWallet1/Views/HelperViews/GradientBorder.swift | 32++++++++++++++++++++------------
MTalerWallet1/Views/HelperViews/QRCodeDetailView.swift | 4++--
MTalerWallet1/Views/Settings/AboutView.swift | 9+++++----
6 files changed, 81 insertions(+), 74 deletions(-)

diff --git a/TalerWallet1/Helper/Data+fromUInt8Array.swift b/TalerWallet1/Helper/Data+fromUInt8Array.swift @@ -22,4 +22,16 @@ extension Data { let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx" return self.map { String(format: format, $0) }.joined() } + + mutating func append(data: Data, offset: Int, size: Int) { + let start = data.startIndex + offset + let end = start + size + self.append(data[start..<end]) + } + + init(fromData data: Data, offset: Int, size: Int) { + let start = data.startIndex + offset + let end = start + size + self.init(data[start..<end]) + } } diff --git a/TalerWallet1/Helper/TagEmulation.swift b/TalerWallet1/Helper/TagEmulation.swift @@ -8,7 +8,7 @@ import CoreNFC import os.log -@available(iOS 18.3, *) +@available(iOS 17.7, *) @MainActor class TagEmulation: ObservableObject { public static let shared = TagEmulation() @@ -51,6 +51,11 @@ class TagEmulation: ObservableObject { do { if presentmentIntent == nil { presentmentIntent = try await NFCPresentmentIntentAssertion.acquire() + /// The presentment intent assertion expires if any of the following occur: + /// • The presentment intent assertion object deinitializes. + /// • Your app goes into the background. + /// • 15 seconds elapse. + /// After the presentment intent assertion expires, you must wait through a 15-second cool-down period before you can acquire a new instance. logger.info("NFCPresentmentIntentAssertion acquired") } else { logger.info("NFCPresentmentIntentAssertion exists") @@ -69,6 +74,8 @@ class TagEmulation: ObservableObject { try await eventStream(cardSession, data: emulatedData) logger.info("eventStream finished") + try? await Task.sleep(nanoseconds: 1_000_000_000 * 3) + logger.info("cardSession.invalidate") await cardSession.invalidate() result = true @@ -77,7 +84,9 @@ class TagEmulation: ObservableObject { } catch let error { // Handle failure to acquire NFC presentment intent assertion or card session. logger.error("❗️NFCPresentmentIntentAssertion not possible: \(error.localizedDescription)") - presentmentIntent = nil /// Release presentment intent assertion. + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + self.presentmentIntent = nil /// Release presentment intent assertion. + } return false } return result @@ -90,7 +99,11 @@ class TagEmulation: ObservableObject { cardSession = nil } logger.info("NFCPresentmentIntentAssertion released") - presentmentIntent = nil /// Release presentment intent assertion + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + if self.cardSession == nil { // don't release if there is a new session + self.presentmentIntent = nil // release presentment intent assertion + } + } } } diff --git a/TalerWallet1/Helper/apdu.swift b/TalerWallet1/Helper/apdu.swift @@ -58,22 +58,6 @@ class APDU { ]) } // 00b0 0000 0f -// private var READ_CAPABILITY_CONTAINER_RESPONSE: Data { -// Data(fromUInt8Array: [ -// 0x00, 0x11, // CCLEN length of the CC file -// 0x20, // Mapping Version 2.0 -// 0xFF, 0xFF, // MLe maximum -// 0xFF, 0xFF, // MLc maximum -// 0x04, // T field of the NDEF File Control TLV -// 0x06, // L field of the NDEF File Control TLV -// 0xE1, 0x04, // File Identifier of NDEF file -// 0xFF, 0xFE, // Maximum NDEF file size of 65534 bytes -// 0x00, // Read access without any security -// 0xFF, // Write access without any security -// 0x90, 0x00, // A_OKAY -// ]) -// } // 0011 20ffffffff 0406 e104 ffff 00ff 9000 - private var READ_CAPABILITY_CONTAINER_RESPONSE: Data { Data(fromUInt8Array: [ 0x00, 0x0f, // CCLEN length of the CC file @@ -83,14 +67,13 @@ class APDU { 0x04, // T field of the NDEF File Control TLV 0x06, // L field of the NDEF File Control TLV 0xE1, 0x04, // File Identifier of NDEF file - 0x00, 0xFF, // Maximum NDEF file size of 65534 bytes + 0x00, 0xFE, // Maximum NDEF file size of 65534 bytes // 254 bytes 0x00, // Read access without any security 0xFF, // Write access without any security 0x90, 0x00, // A_OKAY ]) } //000f 20 003b 0034 04 06 e104 00ff 00ff9000 - private var NDEF_SELECT_OK: Data { Data(fromUInt8Array: [ 0x00, // CLA - Class - Class of instruction @@ -132,17 +115,6 @@ class APDU { ]) } // d101 - private var NDEF_RECORD_HEADER_LONG: Data { - Data(fromUInt8Array: [ - 0xD1, // 1100 + 0001 = MessageBegin, MessageEnd + Format "Well-Known" (but no ID Length and no ShortRecord) - 0x01, // TypeLength - // 0x0000, 0x0000, // Payload Length 4 bytes since ShortRecord is false - // no ID Length byte since the IDlength flag is false - // 0x55, // Payload Type = “U” for URI, 1 byte as specified in the TypeLength field - // since we have no ID length, there is no Payload ID - ]) - } // c101 - private var A_OKAY_ARRAY: [UInt8] { [ 0x90, // SW1 Status byte 1 - Command processing status @@ -234,9 +206,9 @@ class APDU { let uriCount = talerUri.count let ndefLen = UInt16(uriCount + 5) // NDEF_RECORD_HEADER is 4 bytes - /// Fifth Command: Read the Length of the NDEF File + /// Fifth Command: Read the Length of the NDEF File // 00b0 00 00 02 if NDEF_READ_BINARY_NLEN == commandApdu { - logger.info("NDEF_READ_BINARY_NLEN triggered. Our Response: length + A_OKAY") + logger.info("NDEF_READ_BINARY_NLEN triggered. Our Response: length \(ndefLen) + A_OKAY") readCapabilityContainerCheck = false let ndefLenHi = UInt8(ndefLen >> 8) @@ -248,33 +220,34 @@ class APDU { return lenData } // 00b0000002 - /// Sixth Command: Read the NDEF File + /// Sixth (and seventh) Command: Read the NDEF File // 00b0 00 02 3b, and 00b0 00 3d 27 if NDEF_READ_BINARY == commandApdu[0..<2] { let count = commandApdu.count if count > 4 { var arrayApdu = Array<UInt8>(repeating: 0, count: count) _ = arrayApdu.withUnsafeMutableBytes { commandApdu.copyBytes(to: $0) } -// if arrayApdu[..<2] == NDEF_READ_BINARY[..<2] { -// if count == 5 { - let length = arrayApdu[4] - let offset = UInt16(arrayApdu[3]) + (UInt16(arrayApdu[2]) << 8) - logger.info("NDEF_READ_BINARY triggered. Our Response: data + A_OKAY") -// if offset == 2 && length == ndefLen { -// var data = NDEF_RECORD_HEADER_SHORT - var data = NDEF_RECORD_HEADER_LONG - let uriCount8 = UInt8((uriCount + 1) & 0xff) - let lenData = withUnsafeBytes(of: uriCount8) { Data($0) } - data.append(lenData) - data.append("U".data(using: .utf8)!) - let zero: UInt8 = 0 - let zeroData = withUnsafeBytes(of: zero) { Data($0) } - data.append(zeroData) - data.append(talerUri.data(using: .utf8)!) - data.append(A_OKAY) - - readCapabilityContainerCheck = false - return data -// } + + let wantedLength = Int(arrayApdu[4]) + let offset = Int(arrayApdu[3]) + (Int(arrayApdu[2]) << 8) + logger.info("NDEF_READ_BINARY offset \(offset), length \(wantedLength) triggered. Our Response: data + A_OKAY") + + var data = NDEF_RECORD_HEADER_SHORT // 2 bytes + let uriCount8 = UInt8((uriCount + 1) & 0xff) + let lenData = withUnsafeBytes(of: uriCount8) { Data($0) } + data.append(lenData) // 1 byte + data.append("U".data(using: .utf8)!) // 1 byte + let zero: UInt8 = 0 + let zeroData = withUnsafeBytes(of: zero) { Data($0) } + data.append(zeroData) // 1 byte + data.append(talerUri.data(using: .utf8)!) + + var result = Data(fromData: data, + offset: Int(offset) - 2, // subtract 2 length bytes + size: wantedLength) + result.append(A_OKAY) // 2 bytes + + readCapabilityContainerCheck = false + return result } } diff --git a/TalerWallet1/Views/HelperViews/GradientBorder.swift b/TalerWallet1/Views/HelperViews/GradientBorder.swift @@ -24,9 +24,10 @@ */ import SwiftUI -@available(iOS 18.3, *) +@available(iOS 17.7, *) struct BorderWithNFC<Content: View>: View { let talerURI: String + let nfcHint: Bool let size: CGFloat var content: () -> Content @@ -37,18 +38,25 @@ struct BorderWithNFC<Content: View>: View { let _ = Self._printChanges() if tagEmulation.canUseHCE { VStack { - if !minimalistic { - Text("Tap for NFC:") - .talerFont(.subheadline) - .padding(.vertical, -4) - } - let screenWidth = UIScreen.screenWidth - GradientBorder(size: size + 20.0, - color: .accentColor, - background: WalletColors().backgroundColor) - { + if nfcHint { + if !minimalistic { + Text("Tap for NFC:") + .talerFont(.subheadline) + .padding(.vertical, -4) + } + let screenWidth = UIScreen.screenWidth + GradientBorder(size: size + 20.0, + color: .accentColor, + background: WalletColors().backgroundColor) + { + content() + .onTapGesture(count: 1) { + tagEmulation.emulateTag(talerURI) + } + } + } else { content() - .onTapGesture { + .onTapGesture(count: 2) { tagEmulation.emulateTag(talerURI) } } diff --git a/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift b/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift @@ -65,8 +65,8 @@ struct QRCodeDetailView: View { .frame(maxWidth: .infinity, alignment: .center) Group { #if TALER_NIGHTLY || GNU_TALER - if #available(iOS 18.3, *) { - BorderWithNFC(talerURI: talerURI, size: size) { + if #available(iOS 17.7, *) { + BorderWithNFC(talerURI: talerURI, nfcHint: true, size: size) { qrView } } else { diff --git a/TalerWallet1/Views/Settings/AboutView.swift b/TalerWallet1/Views/Settings/AboutView.swift @@ -42,21 +42,22 @@ struct AboutView: View { rotationEnabled: $rotationEnabled) .accessibilityHidden(true) // has its own accessibilityLabel .frame(maxWidth: .infinity, alignment: .center) - .onTapGesture(count: 2) { rotationEnabled.toggle() } +// .onTapGesture(count: 3) { rotationEnabled.toggle() } // would suppress double-tap in BorderWithNFC Group { List { #if TALER_NIGHTLY || GNU_TALER - if #available(iOS 18.3, *) { + if #available(iOS 17.7, *) { let talerURI = TALER_NET -// let talerURI = "taler://withdraw-exchange/exchange.taler-ops.ch" - BorderWithNFC(talerURI: talerURI, size: size) { + BorderWithNFC(talerURI: talerURI, nfcHint: false, size: size) { rotatingTaler } } else { rotatingTaler + .onTapGesture(count: 2) { rotationEnabled.toggle() } } #else rotatingTaler + .onTapGesture(count: 2) { rotationEnabled.toggle() } #endif SettingsItem(name: String(localized: "Visit the taler.net website"), id1: "web",