Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
65f9c34
feat: add profile fetching from pubky
ben-kaufman Mar 5, 2026
851c0e0
add core bluetooth framework
ben-kaufman Mar 5, 2026
dd4475d
Fix claude issues
ben-kaufman Mar 11, 2026
4800529
fix comments
ben-kaufman Mar 11, 2026
66134e1
fix
ben-kaufman Mar 11, 2026
b7a9f66
fixes
ben-kaufman Mar 11, 2026
8a27df2
fix
ben-kaufman Mar 11, 2026
00ebf39
fixes
ben-kaufman Mar 11, 2026
813304d
fixes
ben-kaufman Mar 11, 2026
d218db8
fix comments
ben-kaufman Mar 11, 2026
6c1ad2e
fixes
ben-kaufman Mar 11, 2026
1993bc1
refactor: migrate from Paykit to BitkitCore for pubky profile
ben-kaufman Mar 11, 2026
328c9e0
fix comment
ben-kaufman Mar 11, 2026
41073fd
feat: add pubky contacts list and detail views
ben-kaufman Mar 11, 2026
0c59b1e
fixes
ben-kaufman Mar 12, 2026
3d9021b
fixes
ben-kaufman Mar 16, 2026
4932cbf
fixes
ben-kaufman Mar 16, 2026
a0f0de0
fixes
ben-kaufman Mar 16, 2026
b15a5cb
Update ProfileView.swift
ben-kaufman Mar 16, 2026
578f848
fixes
ben-kaufman Mar 17, 2026
71f063f
Add Pubky profile, contacts, and Ring auth import flow
ben-kaufman Mar 31, 2026
3ae9409
chore: bump bitkit-core to v0.1.53
ben-kaufman Mar 31, 2026
8374d94
fix: keep loading spinner visible during post-Ring-auth import
ben-kaufman Mar 31, 2026
2116547
fix: update loading text after Ring approval while fetching data
ben-kaufman Mar 31, 2026
2669a65
fix: pin bitkit-core to v0.1.52 (v0.1.53 tag has broken binary URL)
ben-kaufman Mar 31, 2026
af4bc5c
chore: bump bitkit-core to v0.1.54
ben-kaufman Mar 31, 2026
a44b1ed
fix: adapt PubkyAuthRequest to bitkit-core v0.1.54 API change
ben-kaufman Mar 31, 2026
7394730
merge: resolve conflicts with master
ben-kaufman Apr 3, 2026
6814a25
feat: add delete buttons to edit views, fix contacts empty state
ben-kaufman Apr 3, 2026
7406468
fix: prevent race condition navigating to profile before session rest…
ben-kaufman Apr 5, 2026
82649bb
fix: address review findings for contacts and import flows
ben-kaufman Apr 5, 2026
85da48a
fix: clear cached profile on restoration failure, fix duplicate l10n key
ben-kaufman Apr 5, 2026
f4de5a4
fix: clear invalid session only when re-sign-in confirms it, add chan…
ben-kaufman Apr 5, 2026
d4fb276
fix: don't cancel task by mutating its id during execution
ben-kaufman Apr 5, 2026
1744ceb
fix: address review findings for session init, avatar restore, and co…
ben-kaufman Apr 6, 2026
d7861fc
fix: rename delete profile to disconnect with correct copy
ben-kaufman Apr 6, 2026
e0d3e4b
fix: prevent duplicate contacts and throw on avatar encoding failure
ben-kaufman Apr 6, 2026
b6c6c99
fix: address pubky review issues
ben-kaufman Apr 7, 2026
1479d38
fix: harden pubky contact flows
ben-kaufman Apr 9, 2026
ff1eeeb
fix: select homegate by environment
ben-kaufman Apr 9, 2026
cac3962
Merge origin/master into feat/pubky-profile
ben-kaufman Apr 9, 2026
cf8ca66
fix: align pubky branch with 2.2.0 release
ben-kaufman Apr 9, 2026
ad85aac
Merge branch 'master' into feat/pubky-profile
ben-kaufman Apr 9, 2026
8bf476f
fix: simplify contacts empty state
ben-kaufman Apr 9, 2026
0e98ff9
fix: prefer bitkit profiles when adding contacts
ben-kaufman Apr 9, 2026
0a495b5
chore: update Package.resolved (auto by xcode)
ovitrif Apr 9, 2026
f125aeb
Merge branch 'master' into feat/pubky-profile
ovitrif Apr 9, 2026
1c655c2
Merge branch 'master' into feat/pubky-profile
ovitrif Apr 9, 2026
842f962
fix: rename profile bio copy to notes
ben-kaufman Apr 9, 2026
9c3884b
fix: address pubky profile review feedback
ben-kaufman Apr 10, 2026
bf322d3
fix: clear pubky data on app wipe
ben-kaufman Apr 10, 2026
e848e9d
fix: add pubky key validation, contact UX improvements, and avatar ed…
ben-kaufman Apr 10, 2026
04d06fc
fix: delete all contacts from homeserver when deleting profile
ben-kaufman Apr 10, 2026
603f2be
fix: tolerate sparse pubky contact profiles
ben-kaufman Apr 10, 2026
0138f74
fix: simplify contacts list sections
ben-kaufman Apr 10, 2026
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ This project follows **modern SwiftUI patterns** and explicitly **AVOIDS traditi
- Async operations should delegate to `@Observable` business logic objects

4. **Component Design**
- **Always reuse existing components** before creating new ones — check `Components/` for buttons, text styles, layouts, and other shared UI. If identical or near-identical UI exists elsewhere in the codebase, extract it into a shared component rather than duplicating it.
- Use the project's text components (`DisplayText`, `HeadlineText`, `TitleText`, `SubtitleText`, `BodyMText`, `BodyMSBText`, `BodySSBText`, `BodySText`, `CaptionMText`, `CaptionText`) instead of raw `Text().font().foregroundColor()` chains.
- Decompose views into small, focused, single-purpose components
- Use descriptive names (e.g., `UserProfileCard` not `Card`)
- Prefer composition over deep view hierarchies
Expand Down
17 changes: 17 additions & 0 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
182817C12F59A7F10055A441 /* Paykit in Frameworks */ = {isa = PBXBuildFile; productRef = 182817C02F59A7F10055A441 /* Paykit */; };
18D65E002EB964B500252335 /* VssRustClientFfi in Frameworks */ = {isa = PBXBuildFile; productRef = 18D65DFF2EB964B500252335 /* VssRustClientFfi */; };
18D65E022EB964BD00252335 /* VssRustClientFfi in Frameworks */ = {isa = PBXBuildFile; productRef = 18D65E012EB964BD00252335 /* VssRustClientFfi */; };
3D76260F4C9C4A53B1E4A001 /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D76260E4C9C4A53B1E4A001 /* CoreBluetooth.framework */; };
Expand Down Expand Up @@ -167,6 +168,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
182817C12F59A7F10055A441 /* Paykit in Frameworks */,
3D76260F4C9C4A53B1E4A001 /* CoreBluetooth.framework in Frameworks */,
4AFCA3702E05933800205CAE /* Zip in Frameworks */,
968FDF162DFAFE230053CD7F /* LDKNode in Frameworks */,
Expand Down Expand Up @@ -281,6 +283,7 @@
4AFCA36F2E05933800205CAE /* Zip */,
4AAB08C92E1FE77600BA63DF /* Lottie */,
18D65DFF2EB964B500252335 /* VssRustClientFfi */,
182817C02F59A7F10055A441 /* Paykit */,
);
productName = Bitkit;
productReference = 96FE1F612C2DE6AA006D0C8B /* Bitkit.app */;
Expand Down Expand Up @@ -388,6 +391,7 @@
968FE13E2DFB016B0053CD7F /* XCRemoteSwiftPackageReference "ldk-node" */,
4AAB08C82E1FE77600BA63DF /* XCRemoteSwiftPackageReference "lottie-ios" */,
18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */,
182817BF2F59A7F10055A441 /* XCRemoteSwiftPackageReference "paykit-rs" */,
);
productRefGroup = 96FE1F622C2DE6AA006D0C8B /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -903,6 +907,14 @@
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
182817BF2F59A7F10055A441 /* XCRemoteSwiftPackageReference "paykit-rs" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/pubky/paykit-rs";
requirement = {
kind = revision;
revision = cd1253291b1582759d569372d5942b8871527ea1;
};
};
18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/synonymdev/vss-rust-client-ffi";
Expand Down Expand Up @@ -962,6 +974,11 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
182817C02F59A7F10055A441 /* Paykit */ = {
isa = XCSwiftPackageProductDependency;
package = 182817BF2F59A7F10055A441 /* XCRemoteSwiftPackageReference "paykit-rs" */;
productName = Paykit;
};
18D65DFF2EB964B500252335 /* VssRustClientFfi */ = {
isa = XCSwiftPackageProductDependency;
package = 18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */;
Expand Down

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

2 changes: 1 addition & 1 deletion Bitkit.xcodeproj/xcshareddata/xcschemes/Bitkit.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
Expand Down
37 changes: 34 additions & 3 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ struct AppScene: View {
@StateObject private var transferTracking: TransferTrackingManager
@StateObject private var channelDetails = ChannelDetailsViewModel.shared
@StateObject private var migrations = MigrationsService.shared
@StateObject private var pubkyProfile = PubkyProfileManager()
@StateObject private var contactsManager = ContactsManager()
@State private var keyboardManager = KeyboardManager()

@State private var hideSplash = false
Expand Down Expand Up @@ -135,7 +137,29 @@ struct AppScene: View {
.environmentObject(tagManager)
.environmentObject(transferTracking)
.environmentObject(channelDetails)
.environmentObject(pubkyProfile)
.environmentObject(contactsManager)
.environment(keyboardManager)
.onChange(of: pubkyProfile.authState, initial: true) { _, authState in
if authState == .authenticated, let pk = pubkyProfile.publicKey {
Task { try? await contactsManager.loadContacts(for: pk) }
} else if authState == .idle {
contactsManager.reset()
}
}
.onChange(of: navigation.currentRoute) { oldRoute, newRoute in
guard shouldDiscardPendingImport(currentRoute: oldRoute, destination: newRoute) else {
return
}

contactsManager.clearPendingImport()
}
.onChange(of: pubkyProfile.sessionRestorationFailed) { _, failed in
if failed {
pubkyProfile.sessionRestorationFailed = false
app.toast(type: .error, title: t("profile__session_expired_title"), description: t("profile__session_expired_description"))
}
}
.onAppear {
if !settings.pinEnabled {
isPinVerified = true
Expand Down Expand Up @@ -240,9 +264,12 @@ struct AppScene: View {
WalletRestoreSuccess()
} else {
if !isPinVerified && settings.pinEnabled {
AuthCheck {
isPinVerified = true
}
AuthCheck(
onCancel: nil,
onPinVerified: {
isPinVerified = true
}
)
} else {
MainNavView()
}
Expand Down Expand Up @@ -397,6 +424,10 @@ struct AppScene: View {
// Handle orphaned keychain before anything else
handleOrphanedKeychain()

// Start Pubky/Paykit initialization after keychain cleanup so
// session restoration never races orphaned-keychain wiping.
Task { await pubkyProfile.initialize() }

await checkAndPerformRNMigration()
try wallet.setWalletExistsState()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "contact-card.png",
"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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "ellipse-inner-green.png",
"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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "ellipse-outer-green.png",
"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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "pubky-ring-logo.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "pubky-ring-logo@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "pubky-ring-logo@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "tag-pubky.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "tag-pubky@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "tag-pubky@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 13 additions & 7 deletions Bitkit/Components/Button/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ struct CustomButtonStyle: ButtonStyle {
}

struct CustomButton: View {
@Environment(\.isEnabled) private var isEnabled

enum Size {
case small
case large
Expand Down Expand Up @@ -70,6 +72,10 @@ struct CustomButton: View {

@State private var isPressed = false

private var effectiveIsDisabled: Bool {
isDisabled || !isEnabled
}

/// Base initializer for optional action
init(
title: String,
Expand Down Expand Up @@ -148,7 +154,7 @@ struct CustomButton: View {
title: title,
size: size,
icon: icon,
isDisabled: isDisabled,
isDisabled: effectiveIsDisabled,
isLoading: isLoading,
isPressed: isPressed,
shouldExpand: shouldExpand,
Expand All @@ -159,7 +165,7 @@ struct CustomButton: View {
title: title,
size: size,
icon: icon,
isDisabled: isDisabled,
isDisabled: effectiveIsDisabled,
isPressed: isPressed
))
case .tertiary:
Expand All @@ -185,17 +191,17 @@ struct CustomButton: View {
isPressed: $isPressed
)
)
.disabled(isDisabled)
.disabled(effectiveIsDisabled)
.simultaneousGesture(
TapGesture().onEnded {
if !isDisabled {
if !effectiveIsDisabled {
Haptics.play(.buttonTap)
}
}
)
} else if let action {
Button {
guard !isLoading, !isDisabled else { return }
guard !isLoading, !effectiveIsDisabled else { return }

Haptics.play(.buttonTap)

Expand All @@ -213,10 +219,10 @@ struct CustomButton: View {
isPressed: $isPressed
)
)
.disabled(isDisabled || isLoading)
.disabled(effectiveIsDisabled || isLoading)
} else {
buttonVariantView
.opacity(isDisabled ? 0.5 : 1)
.opacity(effectiveIsDisabled ? 0.5 : 1)
}
}
}
Expand Down
57 changes: 57 additions & 0 deletions Bitkit/Components/Button/GradientCircleButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import SwiftUI

/// A circular button with a gradient background, used for action icons (copy, share, edit, delete).
struct GradientCircleButton: View {
let icon: String?
let systemIcon: String?
let accessibilityLabel: String
let action: () -> Void

init(icon: String, accessibilityLabel: String, action: @escaping () -> Void) {
self.icon = icon
systemIcon = nil
self.accessibilityLabel = accessibilityLabel
self.action = action
}

init(systemIcon: String, accessibilityLabel: String, action: @escaping () -> Void) {
icon = nil
self.systemIcon = systemIcon
self.accessibilityLabel = accessibilityLabel
self.action = action
}

var body: some View {
Button(action: action) {
ZStack {
Circle()
.fill(
LinearGradient(
colors: [.gray5, .gray6],
startPoint: .top,
endPoint: .bottom
)
)
.overlay(
Circle()
.stroke(Color.white10, lineWidth: 1)
.padding(0.5)
)

if let icon {
Image(icon)
.resizable()
.scaledToFit()
.foregroundColor(.textPrimary)
.frame(width: 24, height: 24)
} else if let systemIcon {
Image(systemName: systemIcon)
.font(.system(size: 18, weight: .medium))
.foregroundColor(.textPrimary)
}
}
.frame(width: 48, height: 48)
}
.accessibilityLabel(accessibilityLabel)
}
}
Loading
Loading