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
17 changes: 17 additions & 0 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,10 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.0;
OTHER_LDFLAGS = (
"-framework",
CoreBluetooth,
);
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.notification;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand Down Expand Up @@ -548,6 +552,10 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.0;
OTHER_LDFLAGS = (
"-framework",
CoreBluetooth,
);
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.notification;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand Down Expand Up @@ -680,6 +688,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Bitkit/Bitkit.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 186;
DEVELOPMENT_ASSET_PATHS = "\"Bitkit/Preview Content\"";
Expand Down Expand Up @@ -708,6 +717,10 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 2.2.0;
OTHER_LDFLAGS = (
"-framework",
CoreBluetooth,
);
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand Down Expand Up @@ -752,6 +765,10 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 2.2.0;
OTHER_LDFLAGS = (
"-framework",
CoreBluetooth,
);
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ struct AppScene: View {
@StateObject private var pubkyProfile = PubkyProfileManager()
@StateObject private var contactsManager = ContactsManager()
@State private var keyboardManager = KeyboardManager()
@State private var trezorViewModel = TrezorViewModel()

@State private var hideSplash = false
@State private var removeSplash = false
Expand Down Expand Up @@ -140,6 +141,7 @@ struct AppScene: View {
.environmentObject(pubkyProfile)
.environmentObject(contactsManager)
.environment(keyboardManager)
.environment(trezorViewModel)
.onChange(of: pubkyProfile.authState, initial: true) { _, authState in
if authState == .authenticated, let pk = pubkyProfile.publicKey {
Task { try? await contactsManager.loadContacts(for: pk) }
Expand Down
212 changes: 212 additions & 0 deletions Bitkit/Components/Trezor/TrezorDeviceRow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import BitkitCore
import SwiftUI

/// Row displaying a discovered Trezor device
struct TrezorDeviceRow: View {
let device: TrezorDeviceInfo
let isConnecting: Bool
let onConnect: () -> Void

var body: some View {
Button(action: {
if !isConnecting {
onConnect()
}
}) {
HStack(spacing: 16) {
// Device icon
Image(systemName: transportIcon)
.font(.system(size: 24))
.foregroundColor(.white)
.frame(width: 48, height: 48)
.background(Color.white.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 12))

// Device info
VStack(alignment: .leading, spacing: 4) {
Text(displayName)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(.white)

Text(transportLabel)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.6))
}

Spacer()

// Connect indicator or chevron
if isConnecting {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text("Connect")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.white.opacity(0.15))
.clipShape(Capsule())
}
}
.padding(16)
.background(Color.white.opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.buttonStyle(.plain)
.disabled(isConnecting)
}

private var displayName: String {
if let label = device.label, !label.isEmpty {
return label
}
return modelName
}

private var modelName: String {
if let model = device.model {
return "Trezor \(model)"
}
return "Trezor"
}

private var transportIcon: String {
switch device.transportType {
case .bluetooth:
return "wave.3.right"
case .usb:
return "cable.connector"
}
}

private var transportLabel: String {
switch device.transportType {
case .bluetooth:
return "Bluetooth"
case .usb:
return "USB"
}
}
}

// MARK: - Known Device Row

/// Row displaying a previously connected (known) Trezor device
struct KnownDeviceRow: View {
let device: TrezorKnownDevice
let isConnecting: Bool
let onConnect: () -> Void
let onForget: () -> Void

var body: some View {
HStack(spacing: 16) {
// Tap area for connect
Button(action: {
if !isConnecting {
onConnect()
}
}) {
HStack(spacing: 16) {
// Device icon
Image(systemName: device.transportType == "bluetooth" ? "wave.3.right" : "cable.connector")
.font(.system(size: 24))
.foregroundColor(.white)
.frame(width: 48, height: 48)
.background(Color.white.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 12))

// Device info
VStack(alignment: .leading, spacing: 4) {
Text(device.label ?? device.name)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(.white)

Text(device.lastConnectedAt.relativeDescription)
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.4))
}

Spacer()

if isConnecting {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
}
}
.buttonStyle(.plain)
.disabled(isConnecting)

// Forget button
Button(action: onForget) {
Image(systemName: "trash")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.4))
.padding(10)
}
.buttonStyle(.plain)
}
.padding(16)
.background(Color.white.opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}

// MARK: - Date Helper

extension Date {
private static let relativeDateFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full
return formatter
}()

/// Returns a relative description like "2 minutes ago"
var relativeDescription: String {
Self.relativeDateFormatter.localizedString(for: self, relativeTo: Date())
}
}

// MARK: - Preview

#if DEBUG
struct TrezorDeviceRow_Previews: PreviewProvider {
static var previews: some View {
ZStack {
Color.black.ignoresSafeArea()

VStack(spacing: 16) {
TrezorDeviceRow(
device: TrezorDeviceInfo(
id: "ble:12345",
transportType: .bluetooth,
name: "Trezor Safe 5",
path: "ble:12345",
label: "My Trezor",
model: "Safe 5",
isBootloader: false
),
isConnecting: false,
onConnect: {}
)

TrezorDeviceRow(
device: TrezorDeviceInfo(
id: "usb:001",
transportType: .usb,
name: "Trezor Model T",
path: "usb:001",
label: nil,
model: "Model T",
isBootloader: false
),
isConnecting: true,
onConnect: {}
)
}
.padding()
}
}
}
#endif
63 changes: 63 additions & 0 deletions Bitkit/Components/Trezor/TrezorExpandableSection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import SwiftUI

/// Reusable expandable section for the Trezor dashboard.
/// Provides a tappable header with animated expand/collapse of content.
struct TrezorExpandableSection<Content: View>: View {
let title: String
let icon: String
let description: String
@Binding var isExpanded: Bool
@ViewBuilder let content: () -> Content

var body: some View {
VStack(spacing: 0) {
// Tappable header
Button(action: {
withAnimation(.easeInOut(duration: 0.25)) {
isExpanded.toggle()
}
}) {
HStack(spacing: 16) {
Image(systemName: icon)
.font(.system(size: 20))
.foregroundColor(.white)
.frame(width: 40, height: 40)
.background(Color.white.opacity(0.1))
.clipShape(Circle())

VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(.white)

Text(description)
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.6))
}

Spacer()

Image(systemName: "chevron.down")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.4))
.rotationEffect(.degrees(isExpanded ? 0 : -90))
.animation(.easeInOut(duration: 0.25), value: isExpanded)
}
}

// Expandable content
if isExpanded {
Divider()
.background(Color.white.opacity(0.1))
.padding(.top, 12)

content()
.padding(.top, 12)
.transition(.opacity)
}
}
.padding(16)
.background(Color.white.opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Loading
Loading