Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@
INFOPLIST_FILE = Bitkit/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Bitkit;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth access is included as part of a library used for wallet operations. Bitkit does not actively use Bluetooth.";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bitkit uses Bluetooth to connect to your hardware wallet for signing transactions.";
INFOPLIST_KEY_NSCameraUsageDescription = "Bitkit needs access to the camera to scan QR codes";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
Expand Down Expand Up @@ -998,7 +998,7 @@
INFOPLIST_FILE = Bitkit/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Bitkit;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth access is included as part of a library used for wallet operations. Bitkit does not actively use Bluetooth.";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bitkit uses Bluetooth to connect to your hardware wallet for signing transactions.";
INFOPLIST_KEY_NSCameraUsageDescription = "Bitkit needs access to the camera to scan QR codes";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
Expand Down Expand Up @@ -1214,7 +1214,7 @@
repositoryURL = "https://github.com/synonymdev/bitkit-core";
requirement = {
kind = exactVersion;
version = 0.5.14;
version = 0.5.17;
};
};
96E20CD22CB6D91A00C24149 /* XCRemoteSwiftPackageReference "CodeScanner" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 29 additions & 10 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ struct AppScene: View {
@State private var keyboardManager = KeyboardManager()
@State private var trezorManager: TrezorManager
@State private var trezorViewModel: TrezorViewModel
@State private var jadeManager: JadeManager
@State private var hwWalletManager: HwWalletManager
@State private var calculatorInputManager = CalculatorInputManager()
@State private var paykitPaymentRequestManager = PaykitPaymentRequestManager()
Expand Down Expand Up @@ -258,7 +259,8 @@ struct AppScene: View {
// Created ahead of `transfer` so the hardware-wallet transfer flow can reach the funding
// (compose/sign/broadcast) and device-session (reconnect) capabilities.
let trezorManager = TrezorManager()
let hwWalletManager = HwWalletManager(session: trezorManager)
let jadeManager = JadeManager()
let hwWalletManager = HwWalletManager(trezorSession: trezorManager, jadeSession: jadeManager)

_transfer = StateObject(wrappedValue: TransferViewModel(
transferService: transferService,
Expand All @@ -283,6 +285,8 @@ struct AppScene: View {
let trezorViewModel = TrezorViewModel(connection: trezorManager)
_trezorManager = State(initialValue: trezorManager)
_trezorViewModel = State(initialValue: trezorViewModel)
// Held here because `HwWalletManager` keeps its vendor sessions weakly.
_jadeManager = State(initialValue: jadeManager)
_hwWalletManager = State(initialValue: hwWalletManager)

CoreService.shared.activity.setPrivatePaykitContactResolvers(
Expand Down Expand Up @@ -326,12 +330,13 @@ struct AppScene: View {
.onChange(of: scenePhase, initial: true) { _, newValue in handleScenePhaseChange(newValue) }
.onChange(of: network.isConnected) { _, isConnected in handleNetworkChange(isConnected) }
.onOpenURL { url in app.retainDeepLink(url) }
// Bridge Trezor device state into the watch-only manager without coupling the two:
// TrezorManager bumps devicesRevision on any device/connection change.
// Bridge the vendor managers' device state into the watch-only manager without coupling them:
// each bumps devicesRevision on any device or connection change.
.onChange(of: trezorManager.devicesRevision) { _, _ in pushHardwareDevices() }
.onChange(of: jadeManager.devicesRevision) { _, _ in pushHardwareDevices() }
.onChange(of: isPinVerified) { _, verified in
if verified {
Task { await trezorManager.autoReconnect() }
Task { await hwWalletManager.reconnectOnForeground() }
}
}
.onReceive(settings.settingsPublisher) { _ in hwWalletManager.reconcileForSettingsChange() }
Expand Down Expand Up @@ -379,6 +384,7 @@ struct AppScene: View {
.environment(keyboardManager)
.environment(trezorManager)
.environment(trezorViewModel)
.environment(jadeManager)
.environment(hwWalletManager)
.environment(calculatorInputManager)
.environment(paykitPaymentRequestManager)
Expand Down Expand Up @@ -789,6 +795,7 @@ struct AppScene: View {

@Sendable
private func setupTask() async {
AppReset.hardwareWallets = hwWalletManager
do {
// Handle orphaned keychain before anything else
handleOrphanedKeychain()
Expand All @@ -800,6 +807,7 @@ struct AppScene: View {
// watchers start at launch (no-op until a device is paired). loadKnownDevices() also
// bumps devicesRevision, but push explicitly so the initial state is delivered.
trezorManager.loadKnownDevices()
jadeManager.loadKnownDevices()
pushHardwareDevices()

// Setup TimedSheetManager with all timed sheets
Expand Down Expand Up @@ -938,12 +946,16 @@ struct AppScene: View {
// If PIN is enabled, lock the app when the app goes to the background
isPinVerified = false
}
hwWalletManager.onAppBackgrounded()
}

// `.inactive` is left alone: the iOS Bluetooth pairing alert puts the app there mid-connect.
if newPhase == .active {
// Called even behind the PIN screen, so a background release still pending is called off.
hwWalletManager.onAppBecameActive()
// Reconnect a known hardware device so its connection indicator turns green again;
if isPinVerified || !settings.pinEnabled {
Task { await trezorManager.autoReconnect() }
Task { await hwWalletManager.reconnectOnForeground() }
}
if wallet.walletExists == true {
Task {
Expand Down Expand Up @@ -1344,13 +1356,20 @@ struct AppScene: View {
center.removeDeliveredNotifications(withIdentifiers: deliveredNotifications.map(\.request.identifier))
}

/// Feed the current Trezor device snapshot into the watch-only manager. This is the only link
/// between the two managers, kept in the composition root so neither type references the other.
/// Feed both vendors' device snapshots into the watch-only manager. This is the only link between
/// the vendor managers and it, kept in the composition root so none of them references another.
private func pushHardwareDevices() {
let connected: (deviceId: String, walletId: String?)? = if let trezorDevice = trezorManager.connectedDevice {
(trezorDevice.id, trezorManager.connectedWalletId)
} else if let jadeDevice = jadeManager.connected {
(jadeDevice.id, jadeDevice.walletId)
} else {
nil
}
hwWalletManager.updateDevices(
knownDevices: trezorManager.knownDevices,
connectedDeviceId: trezorManager.connectedDevice?.id,
connectedWalletId: trezorManager.connectedWalletId
knownDevices: (trezorManager.knownDevices + jadeManager.knownDevices).sorted { $0.lastConnectedAt > $1.lastConnectedAt },
connectedDeviceId: connected?.deviceId,
connectedWalletId: connected?.walletId
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "jade-placeholder.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion Bitkit/BitkitApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ class AppDelegate: NSObject, UIApplicationDelegate {

func applicationWillTerminate(_ application: UIApplication) {
try? StateLocker.unlock(.lightning)
// A Jade left holding a link when the process ends can drop its Bluetooth bond.
JadeTransport.shared.releaseAllImmediately()
}
}

Expand Down Expand Up @@ -178,7 +180,7 @@ struct BitkitApp: App {
init() {
UIWindow.appearance().overrideUserInterfaceStyle = .dark
if Env.shouldResetTrezorEmulatorState {
TrezorKnownDeviceStorage.removeAll()
HwKnownDeviceStorage.removeAll()
TrezorCredentialStorage.deleteAll()
}
_ = ToastWindowManager.shared
Expand Down
17 changes: 14 additions & 3 deletions Bitkit/Components/SegmentedControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@ import SwiftUI

struct TabItem<T: Hashable & CustomStringConvertible> {
let tab: T
/// Shown in place of the tab's own description, for a tab whose name depends on what it shows.
let label: String?
let activeColor: Color?
let badge: Int?
let accessibilityIdentifier: String?

init(_ tab: T, activeColor: Color? = nil, badge: Int? = nil, accessibilityIdentifier: String? = nil) {
init(_ tab: T, label: String? = nil, activeColor: Color? = nil, badge: Int? = nil, accessibilityIdentifier: String? = nil) {
self.tab = tab
self.label = label
self.activeColor = activeColor
self.badge = badge
self.accessibilityIdentifier = accessibilityIdentifier
}

var title: String {
label ?? tab.description
}

var resolvedAccessibilityIdentifier: String {
accessibilityIdentifier ?? "Tab-\(title.lowercased())"
}
}

struct SegmentedControl<T: Hashable & CustomStringConvertible>: View {
Expand Down Expand Up @@ -51,7 +62,7 @@ struct SegmentedControl<T: Hashable & CustomStringConvertible>: View {
badge(for: tabItem)
.hidden()
CaptionBText(
tabItem.tab.description,
tabItem.title,
textColor: selectedTab == tabItem.tab ? .white : inactiveColor ?? .secondary
)
badge(for: tabItem)
Expand All @@ -75,7 +86,7 @@ struct SegmentedControl<T: Hashable & CustomStringConvertible>: View {
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
.accessibilityIdentifier(tabItem.accessibilityIdentifier ?? "Tab-\(tabItem.tab.description.lowercased())")
.accessibilityIdentifier(tabItem.resolvedAccessibilityIdentifier)
}
}
.frame(maxWidth: .infinity)
Expand Down
2 changes: 1 addition & 1 deletion Bitkit/Components/TabBar/TabBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ struct TabBar: View {
sheets.showSheet(
.receive,
data: ReceiveConfig(
view: .qr(cjitInvoice: nil, tab: .trezor),
view: .qr(cjitInvoice: nil, tab: .hardware),
hardwareWalletId: walletId
)
)
Expand Down
2 changes: 1 addition & 1 deletion Bitkit/Components/Trezor/TrezorDeviceRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ struct TrezorDeviceRow: View {

/// Row displaying a previously connected (known) Trezor device
struct KnownDeviceRow: View {
let device: TrezorKnownDevice
let device: HwKnownDevice
let isConnecting: Bool
let onConnect: () -> Void
let onForget: () -> Void
Expand Down
31 changes: 31 additions & 0 deletions Bitkit/Extensions/HwError+Cancellation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// Vendor-neutral views over the Trezor and Jade error predicates, for code shared by every vendor.
/// A `TrezorError` is never a `JadeError`, so for a Trezor error each one answers as its `isTrezor*`
/// counterpart does.
extension Error {
func isHwUserCancellation() -> Bool {
isTrezorUserCancellation() || isJadeUserCancellation()
}

func isHwDeviceBusy() -> Bool {
isTrezorDeviceBusy() || isJadeDeviceBusy()
}

func isHwFirmwareError() -> Bool {
isTrezorFirmwareError() || isJadeFirmwareError()
}

func isHwSessionFailure() -> Bool {
isTrezorSessionFailure() || isJadeSessionFailure()
}

/// The vendor of the busy or locked device this error reports, so its busy copy can name it.
var hwBusyVendor: HwWalletVendor? {
if isJadeDeviceBusy() {
return .blockstream
}
if isTrezorDeviceBusy() {
return .trezor
}
return nil
}
}
63 changes: 63 additions & 0 deletions Bitkit/Extensions/HwWalletVendor+UI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Foundation

/// How each vendor's devices are shown and named across the hardware wallet screens.
extension HwWalletVendor {
/// The upright device illustration; the Jade one is a placeholder until design supplies the asset.
var deviceImageName: String {
switch self {
case .trezor: "trezor-device"
case .blockstream: "jade-placeholder"
}
}

/// The illustration shown while the device signs a transaction.
var signImageName: String {
switch self {
case .trezor: "trezor-card"
case .blockstream: "jade-placeholder"
}
}

var modelName: String {
switch self {
case .trezor: t("hardware__device_model_trezor")
case .blockstream: t("hardware__device_model_jade")
}
}

var foundHeader: String {
switch self {
case .trezor: t("hardware__found_header")
case .blockstream: t("hardware__found_header_jade")
}
}

var pairedHeader: String {
switch self {
case .trezor: t("hardware__paired_header")
case .blockstream: t("hardware__paired_header_jade")
}
}

var sendSignButtonTitle: String {
switch self {
case .trezor: t("hardware__send_open_connect")
case .blockstream: t("hardware__send_open_connect_jade")
}
}

var transferSignButtonTitle: String {
switch self {
case .trezor: t("lightning__transfer_hw__open_connect")
case .blockstream: t("hardware__send_open_connect_jade")
}
}

/// Passphrase (hidden) wallets are a Trezor feature; a Jade holds one wallet per device.
var supportsPassphraseWallets: Bool {
switch self {
case .trezor: true
case .blockstream: false
}
}
}
Loading
Loading