From 5a2da923e7f35bddcde99a0d649692b912d74dd2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 29 Jun 2026 10:57:37 -0300 Subject: [PATCH 01/14] feat: implement hardware wallet setting screen --- Bitkit/MainNavView.swift | 1 + .../Localization/en.lproj/Localizable.strings | 3 + Bitkit/ViewModels/NavigationViewModel.swift | 1 + .../HardwareWalletsSettingsScreen.swift | 193 ++++++++++++++++++ .../Views/Settings/GeneralSettingsView.swift | 12 ++ changelog.d/next/hw-wallet-settings.added.md | 1 + 6 files changed, 211 insertions(+) create mode 100644 Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift create mode 100644 changelog.d/next/hw-wallet-settings.added.md diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index 62d5c6ad4..55a331a34 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -540,6 +540,7 @@ struct MainNavView: View { case .notificationsIntro: NotificationsIntro() case .paymentPreference: if isPaykitUIActive { PaymentPreferenceView() } else { paykitDisabledRedirectView } + case .hardwareWalletsSettings: HardwareWalletsSettingsScreen() // Security settings case .changePin: ChangePinScreen() diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index b56a60446..48e19536d 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -686,6 +686,9 @@ "settings__general__language_other" = "Interface language"; "settings__general__section_interface" = "Interface"; "settings__general__section_payments" = "Payments"; +"settings__hardware_wallets__nav_title" = "Hardware Wallets"; +"settings__hardware_wallets__empty_text" = "Pair a hardware device to watch its balance and activity in Bitkit."; +"settings__hardware_wallets__add_button" = "Add Hardware Wallet"; "settings__widgets__nav_title" = "Widgets"; "settings__widgets__section_display" = "Display"; "settings__widgets__section_reset" = "Reset To Defaults"; diff --git a/Bitkit/ViewModels/NavigationViewModel.swift b/Bitkit/ViewModels/NavigationViewModel.swift index 69503430f..9c5d300c7 100644 --- a/Bitkit/ViewModels/NavigationViewModel.swift +++ b/Bitkit/ViewModels/NavigationViewModel.swift @@ -76,6 +76,7 @@ enum Route: Hashable { case notifications case notificationsIntro case paymentPreference + case hardwareWalletsSettings // Security case dataBackups diff --git a/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift new file mode 100644 index 000000000..8f3e5a4f1 --- /dev/null +++ b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift @@ -0,0 +1,193 @@ +import SwiftUI + +/// Manages all paired hardware wallets, reachable from Settings ▸ General ▸ Payments. Lists each +/// paired device with a connection badge, name, balance and a per-row delete; an empty state when +/// none are paired; and an "Add Hardware Wallet" button that opens the existing intro flow. Tapping +/// a row opens the device's `HardwareWalletScreen`. Ports bitkit-android's `HardwareWalletsSettingsScreen`. +struct HardwareWalletsSettingsScreen: View { + @Environment(HwWalletManager.self) private var hwWalletManager + @Environment(TrezorManager.self) private var trezorManager + @EnvironmentObject private var navigation: NavigationViewModel + @EnvironmentObject private var sheets: SheetViewModel + + @State private var pendingRemoval: HwWallet? + + private var wallets: [HwWallet] { + hwWalletManager.wallets + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + NavigationBar(title: t("settings__hardware_wallets__nav_title")) + .padding(.horizontal, 16) + + ZStack(alignment: .bottom) { + HwDeviceIllustrations() + .frame(maxWidth: .infinity) + .frame(maxHeight: .infinity, alignment: .bottom) + .padding(.bottom, ScreenLayout.bottomPaddingWithSafeArea) + .allowsHitTesting(false) + + VStack(spacing: 0) { + if wallets.isEmpty { + emptyState + .frame(maxHeight: .infinity, alignment: .center) + } else { + deviceList + } + + CustomButton( + title: t("settings__hardware_wallets__add_button"), + variant: .secondary, + shouldExpand: true + ) { + sheets.showSheet(.hardwareIntro) + } + .accessibilityIdentifier("AddHardwareWallet") + .padding(.bottom, 16) + } + .padding(.horizontal, 16) + } + } + .navigationBarHidden(true) + .accessibilityIdentifier("HardwareWalletsScreen") + .alert( + t("hardware__remove_dialog_title", variables: ["name": pendingRemoval?.name ?? ""]), + isPresented: Binding(get: { pendingRemoval != nil }, set: { if !$0 { pendingRemoval = nil } }) + ) { + Button(t("common__remove"), role: .destructive) { + guard let wallet = pendingRemoval else { return } + Task { await remove(wallet) } + } + Button(t("common__dialog_cancel"), role: .cancel) {} + } message: { + Text(t("hardware__remove_dialog_text")) + } + } + + private var emptyState: some View { + VStack(alignment: .leading, spacing: 8) { + DisplayText(t("settings__hardware_wallets__nav_title")) + BodyMText(t("settings__hardware_wallets__empty_text"), textColor: .white80) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var deviceList: some View { + ScrollView(showsIndicators: false) { + LazyVStack(spacing: 0) { + ForEach(wallets) { wallet in + HwWalletRow( + wallet: wallet, + onTap: { navigation.navigate(.hardwareWallet(deviceId: wallet.id)) }, + onRemove: { pendingRemoval = wallet } + ) + CustomDivider() + } + } + } + .frame(maxHeight: .infinity) + } + + /// Stop watching and forget every entry for the device (it may be paired over multiple + /// transports). Mirrors `HardwareWalletScreen.removeWallet()`: `removeDevice` stops the watchers + /// and deletes the persisted activities, then `forgetDevice` clears credentials and drops the + /// known-device entry, which re-pushes the snapshot and removes it from the list. + private func remove(_ wallet: HwWallet) async { + pendingRemoval = nil + hwWalletManager.removeDevice(id: wallet.id) + for id in wallet.deviceIds { + await trezorManager.forgetDevice(id: id) + } + } +} + +/// A paired hardware wallet row: connection badge, name, balance and a trailing delete. The badge, +/// name and balance navigate to the device's detail screen; the trash button removes it. +private struct HwWalletRow: View { + let wallet: HwWallet + let onTap: () -> Void + let onRemove: () -> Void + + var body: some View { + HStack(spacing: 0) { + Button(action: onTap) { + HStack(spacing: 12) { + HwConnectionBadge(isConnected: wallet.isConnected) + + BodyMText(wallet.name) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + + MoneyText( + sats: Int(clamping: wallet.balanceSats), + size: .bodySSB, + symbol: true, + color: .white64, + symbolColor: .white64 + ) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + + Button(action: onRemove) { + Image("trash") + .renderingMode(.template) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + .foregroundColor(.white64) + .padding(.leading, 12) + .padding(.vertical, 8) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityIdentifier("HardwareWalletRowDelete_\(wallet.id)") + } + .frame(height: 52) + .accessibilityIdentifier("HardwareWalletRow_\(wallet.id)") + } +} + +/// Circular connection badge tinting the shared Bluetooth glyph green when connected, gray otherwise. +private struct HwConnectionBadge: View { + let isConnected: Bool + + var body: some View { + ZStack { + Circle() + .fill(isConnected ? Color.green16 : Color.white16) + .frame(width: 32, height: 32) + + HwWalletConnectionIcon(isConnected: isConnected) + .frame(width: 16, height: 16) + } + } +} + +#Preview("With devices") { + NavigationStack { + HardwareWalletsSettingsScreen() + .environment(HwWalletManager()) + .environment(TrezorManager()) + .environmentObject(SheetViewModel()) + .environmentObject(NavigationViewModel()) + .environmentObject(CurrencyViewModel()) + .environmentObject(SettingsViewModel.shared) + } + .preferredColorScheme(.dark) +} + +#Preview("Empty") { + NavigationStack { + HardwareWalletsSettingsScreen() + .environment(HwWalletManager()) + .environment(TrezorManager()) + .environmentObject(SheetViewModel()) + .environmentObject(NavigationViewModel()) + .environmentObject(CurrencyViewModel()) + .environmentObject(SettingsViewModel.shared) + } + .preferredColorScheme(.dark) +} diff --git a/Bitkit/Views/Settings/GeneralSettingsView.swift b/Bitkit/Views/Settings/GeneralSettingsView.swift index ac80c34fd..1ac89ef14 100644 --- a/Bitkit/Views/Settings/GeneralSettingsView.swift +++ b/Bitkit/Views/Settings/GeneralSettingsView.swift @@ -5,6 +5,8 @@ struct GeneralSettingsView: View { @AppStorage(PublicPaykitService.lightningPaymentOptionEnabledKey) private var lightningPaymentOptionEnabled = true @AppStorage(PublicPaykitService.onchainPaymentOptionEnabledKey) private var onchainPaymentOptionEnabled = true + @Environment(HwWalletManager.self) private var hwWalletManager + @EnvironmentObject var app: AppViewModel @EnvironmentObject var currency: CurrencyViewModel @EnvironmentObject var pubkyProfile: PubkyProfileManager @@ -110,6 +112,15 @@ struct GeneralSettingsView: View { ) } .accessibilityIdentifier("NotificationsSettings") + + NavigationLink(value: Route.hardwareWalletsSettings) { + SettingsRow( + title: t("settings__hardware_wallets__nav_title"), + iconName: "device-mobile-speaker", + rightText: String(hwWalletManager.wallets.count) + ) + } + .accessibilityIdentifier("HardwareWalletsSettings") } .padding(.top, 16) .padding(.horizontal, 16) @@ -138,6 +149,7 @@ struct GeneralSettingsView: View { .environmentObject(CurrencyViewModel()) .environmentObject(PubkyProfileManager()) .environmentObject(AppViewModel()) + .environment(HwWalletManager()) } .preferredColorScheme(.dark) } diff --git a/changelog.d/next/hw-wallet-settings.added.md b/changelog.d/next/hw-wallet-settings.added.md new file mode 100644 index 000000000..9b9390f9b --- /dev/null +++ b/changelog.d/next/hw-wallet-settings.added.md @@ -0,0 +1 @@ +Added a Hardware Wallets settings screen, reachable from Settings ▸ General ▸ Payments, to view and remove paired devices. From 748234473d0c14d471e589ad298cd752b8fc26be Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 29 Jun 2026 11:28:00 -0300 Subject: [PATCH 02/14] fix: ilustration alignemnt --- .../Localization/en.lproj/Localizable.strings | 1 - .../HardwareWalletsSettingsScreen.swift | 81 ++++++++++--------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 48e19536d..b6b04a723 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -687,7 +687,6 @@ "settings__general__section_interface" = "Interface"; "settings__general__section_payments" = "Payments"; "settings__hardware_wallets__nav_title" = "Hardware Wallets"; -"settings__hardware_wallets__empty_text" = "Pair a hardware device to watch its balance and activity in Bitkit."; "settings__hardware_wallets__add_button" = "Add Hardware Wallet"; "settings__widgets__nav_title" = "Widgets"; "settings__widgets__section_display" = "Display"; diff --git a/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift index 8f3e5a4f1..6fb3073e3 100644 --- a/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift +++ b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift @@ -1,9 +1,9 @@ import SwiftUI /// Manages all paired hardware wallets, reachable from Settings ▸ General ▸ Payments. Lists each -/// paired device with a connection badge, name, balance and a per-row delete; an empty state when -/// none are paired; and an "Add Hardware Wallet" button that opens the existing intro flow. Tapping -/// a row opens the device's `HardwareWalletScreen`. Ports bitkit-android's `HardwareWalletsSettingsScreen`. +/// paired device with a connection badge, name, balance and a per-row delete; an intro-style empty +/// state when none are paired; and an "Add Hardware Wallet" button that opens the existing intro +/// flow. Tapping a row opens the device's `HardwareWalletScreen`. struct HardwareWalletsSettingsScreen: View { @Environment(HwWalletManager.self) private var hwWalletManager @Environment(TrezorManager.self) private var trezorManager @@ -17,37 +17,25 @@ struct HardwareWalletsSettingsScreen: View { } var body: some View { - VStack(alignment: .leading, spacing: 0) { + VStack(spacing: 0) { NavigationBar(title: t("settings__hardware_wallets__nav_title")) .padding(.horizontal, 16) - ZStack(alignment: .bottom) { - HwDeviceIllustrations() - .frame(maxWidth: .infinity) - .frame(maxHeight: .infinity, alignment: .bottom) - .padding(.bottom, ScreenLayout.bottomPaddingWithSafeArea) - .allowsHitTesting(false) - - VStack(spacing: 0) { - if wallets.isEmpty { - emptyState - .frame(maxHeight: .infinity, alignment: .center) - } else { - deviceList - } - - CustomButton( - title: t("settings__hardware_wallets__add_button"), - variant: .secondary, - shouldExpand: true - ) { - sheets.showSheet(.hardwareIntro) - } - .accessibilityIdentifier("AddHardwareWallet") - .padding(.bottom, 16) - } - .padding(.horizontal, 16) + if wallets.isEmpty { + emptyState + } else { + deviceList + } + + CustomButton( + title: t("settings__hardware_wallets__add_button"), + shouldExpand: true + ) { + sheets.showSheet(.hardwareIntro) } + .accessibilityIdentifier("AddHardwareWallet") + .padding(.horizontal, 16) + .padding(.bottom, 16) } .navigationBarHidden(true) .accessibilityIdentifier("HardwareWalletsScreen") @@ -65,12 +53,21 @@ struct HardwareWalletsSettingsScreen: View { } } + /// Mirrors the hardware intro sheet: staggered device hero filling the top, then an accent title + /// and copy above the Add button. private var emptyState: some View { - VStack(alignment: .leading, spacing: 8) { - DisplayText(t("settings__hardware_wallets__nav_title")) - BodyMText(t("settings__hardware_wallets__empty_text"), textColor: .white80) + VStack(spacing: 0) { + HwDeviceIllustrations() + .frame(maxWidth: .infinity, maxHeight: .infinity) + + VStack(alignment: .leading, spacing: 8) { + DisplayText(t("hardware__intro_header"), accentColor: .blueAccent) + BodyMText(t("hardware__intro_text")) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + .padding(.bottom, 32) } - .frame(maxWidth: .infinity, alignment: .leading) } private var deviceList: some View { @@ -85,8 +82,16 @@ struct HardwareWalletsSettingsScreen: View { CustomDivider() } } + .padding(.horizontal, 16) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .background(alignment: .bottom) { + HwDeviceIllustrations() + .frame(height: 256) + // Figma keeps a 59.77pt gap between the illustration and the Add button. + .padding(.bottom, 59.77) + .allowsHitTesting(false) } - .frame(maxHeight: .infinity) } /// Stop watching and forget every entry for the device (it may be paired over multiple @@ -115,13 +120,13 @@ private struct HwWalletRow: View { HStack(spacing: 12) { HwConnectionBadge(isConnected: wallet.isConnected) - BodyMText(wallet.name) + BodyMText(wallet.name, textColor: .textPrimary) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .leading) MoneyText( sats: Int(clamping: wallet.balanceSats), - size: .bodySSB, + size: .bodyMSB, symbol: true, color: .white64, symbolColor: .white64 @@ -145,7 +150,7 @@ private struct HwWalletRow: View { .buttonStyle(.plain) .accessibilityIdentifier("HardwareWalletRowDelete_\(wallet.id)") } - .frame(height: 52) + .frame(height: 50) .accessibilityIdentifier("HardwareWalletRow_\(wallet.id)") } } From 778b3295bf683d02da6125d849a78fde99116e48 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 29 Jun 2026 11:28:08 -0300 Subject: [PATCH 03/14] fix: ilustration alignemnt --- .../Views/Settings/General/HardwareWalletsSettingsScreen.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift index 6fb3073e3..a0bf0e6db 100644 --- a/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift +++ b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift @@ -53,8 +53,6 @@ struct HardwareWalletsSettingsScreen: View { } } - /// Mirrors the hardware intro sheet: staggered device hero filling the top, then an accent title - /// and copy above the Add button. private var emptyState: some View { VStack(spacing: 0) { HwDeviceIllustrations() From c7e266266c230310d4d753263ff17f81cd473663 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 29 Jun 2026 11:29:27 -0300 Subject: [PATCH 04/14] refactor: comments cleanup --- .../Settings/General/HardwareWalletsSettingsScreen.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift index a0bf0e6db..4f5ab7587 100644 --- a/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift +++ b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift @@ -86,16 +86,11 @@ struct HardwareWalletsSettingsScreen: View { .background(alignment: .bottom) { HwDeviceIllustrations() .frame(height: 256) - // Figma keeps a 59.77pt gap between the illustration and the Add button. .padding(.bottom, 59.77) .allowsHitTesting(false) } } - /// Stop watching and forget every entry for the device (it may be paired over multiple - /// transports). Mirrors `HardwareWalletScreen.removeWallet()`: `removeDevice` stops the watchers - /// and deletes the persisted activities, then `forgetDevice` clears credentials and drops the - /// known-device entry, which re-pushes the snapshot and removes it from the list. private func remove(_ wallet: HwWallet) async { pendingRemoval = nil hwWalletManager.removeDevice(id: wallet.id) From 560bfc256d5cd3a514313329d15eddf6f9d864ed Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 29 Jun 2026 11:49:17 -0300 Subject: [PATCH 05/14] fix: trash button color and item padding --- .../Views/Settings/General/HardwareWalletsSettingsScreen.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift index 4f5ab7587..f3a23866a 100644 --- a/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift +++ b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift @@ -80,6 +80,7 @@ struct HardwareWalletsSettingsScreen: View { CustomDivider() } } + .padding(.top, 14) .padding(.horizontal, 16) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) @@ -135,7 +136,7 @@ private struct HwWalletRow: View { .resizable() .scaledToFit() .frame(width: 24, height: 24) - .foregroundColor(.white64) + .foregroundColor(.white) .padding(.leading, 12) .padding(.vertical, 8) .contentShape(Rectangle()) From 8fda979cd26dff09fdcf8329a05f7e20bfc564e4 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 29 Jun 2026 12:32:58 -0300 Subject: [PATCH 06/14] feat: add rename feature --- .../Extensions/TrezorDevice+DisplayName.swift | 10 ++- Bitkit/MainNavView.swift | 8 ++ Bitkit/Managers/TrezorManager.swift | 24 ++++++ .../Localization/en.lproj/Localizable.strings | 2 + .../Trezor/TrezorKnownDeviceStorage.swift | 16 +++- Bitkit/ViewModels/SheetViewModel.swift | 14 ++++ .../HardwareWalletsSettingsScreen.swift | 50 +++++++------ .../Sheets/RenameHardwareWalletSheet.swift | 73 +++++++++++++++++++ ...-wallet-settings.added.md => 612.added.md} | 0 9 files changed, 168 insertions(+), 29 deletions(-) create mode 100644 Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift rename changelog.d/next/{hw-wallet-settings.added.md => 612.added.md} (100%) diff --git a/Bitkit/Extensions/TrezorDevice+DisplayName.swift b/Bitkit/Extensions/TrezorDevice+DisplayName.swift index 343816001..bef96a1b4 100644 --- a/Bitkit/Extensions/TrezorDevice+DisplayName.swift +++ b/Bitkit/Extensions/TrezorDevice+DisplayName.swift @@ -1,8 +1,10 @@ import BitkitCore -/// Canonical Trezor display name: the user-set label when it differs from the factory model, -/// otherwise the vendor-prefixed model, falling back to "Trezor". -func resolveHwWalletName(label: String?, model: String?) -> String { +/// Canonical Trezor display name: the Bitkit-side custom name when set, otherwise the device's own +/// label when it differs from the factory model, otherwise the vendor-prefixed model, falling back +/// to "Trezor". +func resolveHwWalletName(label: String?, model: String?, customLabel: String? = nil) -> String { + if let customLabel, !customLabel.isEmpty { return customLabel } if let label, !label.isEmpty, label != model { return label } guard let model else { return "Trezor" } return model.hasPrefix("Trezor") ? model : "Trezor \(model)" @@ -10,7 +12,7 @@ func resolveHwWalletName(label: String?, model: String?) -> String { extension TrezorKnownDevice { var displayName: String { - resolveHwWalletName(label: label, model: model) + resolveHwWalletName(label: label, model: model, customLabel: customLabel) } } diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index 55a331a34..efb935581 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -213,6 +213,14 @@ struct MainNavView: View { ) { config in HardwarePairingSheet(config: config) } + .sheet( + item: $sheets.renameHardwareWalletSheetItem, + onDismiss: { + sheets.hideSheet() + } + ) { + config in RenameHardwareWalletSheet(config: config) + } .onChange(of: trezorManager.showPairingCode) { _, needsCode in // A hardware device asked for its one-time pairing code (e.g. during reconnect); // surface the app-wide Pair Device sheet. Hidden again once submitted/cancelled. diff --git a/Bitkit/Managers/TrezorManager.swift b/Bitkit/Managers/TrezorManager.swift index 50f9451a5..f5c6d9a4e 100644 --- a/Bitkit/Managers/TrezorManager.swift +++ b/Bitkit/Managers/TrezorManager.swift @@ -427,6 +427,29 @@ final class TrezorManager { knownDevices = TrezorKnownDeviceStorage.loadAll() } + /// Set the Bitkit-side custom name for a paired device. The name is trimmed and capped; an empty + /// result clears the custom name (falling back to the device label/model). Applies to every stored + /// entry sharing the target's xpub set so the same device renamed over either transport stays + /// consistent, then reloads so the snapshot re-pushes and `HwWallet.name` updates. + func renameDevice(id: String, newName: String) { + let devices = TrezorKnownDeviceStorage.loadAll() + guard let target = devices.first(where: { $0.id == id }) else { return } + + let trimmed = String(newName.trimmingCharacters(in: .whitespacesAndNewlines).prefix(Self.deviceLabelMaxLength)) + let customLabel = trimmed.isEmpty ? nil : trimmed + + let updated = devices.map { device -> TrezorKnownDevice in + let sameGroup = device.id == id || (!target.xpubs.isEmpty && device.xpubs == target.xpubs) + guard sameGroup else { return device } + var copy = device + copy.customLabel = customLabel + return copy + } + TrezorKnownDeviceStorage.saveAll(updated) + loadKnownDevices() + trezorLog("Renamed device \(id) to \(customLabel ?? "")") + } + /// Captures the connected device's account xpubs so watch-only balances/activity stay available /// while disconnected. The watch-only wallet id is derived from the captured xpub set, so a save /// is blocked only when an address type failed *transiently* (a retryable transport error) and @@ -474,6 +497,7 @@ final class TrezorManager { private static let maxXpubFetchAttempts = 3 private static let xpubFetchRetryDelayNanos: UInt64 = 300_000_000 + private static let deviceLabelMaxLength = 50 /// Markers (matched against the underlying `TrezorError` carried in the wrapped error's text) /// for transient transport problems worth retrying. Anything else is treated as the address diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index b6b04a723..9433138f7 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -688,6 +688,8 @@ "settings__general__section_payments" = "Payments"; "settings__hardware_wallets__nav_title" = "Hardware Wallets"; "settings__hardware_wallets__add_button" = "Add Hardware Wallet"; +"settings__hardware_wallets__rename_title" = "Rename Hardware Wallet"; +"settings__hardware_wallets__name_label" = "Name"; "settings__widgets__nav_title" = "Widgets"; "settings__widgets__section_display" = "Display"; "settings__widgets__section_reset" = "Reset To Defaults"; diff --git a/Bitkit/Services/Trezor/TrezorKnownDeviceStorage.swift b/Bitkit/Services/Trezor/TrezorKnownDeviceStorage.swift index 63f22d649..e2407b598 100644 --- a/Bitkit/Services/Trezor/TrezorKnownDeviceStorage.swift +++ b/Bitkit/Services/Trezor/TrezorKnownDeviceStorage.swift @@ -12,6 +12,9 @@ struct TrezorKnownDevice: Codable, Identifiable { /// Account-level extended public keys keyed by `AddressScriptType.stringValue`. /// Persisted so watch-only balances/activity stay available while disconnected. var xpubs: [String: String] + /// User-set name applied while managing the wallet in Bitkit; nil until renamed. Takes priority + /// over the device's own `label`/`model` when resolving the display name. + var customLabel: String? init( id: String, @@ -21,7 +24,8 @@ struct TrezorKnownDevice: Codable, Identifiable { label: String? = nil, model: String? = nil, lastConnectedAt: Date, - xpubs: [String: String] = [:] + xpubs: [String: String] = [:], + customLabel: String? = nil ) { self.id = id self.name = name @@ -31,6 +35,7 @@ struct TrezorKnownDevice: Codable, Identifiable { self.model = model self.lastConnectedAt = lastConnectedAt self.xpubs = xpubs + self.customLabel = customLabel } init(from decoder: any Decoder) throws { @@ -43,6 +48,7 @@ struct TrezorKnownDevice: Codable, Identifiable { model = try container.decodeIfPresent(String.self, forKey: .model) lastConnectedAt = try container.decode(Date.self, forKey: .lastConnectedAt) xpubs = try container.decodeIfPresent([String: String].self, forKey: .xpubs) ?? [:] + customLabel = try container.decodeIfPresent(String.self, forKey: .customLabel) } } @@ -68,6 +74,14 @@ enum TrezorKnownDeviceStorage { } } + /// Persist the full device list as-is. Used for bulk updates (e.g. renaming every entry of a + /// device shared across transports) without per-device reordering. + static func saveAll(_ devices: [TrezorKnownDevice]) { + if let data = try? JSONEncoder().encode(devices) { + UserDefaults.standard.set(data, forKey: key) + } + } + /// Remove a known device by ID static func remove(id: String) { var devices = loadAll() diff --git a/Bitkit/ViewModels/SheetViewModel.swift b/Bitkit/ViewModels/SheetViewModel.swift index f9e53c39c..e3228cbf1 100644 --- a/Bitkit/ViewModels/SheetViewModel.swift +++ b/Bitkit/ViewModels/SheetViewModel.swift @@ -26,6 +26,7 @@ enum SheetID: String, CaseIterable { case widgets case hardwareIntro case hardwarePairing + case renameHardwareWallet } struct SheetConfiguration { @@ -327,6 +328,19 @@ class SheetViewModel: ObservableObject { } } + var renameHardwareWalletSheetItem: RenameHardwareWalletSheetItem? { + get { + guard let config = activeSheetConfiguration, config.id == .renameHardwareWallet else { return nil } + guard let data = config.data as? RenameHardwareWalletConfig else { return nil } + return RenameHardwareWalletSheetItem(deviceId: data.deviceId, currentName: data.currentName) + } + set { + if newValue == nil { + activeSheetConfiguration = nil + } + } + } + var scannerSheetItem: ScannerSheetItem? { get { guard let config = activeSheetConfiguration, config.id == .scanner else { return nil } diff --git a/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift index f3a23866a..6e07cc2be 100644 --- a/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift +++ b/Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift @@ -7,7 +7,6 @@ import SwiftUI struct HardwareWalletsSettingsScreen: View { @Environment(HwWalletManager.self) private var hwWalletManager @Environment(TrezorManager.self) private var trezorManager - @EnvironmentObject private var navigation: NavigationViewModel @EnvironmentObject private var sheets: SheetViewModel @State private var pendingRemoval: HwWallet? @@ -74,7 +73,12 @@ struct HardwareWalletsSettingsScreen: View { ForEach(wallets) { wallet in HwWalletRow( wallet: wallet, - onTap: { navigation.navigate(.hardwareWallet(deviceId: wallet.id)) }, + onRename: { + sheets.showSheet( + .renameHardwareWallet, + data: RenameHardwareWalletConfig(deviceId: wallet.id, currentName: wallet.name) + ) + }, onRemove: { pendingRemoval = wallet } ) CustomDivider() @@ -101,34 +105,33 @@ struct HardwareWalletsSettingsScreen: View { } } -/// A paired hardware wallet row: connection badge, name, balance and a trailing delete. The badge, -/// name and balance navigate to the device's detail screen; the trash button removes it. +/// A paired hardware wallet row: connection badge, name, balance and a trailing delete. Tapping the +/// name opens the rename sheet; the trash button removes the device. private struct HwWalletRow: View { let wallet: HwWallet - let onTap: () -> Void + let onRename: () -> Void let onRemove: () -> Void var body: some View { - HStack(spacing: 0) { - Button(action: onTap) { - HStack(spacing: 12) { - HwConnectionBadge(isConnected: wallet.isConnected) - - BodyMText(wallet.name, textColor: .textPrimary) - .lineLimit(1) - .frame(maxWidth: .infinity, alignment: .leading) - - MoneyText( - sats: Int(clamping: wallet.balanceSats), - size: .bodyMSB, - symbol: true, - color: .white64, - symbolColor: .white64 - ) - } - .contentShape(Rectangle()) + HStack(spacing: 12) { + HwConnectionBadge(isConnected: wallet.isConnected) + + Button(action: onRename) { + BodyMText(wallet.name, textColor: .textPrimary) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) } .buttonStyle(.plain) + .accessibilityIdentifier("HardwareWalletRowName_\(wallet.id)") + + MoneyText( + sats: Int(clamping: wallet.balanceSats), + size: .bodyMSB, + symbol: true, + color: .white64, + symbolColor: .white64 + ) Button(action: onRemove) { Image("trash") @@ -137,7 +140,6 @@ private struct HwWalletRow: View { .scaledToFit() .frame(width: 24, height: 24) .foregroundColor(.white) - .padding(.leading, 12) .padding(.vertical, 8) .contentShape(Rectangle()) } diff --git a/Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift b/Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift new file mode 100644 index 000000000..d13e4e75a --- /dev/null +++ b/Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift @@ -0,0 +1,73 @@ +import SwiftUI + +struct RenameHardwareWalletConfig { + let deviceId: String + let currentName: String +} + +struct RenameHardwareWalletSheetItem: SheetItem, Equatable { + let id: SheetID = .renameHardwareWallet + let size: SheetSize = .small + let deviceId: String + let currentName: String +} + +/// Renames a paired hardware wallet: a single NAME field pre-filled with the current name and a Save +/// button. Persists the custom name via `TrezorManager.renameDevice`, which re-pushes the device +/// snapshot so `HwWallet.name` updates everywhere. +struct RenameHardwareWalletSheet: View { + @Environment(TrezorManager.self) private var trezorManager + @EnvironmentObject private var sheets: SheetViewModel + + let config: RenameHardwareWalletSheetItem + + @State private var name: String = "" + @FocusState private var isNameFocused: Bool + + private var trimmedName: String { + name.trimmingCharacters(in: .whitespacesAndNewlines) + } + + var body: some View { + Sheet(id: .renameHardwareWallet) { + VStack(alignment: .leading, spacing: 0) { + SheetHeader(title: t("settings__hardware_wallets__rename_title")) + + CaptionMText(t("settings__hardware_wallets__name_label")) + .padding(.bottom, 8) + + TextField( + config.currentName, + text: $name, + testIdentifier: "RenameHardwareWalletInput" + ) + .focused($isNameFocused) + .submitLabel(.done) + .onSubmit(save) + + Spacer(minLength: 16) + + CustomButton( + title: t("common__save"), + isDisabled: trimmedName.isEmpty, + shouldExpand: true + ) { + save() + } + .buttonBottomPadding(isFocused: isNameFocused) + .accessibilityIdentifier("RenameHardwareWalletSave") + } + .padding(.horizontal, 16) + .task { + name = config.currentName + isNameFocused = true + } + } + } + + private func save() { + guard !trimmedName.isEmpty else { return } + trezorManager.renameDevice(id: config.deviceId, newName: trimmedName) + sheets.hideSheet() + } +} diff --git a/changelog.d/next/hw-wallet-settings.added.md b/changelog.d/next/612.added.md similarity index 100% rename from changelog.d/next/hw-wallet-settings.added.md rename to changelog.d/next/612.added.md From 146bee431a6502f0541758ff90bde7e8ca28045c Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 29 Jun 2026 13:59:33 -0300 Subject: [PATCH 07/14] test: add name tests --- BitkitTests/HwWalletNameTests.swift | 21 +++++++++++++++++++++ changelog.d/next/612.added.md | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/BitkitTests/HwWalletNameTests.swift b/BitkitTests/HwWalletNameTests.swift index 11b59bc94..5b2f28fc3 100644 --- a/BitkitTests/HwWalletNameTests.swift +++ b/BitkitTests/HwWalletNameTests.swift @@ -25,4 +25,25 @@ final class HwWalletNameTests: XCTestCase { func testEmptyLabelFallsBackToModel() { XCTAssertEqual(resolveHwWalletName(label: "", model: "Safe 5"), "Trezor Safe 5") } + + func testCustomLabelTakesPriorityOverLabelAndModel() { + XCTAssertEqual( + resolveHwWalletName(label: "My Trezor", model: "Safe 5", customLabel: "Cold Storage"), + "Cold Storage" + ) + } + + func testEmptyCustomLabelFallsBackToLabel() { + XCTAssertEqual( + resolveHwWalletName(label: "My Trezor", model: "Safe 5", customLabel: ""), + "My Trezor" + ) + } + + func testNilCustomLabelFallsBackToPrefixedModel() { + XCTAssertEqual( + resolveHwWalletName(label: nil, model: "Safe 5", customLabel: nil), + "Trezor Safe 5" + ) + } } diff --git a/changelog.d/next/612.added.md b/changelog.d/next/612.added.md index 9b9390f9b..7a438c2b1 100644 --- a/changelog.d/next/612.added.md +++ b/changelog.d/next/612.added.md @@ -1 +1 @@ -Added a Hardware Wallets settings screen, reachable from Settings ▸ General ▸ Payments, to view and remove paired devices. +Added a Hardware Wallets settings screen, reachable from Settings ▸ General ▸ Payments, to view, rename, and remove paired devices. From 5581bac490dbe6970d9890b82d0338f4996d284b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 1 Jul 2026 07:35:31 -0300 Subject: [PATCH 08/14] fix: display display disconnected status when bluetooth is off or device is out of range --- Bitkit/Managers/TrezorManager.swift | 11 ++++++ Bitkit/Services/Trezor/TrezorBLEManager.swift | 37 +++++++++++++++++++ Bitkit/Services/Trezor/TrezorTransport.swift | 6 +++ 3 files changed, 54 insertions(+) diff --git a/Bitkit/Managers/TrezorManager.swift b/Bitkit/Managers/TrezorManager.swift index 3485dc616..f722002ae 100644 --- a/Bitkit/Managers/TrezorManager.swift +++ b/Bitkit/Managers/TrezorManager.swift @@ -125,6 +125,17 @@ final class TrezorManager { } .store(in: &cancellables) + // A spontaneous BLE drop (device out of range or phone Bluetooth turned off) + // must clear the live session. + transport.externalDisconnectPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] path in + guard let self, connectedDevice?.path == path else { return } + trezorLog("External disconnect for \(path); clearing session") + clearDisconnectedDeviceState() + } + .store(in: &cancellables) + // Passphrase entry is now driven proactively by the wallet-mode selector // (see setWalletMode / requestPassphraseWallet). The device callback // `onPassphraseRequest` is answered silently from the selected mode, so there diff --git a/Bitkit/Services/Trezor/TrezorBLEManager.swift b/Bitkit/Services/Trezor/TrezorBLEManager.swift index 0af190e52..13d8765ce 100644 --- a/Bitkit/Services/Trezor/TrezorBLEManager.swift +++ b/Bitkit/Services/Trezor/TrezorBLEManager.swift @@ -1,3 +1,4 @@ +import Combine import CoreBluetooth import Foundation import Observation @@ -47,6 +48,15 @@ class TrezorBLEManager: NSObject { private var connectedPeripheral: CBPeripheral? private var connectedPath: String? + /// Paths whose disconnect was user-initiated (via `disconnect(path:)`), so the + /// spontaneous-disconnect signal is suppressed for deliberate teardowns. + private var expectedDisconnectPaths: Set = [] + + /// Emits the device path when an established connection drops for a reason the + /// user did not request — device out of range or phone Bluetooth turned off. + /// `TrezorManager` subscribes (via `TrezorTransport`) to clear the stale session. + let externalDisconnectPublisher = PassthroughSubject() + /// GATT characteristics private var writeCharacteristic: CBCharacteristic? private var notifyCharacteristic: CBCharacteristic? @@ -437,6 +447,10 @@ class TrezorBLEManager: NSObject { debugLog("Disconnecting: \(path)") + // Mark this as a deliberate teardown so the resulting didDisconnect callback + // does not surface as a spontaneous (out-of-range) drop. + expectedDisconnectPaths.insert(path) + if let notifyChar = notifyCharacteristic { peripheral.setNotifyValue(false, for: notifyChar) } @@ -550,6 +564,20 @@ extension TrezorBLEManager: CBCentralManagerDelegate { self.isScanning = false } } + + // Bluetooth turned off / unauthorized invalidates any live connection. iOS does + // not guarantee a per-peripheral didDisconnect on power-off, so treat it as a + // spontaneous drop here: clear internal state and surface the disconnect. + if central.state != .poweredOn, let path = connectedPath { + connectedPeripheral = nil + connectedPath = nil + writeCharacteristic = nil + notifyCharacteristic = nil + readQueue.clear() + if expectedDisconnectPaths.remove(path) == nil { + externalDisconnectPublisher.send(path) + } + } } func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, @@ -604,11 +632,20 @@ extension TrezorBLEManager: CBCentralManagerDelegate { takeWriteContinuation()?.resume(throwing: disconnectError) if connectedPeripheral?.identifier == peripheral.identifier { + let path = connectedPath connectedPeripheral = nil connectedPath = nil writeCharacteristic = nil notifyCharacteristic = nil readQueue.clear() + + // Surface a spontaneous drop (out of range) so the session can be cleared. + // A deliberate disconnect(path:) is suppressed via expectedDisconnectPaths. + if let path { + if expectedDisconnectPaths.remove(path) == nil { + externalDisconnectPublisher.send(path) + } + } } } } diff --git a/Bitkit/Services/Trezor/TrezorTransport.swift b/Bitkit/Services/Trezor/TrezorTransport.swift index 4649c718f..b143c488a 100644 --- a/Bitkit/Services/Trezor/TrezorTransport.swift +++ b/Bitkit/Services/Trezor/TrezorTransport.swift @@ -305,6 +305,12 @@ final class TrezorTransport: TrezorTransportCallback { bleManager.bluetoothState } + /// Emits a device path when an established BLE connection drops unexpectedly + /// (out of range or phone Bluetooth turned off). + var externalDisconnectPublisher: PassthroughSubject { + bleManager.externalDisconnectPublisher + } + var isBridgeEnabled: Bool { bridgeTransport.isEnabled } From a2ca91fcb8f4db2374cb911e6be51592e4812951 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Jul 2026 07:36:15 -0300 Subject: [PATCH 09/14] consume any deliberate disconnect marker even when the path was already cleared, so it can't be mistaken fir a future real drop --- Bitkit/Services/Trezor/TrezorBLEManager.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Bitkit/Services/Trezor/TrezorBLEManager.swift b/Bitkit/Services/Trezor/TrezorBLEManager.swift index 13d8765ce..a7cd073a9 100644 --- a/Bitkit/Services/Trezor/TrezorBLEManager.swift +++ b/Bitkit/Services/Trezor/TrezorBLEManager.swift @@ -620,7 +620,11 @@ extension TrezorBLEManager: CBCentralManagerDelegate { takeConnectContinuation()?.resume(throwing: error ?? TrezorBLEError.connectionFailed) } - func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + func centralManager( + _ central: CBCentralManager, + didDisconnectPeripheral peripheral: CBPeripheral, + error: Error? + ) { debugLog("didDisconnect: \(peripheral.identifier)\(error.map { " error: \($0.localizedDescription)" } ?? "")") let disconnectError = error ?? TrezorBLEError.connectionFailed @@ -631,8 +635,12 @@ extension TrezorBLEManager: CBCentralManagerDelegate { takeNotificationContinuation()?.resume(throwing: disconnectError) takeWriteContinuation()?.resume(throwing: disconnectError) + let path = "ble:\(peripheral.identifier.uuidString)" + // Consume any deliberate-disconnect marker even when disconnect(path:) already + // cleared connection state, so it can't be mistaken for a future real drop. + let wasExpected = expectedDisconnectPaths.remove(path) != nil + if connectedPeripheral?.identifier == peripheral.identifier { - let path = connectedPath connectedPeripheral = nil connectedPath = nil writeCharacteristic = nil @@ -641,10 +649,8 @@ extension TrezorBLEManager: CBCentralManagerDelegate { // Surface a spontaneous drop (out of range) so the session can be cleared. // A deliberate disconnect(path:) is suppressed via expectedDisconnectPaths. - if let path { - if expectedDisconnectPaths.remove(path) == nil { - externalDisconnectPublisher.send(path) - } + if !wasExpected { + externalDisconnectPublisher.send(path) } } } From d4b1096abe3c88c8d837bf4ff95a1c966a77897b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Jul 2026 07:42:32 -0300 Subject: [PATCH 10/14] fix: get custom label on reconnect known device --- Bitkit/Managers/TrezorManager.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Bitkit/Managers/TrezorManager.swift b/Bitkit/Managers/TrezorManager.swift index f722002ae..202a01631 100644 --- a/Bitkit/Managers/TrezorManager.swift +++ b/Bitkit/Managers/TrezorManager.swift @@ -498,7 +498,8 @@ final class TrezorManager { label: device.label ?? deviceFeatures?.label, model: device.model ?? deviceFeatures?.model, lastConnectedAt: Date(), - xpubs: mergedXpubs + xpubs: mergedXpubs, + customLabel: previous?.customLabel ) TrezorKnownDeviceStorage.save(known) loadKnownDevices() From cffd5655472f866b0b6fef2c02c17cb3fcdb1228 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Jul 2026 08:00:30 -0300 Subject: [PATCH 11/14] fix: display custom name on dev screen --- Bitkit/Managers/TrezorManager.swift | 13 +++++++++++++ Bitkit/Views/Trezor/TrezorConnectedView.swift | 8 ++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Bitkit/Managers/TrezorManager.swift b/Bitkit/Managers/TrezorManager.swift index 202a01631..7805a33be 100644 --- a/Bitkit/Managers/TrezorManager.swift +++ b/Bitkit/Managers/TrezorManager.swift @@ -438,6 +438,19 @@ final class TrezorManager { knownDevices = TrezorKnownDeviceStorage.loadAll() } + /// Display name for the currently connected device, applying any Bitkit-side custom rename (from + /// the stored known-device record) over the device's own label/model — mirrors how watch-only + /// tiles resolve names, so a rename shows on the connected-device screen too. + var connectedDeviceDisplayName: String? { + guard let device = connectedDevice else { return nil } + let customLabel = knownDevices.first { $0.id == device.id }?.customLabel + return resolveHwWalletName( + label: device.label ?? deviceFeatures?.label, + model: device.model ?? deviceFeatures?.model, + customLabel: customLabel + ) + } + /// Set the Bitkit-side custom name for a paired device. The name is trimmed and capped; an empty /// result clears the custom name (falling back to the device label/model). Applies to every stored /// entry sharing the target's xpub set so the same device renamed over either transport stays diff --git a/Bitkit/Views/Trezor/TrezorConnectedView.swift b/Bitkit/Views/Trezor/TrezorConnectedView.swift index 35426a3d4..270c5d402 100644 --- a/Bitkit/Views/Trezor/TrezorConnectedView.swift +++ b/Bitkit/Views/Trezor/TrezorConnectedView.swift @@ -19,7 +19,7 @@ struct TrezorConnectedView: View { VStack(spacing: 24) { // Device card DeviceInfoCard( - device: trezorManager.connectedDevice, + name: trezorManager.connectedDeviceDisplayName ?? "Trezor", features: trezorManager.deviceFeatures ) @@ -143,7 +143,7 @@ struct TrezorConnectedView: View { ToolbarItem(placement: .navigationBarTrailing) { TrezorStatusBadge( isConnected: trezorManager.isConnected, - deviceName: trezorManager.deviceFeatures?.label + deviceName: trezorManager.connectedDeviceDisplayName ) .allowsHitTesting(false) } @@ -202,7 +202,7 @@ private struct WalletModeSelectorRow: View { // MARK: - Device Info Card private struct DeviceInfoCard: View { - let device: TrezorDeviceInfo? + let name: String let features: TrezorFeatures? var body: some View { @@ -217,7 +217,7 @@ private struct DeviceInfoCard: View { // Device name VStack(spacing: 4) { - Text(features?.label ?? "Trezor") + Text(name) .font(.system(size: 20, weight: .bold)) .foregroundColor(.white) From d9314a31ab958e8baa94727213e82b6f7d4a620a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Jul 2026 08:40:33 -0300 Subject: [PATCH 12/14] fix: clear any stale expectedDisconnectPaths marker at the start of connectOnce, so a reused BLE path can never carry a leaked suppression marker into a new session --- Bitkit/Services/Trezor/TrezorBLEManager.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Bitkit/Services/Trezor/TrezorBLEManager.swift b/Bitkit/Services/Trezor/TrezorBLEManager.swift index a7cd073a9..eab25581d 100644 --- a/Bitkit/Services/Trezor/TrezorBLEManager.swift +++ b/Bitkit/Services/Trezor/TrezorBLEManager.swift @@ -294,6 +294,10 @@ class TrezorBLEManager: NSObject { // Clean up any stale connection state (preserves discoveredPeripherals cache) cleanupConnectionState() + // Drop any stale deliberate-disconnect marker from a prior session so it can't + // suppress a future spontaneous drop on this reused path. + expectedDisconnectPaths.remove(path) + debugLog("connectOnce: \(path)") // Try to get peripheral from cache first From d237b35827c6bb52a3482d43e2a55fa8318c4f12 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Jul 2026 08:44:07 -0300 Subject: [PATCH 13/14] fix: don't guard empty name for allow reseting to default --- Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift b/Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift index d13e4e75a..5a581e709 100644 --- a/Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift +++ b/Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift @@ -49,7 +49,6 @@ struct RenameHardwareWalletSheet: View { CustomButton( title: t("common__save"), - isDisabled: trimmedName.isEmpty, shouldExpand: true ) { save() @@ -66,7 +65,6 @@ struct RenameHardwareWalletSheet: View { } private func save() { - guard !trimmedName.isEmpty else { return } trezorManager.renameDevice(id: config.deviceId, newName: trimmedName) sheets.hideSheet() } From cbecdacd807af075c77baff3cbe0e109a76f6026 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Jul 2026 09:00:22 -0300 Subject: [PATCH 14/14] fix: restore empty text gating --- Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift b/Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift index 5a581e709..d13e4e75a 100644 --- a/Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift +++ b/Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift @@ -49,6 +49,7 @@ struct RenameHardwareWalletSheet: View { CustomButton( title: t("common__save"), + isDisabled: trimmedName.isEmpty, shouldExpand: true ) { save() @@ -65,6 +66,7 @@ struct RenameHardwareWalletSheet: View { } private func save() { + guard !trimmedName.isEmpty else { return } trezorManager.renameDevice(id: config.deviceId, newName: trimmedName) sheets.hideSheet() }