Skip to content

Commit c930d2d

Browse files
authored
Merge pull request #507 from synonymdev/feat/trezor-integration
feat: trezor hardware support
2 parents cc1e08c + 47eb24b commit c930d2d

32 files changed

Lines changed: 7869 additions & 7 deletions

Bitkit.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,10 @@
522522
"@executable_path/../../Frameworks",
523523
);
524524
MARKETING_VERSION = 2.2.1;
525+
OTHER_LDFLAGS = (
526+
"-framework",
527+
CoreBluetooth,
528+
);
525529
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.notification;
526530
PRODUCT_NAME = "$(TARGET_NAME)";
527531
SDKROOT = iphoneos;
@@ -550,6 +554,10 @@
550554
"@executable_path/../../Frameworks",
551555
);
552556
MARKETING_VERSION = 2.2.1;
557+
OTHER_LDFLAGS = (
558+
"-framework",
559+
CoreBluetooth,
560+
);
553561
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.notification;
554562
PRODUCT_NAME = "$(TARGET_NAME)";
555563
SDKROOT = iphoneos;
@@ -710,6 +718,10 @@
710718
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
711719
MACOSX_DEPLOYMENT_TARGET = 14.0;
712720
MARKETING_VERSION = 2.2.1;
721+
OTHER_LDFLAGS = (
722+
"-framework",
723+
CoreBluetooth,
724+
);
713725
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit;
714726
PRODUCT_NAME = "$(TARGET_NAME)";
715727
SDKROOT = auto;
@@ -754,6 +766,10 @@
754766
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
755767
MACOSX_DEPLOYMENT_TARGET = 14.0;
756768
MARKETING_VERSION = 2.2.1;
769+
OTHER_LDFLAGS = (
770+
"-framework",
771+
CoreBluetooth,
772+
);
757773
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit;
758774
PRODUCT_NAME = "$(TARGET_NAME)";
759775
SDKROOT = auto;

Bitkit/AppScene.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ struct AppScene: View {
3030
@StateObject private var pubkyProfile = PubkyProfileManager()
3131
@StateObject private var contactsManager = ContactsManager()
3232
@State private var keyboardManager = KeyboardManager()
33+
@State private var trezorViewModel = TrezorViewModel()
3334

3435
@State private var hideSplash = false
3536
@State private var removeSplash = false
@@ -144,6 +145,7 @@ struct AppScene: View {
144145
.environmentObject(pubkyProfile)
145146
.environmentObject(contactsManager)
146147
.environment(keyboardManager)
148+
.environment(trezorViewModel)
147149
.onChange(of: pubkyProfile.authState, initial: true) { _, authState in
148150
if authState == .authenticated, let pk = pubkyProfile.publicKey {
149151
Task { try? await contactsManager.loadContacts(for: pk) }
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import BitkitCore
2+
import SwiftUI
3+
4+
/// Row displaying a discovered Trezor device
5+
struct TrezorDeviceRow: View {
6+
let device: TrezorDeviceInfo
7+
let isConnecting: Bool
8+
let onConnect: () -> Void
9+
10+
var body: some View {
11+
Button(action: {
12+
if !isConnecting {
13+
onConnect()
14+
}
15+
}) {
16+
HStack(spacing: 16) {
17+
// Device icon
18+
Image(systemName: transportIcon)
19+
.font(.system(size: 24))
20+
.foregroundColor(.white)
21+
.frame(width: 48, height: 48)
22+
.background(Color.white.opacity(0.1))
23+
.clipShape(RoundedRectangle(cornerRadius: 12))
24+
25+
// Device info
26+
VStack(alignment: .leading, spacing: 4) {
27+
Text(displayName)
28+
.font(.system(size: 16, weight: .semibold))
29+
.foregroundColor(.white)
30+
31+
Text(transportLabel)
32+
.font(.system(size: 14))
33+
.foregroundColor(.white.opacity(0.6))
34+
}
35+
36+
Spacer()
37+
38+
// Connect indicator or chevron
39+
if isConnecting {
40+
ProgressView()
41+
.progressViewStyle(CircularProgressViewStyle(tint: .white))
42+
} else {
43+
Text("Connect")
44+
.font(.system(size: 14, weight: .medium))
45+
.foregroundColor(.white)
46+
.padding(.horizontal, 16)
47+
.padding(.vertical, 8)
48+
.background(Color.white.opacity(0.15))
49+
.clipShape(Capsule())
50+
}
51+
}
52+
.padding(16)
53+
.background(Color.white.opacity(0.05))
54+
.clipShape(RoundedRectangle(cornerRadius: 16))
55+
}
56+
.buttonStyle(.plain)
57+
.disabled(isConnecting)
58+
}
59+
60+
private var displayName: String {
61+
if let label = device.label, !label.isEmpty {
62+
return label
63+
}
64+
return modelName
65+
}
66+
67+
private var modelName: String {
68+
if let model = device.model {
69+
return "Trezor \(model)"
70+
}
71+
return "Trezor"
72+
}
73+
74+
private var transportIcon: String {
75+
switch device.transportType {
76+
case .bluetooth:
77+
return "wave.3.right"
78+
case .usb:
79+
return "cable.connector"
80+
}
81+
}
82+
83+
private var transportLabel: String {
84+
switch device.transportType {
85+
case .bluetooth:
86+
return "Bluetooth"
87+
case .usb:
88+
return "USB"
89+
}
90+
}
91+
}
92+
93+
// MARK: - Known Device Row
94+
95+
/// Row displaying a previously connected (known) Trezor device
96+
struct KnownDeviceRow: View {
97+
let device: TrezorKnownDevice
98+
let isConnecting: Bool
99+
let onConnect: () -> Void
100+
let onForget: () -> Void
101+
102+
var body: some View {
103+
HStack(spacing: 16) {
104+
// Tap area for connect
105+
Button(action: {
106+
if !isConnecting {
107+
onConnect()
108+
}
109+
}) {
110+
HStack(spacing: 16) {
111+
// Device icon
112+
Image(systemName: device.transportType == "bluetooth" ? "wave.3.right" : "cable.connector")
113+
.font(.system(size: 24))
114+
.foregroundColor(.white)
115+
.frame(width: 48, height: 48)
116+
.background(Color.white.opacity(0.1))
117+
.clipShape(RoundedRectangle(cornerRadius: 12))
118+
119+
// Device info
120+
VStack(alignment: .leading, spacing: 4) {
121+
Text(device.label ?? device.name)
122+
.font(.system(size: 16, weight: .semibold))
123+
.foregroundColor(.white)
124+
125+
Text(device.lastConnectedAt.relativeDescription)
126+
.font(.system(size: 12))
127+
.foregroundColor(.white.opacity(0.4))
128+
}
129+
130+
Spacer()
131+
132+
if isConnecting {
133+
ProgressView()
134+
.progressViewStyle(CircularProgressViewStyle(tint: .white))
135+
}
136+
}
137+
}
138+
.buttonStyle(.plain)
139+
.disabled(isConnecting)
140+
141+
// Forget button
142+
Button(action: onForget) {
143+
Image(systemName: "trash")
144+
.font(.system(size: 14))
145+
.foregroundColor(.white.opacity(0.4))
146+
.padding(10)
147+
}
148+
.buttonStyle(.plain)
149+
}
150+
.padding(16)
151+
.background(Color.white.opacity(0.05))
152+
.clipShape(RoundedRectangle(cornerRadius: 16))
153+
}
154+
}
155+
156+
// MARK: - Date Helper
157+
158+
extension Date {
159+
private static let relativeDateFormatter: RelativeDateTimeFormatter = {
160+
let formatter = RelativeDateTimeFormatter()
161+
formatter.unitsStyle = .full
162+
return formatter
163+
}()
164+
165+
/// Returns a relative description like "2 minutes ago"
166+
var relativeDescription: String {
167+
Self.relativeDateFormatter.localizedString(for: self, relativeTo: Date())
168+
}
169+
}
170+
171+
// MARK: - Preview
172+
173+
#if DEBUG
174+
struct TrezorDeviceRow_Previews: PreviewProvider {
175+
static var previews: some View {
176+
ZStack {
177+
Color.black.ignoresSafeArea()
178+
179+
VStack(spacing: 16) {
180+
TrezorDeviceRow(
181+
device: TrezorDeviceInfo(
182+
id: "ble:12345",
183+
transportType: .bluetooth,
184+
name: "Trezor Safe 5",
185+
path: "ble:12345",
186+
label: "My Trezor",
187+
model: "Safe 5",
188+
isBootloader: false
189+
),
190+
isConnecting: false,
191+
onConnect: {}
192+
)
193+
194+
TrezorDeviceRow(
195+
device: TrezorDeviceInfo(
196+
id: "usb:001",
197+
transportType: .usb,
198+
name: "Trezor Model T",
199+
path: "usb:001",
200+
label: nil,
201+
model: "Model T",
202+
isBootloader: false
203+
),
204+
isConnecting: true,
205+
onConnect: {}
206+
)
207+
}
208+
.padding()
209+
}
210+
}
211+
}
212+
#endif
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import SwiftUI
2+
3+
/// Reusable expandable section for the Trezor dashboard.
4+
/// Provides a tappable header with animated expand/collapse of content.
5+
struct TrezorExpandableSection<Content: View>: View {
6+
let title: String
7+
let icon: String
8+
let description: String
9+
@Binding var isExpanded: Bool
10+
@ViewBuilder let content: () -> Content
11+
12+
var body: some View {
13+
VStack(spacing: 0) {
14+
// Tappable header
15+
Button(action: {
16+
withAnimation(.easeInOut(duration: 0.25)) {
17+
isExpanded.toggle()
18+
}
19+
}) {
20+
HStack(spacing: 16) {
21+
Image(systemName: icon)
22+
.font(.system(size: 20))
23+
.foregroundColor(.white)
24+
.frame(width: 40, height: 40)
25+
.background(Color.white.opacity(0.1))
26+
.clipShape(Circle())
27+
28+
VStack(alignment: .leading, spacing: 2) {
29+
Text(title)
30+
.font(.system(size: 16, weight: .semibold))
31+
.foregroundColor(.white)
32+
33+
Text(description)
34+
.font(.system(size: 12))
35+
.foregroundColor(.white.opacity(0.6))
36+
}
37+
38+
Spacer()
39+
40+
Image(systemName: "chevron.down")
41+
.font(.system(size: 14))
42+
.foregroundColor(.white.opacity(0.4))
43+
.rotationEffect(.degrees(isExpanded ? 0 : -90))
44+
.animation(.easeInOut(duration: 0.25), value: isExpanded)
45+
}
46+
}
47+
48+
// Expandable content
49+
if isExpanded {
50+
Divider()
51+
.background(Color.white.opacity(0.1))
52+
.padding(.top, 12)
53+
54+
content()
55+
.padding(.top, 12)
56+
.transition(.opacity)
57+
}
58+
}
59+
.padding(16)
60+
.background(Color.white.opacity(0.05))
61+
.clipShape(RoundedRectangle(cornerRadius: 12))
62+
}
63+
}

0 commit comments

Comments
 (0)