commit 5a0f216537b9020dcd685c47210da014402b5c27
parent 723d02fa3d539c7b14bfc110698be8dd7863dde4
Author: Marc Stibane <marc@taler.net>
Date: Tue, 24 Jun 2025 07:15:22 +0200
Fix emulation
Diffstat:
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",