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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ buildServer.json

# AIs
.ai/
.codex/
.claude/*.local*
2 changes: 1 addition & 1 deletion Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,7 @@
repositoryURL = "https://github.com/pubky/paykit-rs";
requirement = {
kind = exactVersion;
version = "0.1.0-rc5";
version = "0.1.0-rc8";
};
};
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.

15 changes: 15 additions & 0 deletions Bitkit/Assets.xcassets/icons/user-minus.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "user-minus.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions Bitkit/Components/PubkyContactAvatar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import SwiftUI

struct PubkyContactAvatar: View {
let name: String
let imageUrl: String?
let size: CGFloat

init(name: String, imageUrl: String?, size: CGFloat) {
self.name = name
self.imageUrl = imageUrl
self.size = size
}

init(contact: PubkyContact, size: CGFloat) {
name = contact.displayName
imageUrl = contact.profile.imageUrl
self.size = size
}

var body: some View {
Group {
if let imageUrl {
PubkyImage(uri: imageUrl, size: size)
} else {
ContactAvatarLetter(source: name, size: size)
}
}
.accessibilityHidden(true)
}
}
42 changes: 42 additions & 0 deletions Bitkit/Components/PubkyContactRow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import SwiftUI

struct PubkyContactRow: View {
let contact: PubkyContact
var verticalPadding: CGFloat = 12
var showsDivider = true
var isLoading = false
let action: () -> Void

var body: some View {
VStack(spacing: 0) {
Button(action: action) {
HStack(spacing: 16) {
PubkyContactAvatar(contact: contact, size: 48)

VStack(alignment: .leading, spacing: 4) {
CaptionText(contact.profile.truncatedPublicKey.localizedUppercase)
.lineLimit(1)

BodyMSBText(contact.displayName)
.lineLimit(1)
}

Spacer()

if isLoading {
ProgressView()
}
}
.padding(.vertical, verticalPadding)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.disabled(isLoading)
.accessibilityLabel(contact.displayName)

if showsDivider {
CustomDivider()
}
}
}
}
2 changes: 2 additions & 0 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ struct MainNavView: View {
case .contactsIntro: ContactsIntroView()
case let .contactDetail(publicKey): ContactDetailView(publicKey: publicKey)
case let .contactActivity(publicKey): ContactActivityView(publicKey: publicKey)
case let .assignActivityContact(activityId): AssignActivityContactView(activityId: activityId)
case .contactImportOverview:
if let fallbackRoute = fallbackRouteForMissingPendingImport(hasPendingImport: contactsManager.hasPendingImport) {
missingPendingImportView(fallbackRoute: fallbackRoute)
Expand Down Expand Up @@ -450,6 +451,7 @@ struct MainNavView: View {
case .widgetsSettings: WidgetsSettingsScreen()
case .notifications: NotificationsSettings()
case .notificationsIntro: NotificationsIntro()
case .paymentPreference: PaymentPreferenceView()

// Security settings
case .changePin: ChangePinScreen()
Expand Down
25 changes: 23 additions & 2 deletions Bitkit/Managers/PubkyProfileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,10 @@ class PubkyProfileManager: ObservableObject {
}

do {
try? Keychain.delete(key: .pubkySecretKey)
try Self.upsertKeychainString(.paykitSession, value: sessionSecret)
UserDefaults.standard.set(false, forKey: PrivatePaykitService.publishingEnabledKey)
PrivatePaykitService.setProfileRecoveryPending(false)
Self.notifyAppStateBackupChanged()
} catch {
await PubkyService.forceSignOut()
Expand Down Expand Up @@ -753,7 +756,7 @@ class PubkyProfileManager: ObservableObject {
// MARK: - Sign Out

static func clearLocalState() async {
await PrivatePaykitService.shared.closeAndClear()
await PrivatePaykitService.shared.closeAndClear(markProfileRecoveryPending: true)
await PrivatePaykitAddressReservationStore.shared.clearContactAssignments()
await PubkyService.forceSignOut()
try? Keychain.delete(key: .paykitSession)
Expand All @@ -766,7 +769,8 @@ class PubkyProfileManager: ObservableObject {
}

private static func clearPublicPaykitSharingState() {
UserDefaults.standard.set(false, forKey: "sharesPublicPaykitEndpoints")
UserDefaults.standard.set(false, forKey: PublicPaykitService.publishingEnabledKey)
UserDefaults.standard.set(false, forKey: PrivatePaykitService.publishingEnabledKey)
UserDefaults.standard.set(false, forKey: "hasConfirmedPublicPaykitEndpoints")
PrivatePaykitService.setContactSharingCleanupPending(false)
UserDefaults.standard.removeObject(forKey: "publicPaykitBolt11")
Expand Down Expand Up @@ -870,6 +874,23 @@ class PubkyProfileManager: ObservableObject {
publicKey != nil
}

var hasLocalSecretKeyForCurrentProfile: Bool {
Self.hasLocalSecretKey(for: publicKey)
}

nonisolated static func hasLocalSecretKey(for publicKey: String?) -> Bool {
guard let publicKey,
let secretKeyHex = try? Keychain.loadString(key: .pubkySecretKey),
!secretKeyHex.isEmpty,
let rawPublicKey = try? PubkyService.pubkyPublicKeyFromSecret(secretKeyHex: secretKeyHex)
else {
return false
}

let prefixedPublicKey = rawPublicKey.hasPrefix("pubky") ? rawPublicKey : "pubky\(rawPublicKey)"
return PubkyPublicKeyFormat.matches(prefixedPublicKey, publicKey)
}

nonisolated static func snapshotSessionBackupState(
loadKeychainString: (KeychainEntryType) throws -> String? = {
try Keychain.loadString(key: $0)
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Models/BackupPayloads.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct PrivatePaykitContactLinkBackupV1: Codable, Equatable {
let recoveryStartedAt: UInt64?
let mainRecoveryAttemptId: String?
let responderRecoveryAttemptId: String?
var awaitingRecoveredRemoteEndpoints: Bool? = nil
}

struct MetadataBackupV1: Codable {
Expand Down
12 changes: 11 additions & 1 deletion Bitkit/Resources/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@
"settings__adv__pp_header" = "Choose how you prefer to receive money when users send funds to your profile key.";
"settings__adv__pp_footer" = "* This requires sharing payment data.";
"settings__adv__pp_drag" = "Payment preference (drag to reorder)";
"settings__adv__pp_contacts" = "Pay to/from contacts";
"settings__adv__pp_contacts" = "Payments from contacts";
"settings__adv__pp_contacts_switch" = "Enable payments with contacts*";
"settings__adv__address_viewer" = "Address Viewer";
"settings__adv__sweep_funds" = "Sweep Funds";
Expand Down Expand Up @@ -1416,3 +1416,13 @@
"widgets__weather__current_fee" = "Current average fee";
"widgets__weather__next_block" = "Next block inclusion";
"widgets__weather__error" = "Couldn\'t get current fee weather";

"settings__adv__pp_options" = "Payment options";
"settings__adv__pp_lightning" = "Lightning (Bitkit)";
"settings__adv__pp_onchain" = "On-chain (Bitkit)";
"settings__adv__pp_private_contacts" = "Private payments with contacts";
"settings__adv__pp_public_contacts" = "Public payments with contacts*";
"settings__adv__pp_public_footer" = "*Public payments with contacts requires payment data to be shared publicly.";
"settings__adv__pp_both" = "Both";
"settings__adv__pp_lightning_short" = "Lightning";
"settings__adv__pp_onchain_short" = "On-chain";
6 changes: 5 additions & 1 deletion Bitkit/Services/PrivatePaykitService+Backup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ extension PrivatePaykitService {
handshakeUpdatedAt: contactState.handshakeUpdatedAt,
recoveryStartedAt: contactState.recoveryStartedAt,
mainRecoveryAttemptId: contactState.mainRecoveryAttemptId,
responderRecoveryAttemptId: contactState.responderRecoveryAttemptId
responderRecoveryAttemptId: contactState.responderRecoveryAttemptId,
awaitingRecoveredRemoteEndpoints: contactState.awaitingRecoveredRemoteEndpoints ? true : nil
)
)
}
Expand All @@ -41,6 +42,7 @@ extension PrivatePaykitService {
guard let backup else {
state = PrivatePaykitState(contacts: [:])
persistState()
Self.setProfileRecoveryPending(false)
return
}

Expand All @@ -67,11 +69,13 @@ extension PrivatePaykitService {
contactState.recoveryStartedAt = contactBackup.recoveryStartedAt
contactState.mainRecoveryAttemptId = contactBackup.mainRecoveryAttemptId
contactState.responderRecoveryAttemptId = contactBackup.responderRecoveryAttemptId
contactState.awaitingRecoveredRemoteEndpoints = contactBackup.awaitingRecoveredRemoteEndpoints == true
restoredContacts[normalizedKey] = contactState
}

state = PrivatePaykitState(contacts: restoredContacts)
persistState()
Self.setProfileRecoveryPending(false)
}

func validatedSnapshot(
Expand Down
Loading
Loading