From db0011a8e4bce5523faf1858ded50bfa4e84d091 Mon Sep 17 00:00:00 2001 From: Kartik Angiras Date: Sun, 19 Apr 2026 14:06:50 +0530 Subject: [PATCH] Enhance error handling for connection issues in TapSigner flow NFC interactions --- .../org/bitcoinppl/cove/CoinControlManager.kt | 3 ++ .../TapSignerFlow/TapSignerEnterPinView.kt | 15 ++++++++ .../bitcoinppl/cove/nfc/TapCardNfcManager.kt | 4 +- .../java/org/bitcoinppl/cove_core/cove.kt | 37 ++++++++++++++++++- ios/Cove/AppManager.swift | 6 ++- .../TapSignerConfirmPinView.swift | 1 + .../TapSignerFlow/TapSignerEnterPinView.swift | 6 +++ ios/Cove/ImportWalletManager.swift | 4 +- ios/Cove/Security.swift | 3 +- ios/Cove/ShareSheet.swift | 3 +- ios/Cove/TapSignerNFC.swift | 20 ++++++++-- ios/Cove/WalletManager.swift | 8 ++-- .../Sources/CoveCore/generated/cove.swift | 22 ++++++++++- rust/src/tap_card.rs | 4 ++ rust/src/tap_card/tap_signer_reader.rs | 9 +++++ 15 files changed, 126 insertions(+), 19 deletions(-) diff --git a/android/app/src/main/java/org/bitcoinppl/cove/CoinControlManager.kt b/android/app/src/main/java/org/bitcoinppl/cove/CoinControlManager.kt index 99e46089b..44896aa3b 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/CoinControlManager.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/CoinControlManager.kt @@ -124,6 +124,8 @@ class CoinControlManager( * called when user presses continue button * navigates forward to CoinControlSetAmount screen with selected UTXOs */ + + @MainActor fun continuePressed(app: AppManager) { val walletId = rust.id() val selectedUtxos = utxos.filter { selected.contains(it.outpoint.hashToUint()) } @@ -133,6 +135,7 @@ class CoinControlManager( app.pushRoute(Route.Send(sendRoute)) } + @MainActor private fun updateSendFlowManager() { val sfm = AppManager.getInstance().sendFlowManager ?: return updateSendFlowManagerTask?.cancel() diff --git a/android/app/src/main/java/org/bitcoinppl/cove/flows/TapSignerFlow/TapSignerEnterPinView.kt b/android/app/src/main/java/org/bitcoinppl/cove/flows/TapSignerFlow/TapSignerEnterPinView.kt index 5f8b4783d..036f1bb36 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/flows/TapSignerFlow/TapSignerEnterPinView.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/flows/TapSignerFlow/TapSignerEnterPinView.kt @@ -252,6 +252,9 @@ private suspend fun deriveAction( if (isAuthError(e)) { Log.w("TapSignerEnterPin", "TapSigner auth failed - likely wrong PIN") manager.errorMessage = "Wrong PIN, please try again" + } else if (isConnectionError(e)) { + Log.w("TapSignerEnterPin", "TapSigner connection lost") + manager.errorMessage = "Tag connection lost, please hold your phone still" } else { manager.errorMessage = "Connection failed, please try again" } @@ -317,6 +320,9 @@ private suspend fun backupAction( if (isAuthError(e)) { Log.w("TapSignerEnterPin", "TapSigner auth failed - likely wrong PIN") manager.errorMessage = "Wrong PIN, please try again" + } else if (isConnectionError(e)) { + Log.w("TapSignerEnterPin", "TapSigner connection lost") + manager.errorMessage = "Tag connection lost, please hold your phone still" } else { manager.errorMessage = "Connection failed, please try again" } @@ -374,6 +380,9 @@ private suspend fun signAction( if (isAuthError(e)) { Log.w("TapSignerEnterPin", "TapSigner auth failed - likely wrong PIN") manager.errorMessage = "Wrong PIN, please try again" + } else if (isConnectionError(e)) { + Log.w("TapSignerEnterPin", "TapSigner connection lost") + manager.errorMessage = "Tag connection lost, please hold your phone still" } else { manager.errorMessage = "Connection failed, please try again" } @@ -385,3 +394,9 @@ private fun isAuthError(error: Exception): Boolean { return error is org.bitcoinppl.cove_core.TapSignerReaderException && error.isAuthError() } + +private fun isConnectionError(error: Exception): Boolean { + // check if error is a connection error using type-safe FFI function + return error is org.bitcoinppl.cove_core.TapSignerReaderException && + error.isConnectionError() +} diff --git a/android/app/src/main/java/org/bitcoinppl/cove/nfc/TapCardNfcManager.kt b/android/app/src/main/java/org/bitcoinppl/cove/nfc/TapCardNfcManager.kt index c7cab0c4e..fa8fd17e9 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/nfc/TapCardNfcManager.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/nfc/TapCardNfcManager.kt @@ -283,9 +283,7 @@ private class TapCardTransport( response } catch (e: Exception) { Log.e(tag, "APDU error", e) - throw TransportException.UnknownException( - "Tag connection lost, please hold your phone still and try again" - ) + throw TransportException.ConnectionException("Tag connection lost, please hold your phone still") } } diff --git a/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt b/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt index d322a3731..18c1faf08 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt @@ -3244,6 +3244,8 @@ internal object UniffiLib { ): RustBuffer.ByValue external fun uniffi_cove_fn_method_tapsignerreadererror_isautherror(`ptr`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Byte + external fun uniffi_cove_fn_method_tapsignerreadererror_isconnectionerror(`ptr`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Byte external fun uniffi_cove_fn_method_tapsignerreadererror_isnobackuperror(`ptr`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Byte external fun uniffi_cove_fn_method_tapsignerreadererror_uniffi_trait_display(`ptr`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, @@ -46601,6 +46603,16 @@ sealed class TapSignerReaderException: kotlin.Exception() { } + fun `isConnectionError`(): kotlin.Boolean { + return FfiConverterBoolean.lift( + uniffiRustCall() { _status -> + UniffiLib.uniffi_cove_fn_method_tapsignerreadererror_isconnectionerror(FfiConverterTypeTapSignerReaderError.lower(this), + _status) +} + ) + } + + fun `isNoBackupError`(): kotlin.Boolean { return FfiConverterBoolean.lift( uniffiRustCall() { _status -> @@ -47725,6 +47737,14 @@ sealed class TransportException: kotlin.Exception() { get() = "v1=${ v1 }" } + class ConnectionException( + + val v1: kotlin.String + ) : TransportException() { + override val message + get() = "v1=${ v1 }" + } + class UnknownException( val v1: kotlin.String @@ -47779,7 +47799,10 @@ public object FfiConverterTypeTransportError : FfiConverterRustBuffer TransportException.CvcChangeException( FfiConverterString.read(buf), ) - 7 -> TransportException.UnknownException( + 7 -> TransportException.ConnectionException( + FfiConverterString.read(buf), + ) + 8 -> TransportException.UnknownException( FfiConverterString.read(buf), ) else -> throw RuntimeException("invalid error enum value, something is very wrong!!") @@ -47818,6 +47841,11 @@ public object FfiConverterTypeTransportError : FfiConverterRustBuffer ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) is TransportException.UnknownException -> ( // Add the size for the Int that specifies the variant plus the size needed for all fields 4UL @@ -47858,11 +47886,16 @@ public object FfiConverterTypeTransportError : FfiConverterRustBuffer { + is TransportException.ConnectionException -> { buf.putInt(7) FfiConverterString.write(value.v1, buf) Unit } + is TransportException.UnknownException -> { + buf.putInt(8) + FfiConverterString.write(value.v1, buf) + Unit + } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } diff --git a/ios/Cove/AppManager.swift b/ios/Cove/AppManager.swift index d29266907..2b6b7f928 100644 --- a/ios/Cove/AppManager.swift +++ b/ios/Cove/AppManager.swift @@ -4,7 +4,9 @@ import SwiftUI private let walletModeChangeDelayMs = 250 -@Observable final class AppManager: FfiReconcile { +@MainActor +@Observable +final class AppManager: FfiReconcile { static let shared = makeShared() private let logger = Log(id: "AppManager") @@ -93,6 +95,7 @@ private let walletModeChangeDelayMs = 250 self.rust.listenForUpdates(updater: self) } + @MainActor public func getWalletManager(id: WalletId) throws -> WalletManager { if let walletvm = walletManager, walletvm.id == id { logger.debug("found and using vm for \(id)") @@ -107,6 +110,7 @@ private let walletModeChangeDelayMs = 250 return walletManager! } + @MainActor public func getSendFlowManager(_ wm: WalletManager, presenter: SendFlowPresenter) -> SendFlowManager { let id = wm.id diff --git a/ios/Cove/Flows/TapSignerFlow/TapSignerConfirmPinView.swift b/ios/Cove/Flows/TapSignerFlow/TapSignerConfirmPinView.swift index d29ad1a47..62664a0c2 100644 --- a/ios/Cove/Flows/TapSignerFlow/TapSignerConfirmPinView.swift +++ b/ios/Cove/Flows/TapSignerFlow/TapSignerConfirmPinView.swift @@ -81,6 +81,7 @@ struct TapSignerConfirmPinView: View { case let .failure(error): if error.isAuthError() { return app.alertState = .init(.tapSignerInvalidAuth) } if error.isNoBackupError() { return app.alertState = .init(.tapSignerNoBackup(tapSigner: args.tapSigner)) } + if error.isConnectionError() { return app.alertState = .init(.general(title: "Connection Lost", message: "Tag connection lost, please hold your phone still")) } app.alertState = .init(.general(title: "Error", message: error.description)) } } diff --git a/ios/Cove/Flows/TapSignerFlow/TapSignerEnterPinView.swift b/ios/Cove/Flows/TapSignerFlow/TapSignerEnterPinView.swift index 32ab028ba..51d24f6bc 100644 --- a/ios/Cove/Flows/TapSignerFlow/TapSignerEnterPinView.swift +++ b/ios/Cove/Flows/TapSignerFlow/TapSignerEnterPinView.swift @@ -54,6 +54,8 @@ struct TapSignerEnterPin: View { if error.isAuthError() { app.sheetState = nil app.alertState = .init(.tapSignerWrongPin(tapSigner: tapSigner, action: .derive)) + } else if error.isConnectionError() { + app.alertState = .init(.general(title: "Connection Lost", message: "Tag connection lost, please hold your phone still")) } else { app.alertState = .init(.tapSignerDeriveFailed(message: error.description)) } @@ -83,6 +85,8 @@ struct TapSignerEnterPin: View { if error.isAuthError() { app.sheetState = nil app.alertState = .init(.tapSignerWrongPin(tapSigner: tapSigner, action: .backup)) + } else if error.isConnectionError() { + app.alertState = .init(.general(title: "Connection Lost", message: "Tag connection lost, please hold your phone still")) } else { app.alertState = .init( .general(title: "Backup Failed!", message: error.description) @@ -128,6 +132,8 @@ struct TapSignerEnterPin: View { if error.isAuthError() { app.sheetState = nil app.alertState = .init(.tapSignerWrongPin(tapSigner: tapSigner, action: .sign(psbt))) + } else if error.isConnectionError() { + app.alertState = .init(.general(title: "Connection Lost", message: "Tag connection lost, please hold your phone still")) } else { app.alertState = .init( .general(title: "Signing Failed!", message: error.description) diff --git a/ios/Cove/ImportWalletManager.swift b/ios/Cove/ImportWalletManager.swift index 9d2688a12..eb983c313 100644 --- a/ios/Cove/ImportWalletManager.swift +++ b/ios/Cove/ImportWalletManager.swift @@ -1,6 +1,8 @@ import SwiftUI -@Observable class ImportWalletManager: ImportWalletManagerReconciler { +@MainActor +@Observable +final class ImportWalletManager: ImportWalletManagerReconciler { private let logger = Log(id: "ImportWalletManager") var rust: RustImportWalletManager diff --git a/ios/Cove/Security.swift b/ios/Cove/Security.swift index 33690744a..344a05807 100644 --- a/ios/Cove/Security.swift +++ b/ios/Cove/Security.swift @@ -8,7 +8,8 @@ import Foundation import KeychainSwift -class KeychainAccessor: KeychainAccess { +@MainActor +final class KeychainAccessor: KeychainAccess { let keychain: KeychainSwift init() { diff --git a/ios/Cove/ShareSheet.swift b/ios/Cove/ShareSheet.swift index 9763f856e..62af538a7 100644 --- a/ios/Cove/ShareSheet.swift +++ b/ios/Cove/ShareSheet.swift @@ -2,8 +2,7 @@ import LinkPresentation import SwiftUI import UIKit -@MainActor -private class ShareableFile: NSObject, UIActivityItemSource { +private final class ShareableFile: NSObject, UIActivityItemSource { let url: URL let iconImage: UIImage? diff --git a/ios/Cove/TapSignerNFC.swift b/ios/Cove/TapSignerNFC.swift index afe8a634b..62c281554 100644 --- a/ios/Cove/TapSignerNFC.swift +++ b/ios/Cove/TapSignerNFC.swift @@ -360,9 +360,12 @@ private class TapCardNFC: NSObject, NFCTagReaderSessionDelegate { } catch let error as TapSignerReaderError { logger.error("TAPSIGNER error: \(error)") tapSignerError = error - if case .TapSignerError(.CkTap(.BadAuth)) = error { + if error.isAuthError() { return session.invalidate(errorMessage: "Wrong PIN, please try again") } + if error.isConnectionError() { + return session.invalidate(errorMessage: "Tag connection lost, please hold your phone still") + } session.invalidate(errorMessage: "TapSigner error: \(error.description)") } catch { logger.error("Error creating reader: \(error)") @@ -439,9 +442,18 @@ class TapCardTransport: TapcardTransportProtocol, @unchecked Sendable { if let error { logger.error("APDU error: \(error)") - continuation.resume( - throwing: TransportError.UnknownError(error.localizedDescription) - ) + // Check if this is a connection error + if let nfcError = error as? NFCReaderError, + nfcError.code == .readerTransceiveErrorTagConnectionLost + { + continuation.resume( + throwing: TransportError.ConnectionError(error.localizedDescription) + ) + } else { + continuation.resume( + throwing: TransportError.UnknownError(error.localizedDescription) + ) + } return } diff --git a/ios/Cove/WalletManager.swift b/ios/Cove/WalletManager.swift index b0a74b5d9..c1359b66d 100644 --- a/ios/Cove/WalletManager.swift +++ b/ios/Cove/WalletManager.swift @@ -2,7 +2,9 @@ import SwiftUI extension WeakReconciler: WalletManagerReconciler where Reconciler == WalletManager {} -@Observable final class WalletManager: AnyReconciler, WalletManagerReconciler { +@MainActor +@Observable +final class WalletManager: AnyReconciler, WalletManagerReconciler { typealias Message = WalletManagerReconcileMessage typealias Action = WalletManagerAction @@ -233,7 +235,7 @@ extension WeakReconciler: WalletManagerReconciler where Reconciler == WalletMana } } - func reconcile(message: Message) { + nonisolated func reconcile(message: Message) { DispatchQueue.main.async { [weak self] in guard let self else { return } logger.debug("reconcile \(message)") @@ -241,7 +243,7 @@ extension WeakReconciler: WalletManagerReconciler where Reconciler == WalletMana } } - func reconcileMany(messages: [Message]) { + nonisolated func reconcileMany(messages: [Message]) { DispatchQueue.main.async { [weak self] in guard let self else { return } logger.debug("reconcile_messages: \(messages)") diff --git a/ios/CoveCore/Sources/CoveCore/generated/cove.swift b/ios/CoveCore/Sources/CoveCore/generated/cove.swift index e0019bf64..dbd5ce4f0 100644 --- a/ios/CoveCore/Sources/CoveCore/generated/cove.swift +++ b/ios/CoveCore/Sources/CoveCore/generated/cove.swift @@ -28627,6 +28627,14 @@ public func isAuthError() -> Bool { }) } +public func isConnectionError() -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_cove_fn_method_tapsignerreadererror_isconnectionerror( + FfiConverterTypeTapSignerReaderError_lower(self),$0 + ) +}) +} + public func isNoBackupError() -> Bool { return try! FfiConverterBool.lift(try! rustCall() { uniffi_cove_fn_method_tapsignerreadererror_isnobackuperror( @@ -29338,6 +29346,8 @@ enum TransportError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError ) case CvcChangeError(String ) + case ConnectionError(String + ) case UnknownError(String ) @@ -29397,7 +29407,10 @@ public struct FfiConverterTypeTransportError: FfiConverterRustBuffer { case 6: return .CvcChangeError( try FfiConverterString.read(from: &buf) ) - case 7: return .UnknownError( + case 7: return .ConnectionError( + try FfiConverterString.read(from: &buf) + ) + case 8: return .UnknownError( try FfiConverterString.read(from: &buf) ) @@ -29442,10 +29455,15 @@ public struct FfiConverterTypeTransportError: FfiConverterRustBuffer { FfiConverterString.write(v1, into: &buf) - case let .UnknownError(v1): + case let .ConnectionError(v1): writeInt(&buf, Int32(7)) FfiConverterString.write(v1, into: &buf) + + case let .UnknownError(v1): + writeInt(&buf, Int32(8)) + FfiConverterString.write(v1, into: &buf) + } } } diff --git a/rust/src/tap_card.rs b/rust/src/tap_card.rs index f7e8d7711..9cd7d6815 100644 --- a/rust/src/tap_card.rs +++ b/rust/src/tap_card.rs @@ -25,6 +25,9 @@ pub enum TransportError { #[error("CvcChangeError: {0}")] CvcChangeError(String), + #[error("ConnectionError: {0}")] + ConnectionError(String), + #[error("UnknownError: {0}")] UnknownError(String), } @@ -108,6 +111,7 @@ impl From for ApduError { TransportError::IncorrectSignature(msg) => Self::IncorrectSignature(msg), TransportError::UnknownCardType(msg) => Self::UnknownCardType(msg), TransportError::CvcChangeError(_) => Self::CkTap(CkTapError::BadArguments.into()), + TransportError::ConnectionError(_) => Self::CkTap(CkTapError::BadArguments.into()), TransportError::UnknownError(_) => Self::CkTap(CkTapError::BadArguments.into()), } } diff --git a/rust/src/tap_card/tap_signer_reader.rs b/rust/src/tap_card/tap_signer_reader.rs index e92bb34bf..9368dc319 100644 --- a/rust/src/tap_card/tap_signer_reader.rs +++ b/rust/src/tap_card/tap_signer_reader.rs @@ -539,6 +539,10 @@ impl TapSignerReaderError { pub const fn is_no_backup_error(&self) -> bool { matches!(self, Self::TapSignerError(TransportError::CkTap(CkTapError::BackupFirst))) } + + pub const fn is_connection_error(&self) -> bool { + matches!(self, Self::TapSignerError(TransportError::ConnectionError(_))) + } } mod ffi { @@ -622,6 +626,11 @@ impl TapSignerReaderError { fn ffi_is_no_backup_error(&self) -> bool { self.is_no_backup_error() } + + #[uniffi::method(name = "isConnectionError")] + fn ffi_is_connection_error(&self) -> bool { + self.is_connection_error() + } } // MARK: - FFI PREVIEW