Skip to content
Merged
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
16 changes: 11 additions & 5 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
Services/GeoService.swift,
Services/LightningService.swift,
Services/MigrationsService.swift,
Services/WatchOnlyAccountService.swift,
Services/RNBackupClient.swift,
Services/ServiceQueue.swift,
Services/TransferStorage.swift,
Expand All @@ -182,6 +183,7 @@
Utilities/Errors.swift,
Utilities/Keychain.swift,
Utilities/LightningAmountConversion.swift,
Utilities/LocalizeHelpers.swift,
Utilities/Logger.swift,
Utilities/StateLocker.swift,
);
Expand All @@ -201,12 +203,14 @@
Services/CoreService.swift,
Services/GeoService.swift,
Services/LightningService.swift,
Services/WatchOnlyAccountService.swift,
Services/ServiceQueue.swift,
Services/VssStoreIdProvider.swift,
Utilities/Crypto.swift,
Utilities/Errors.swift,
Utilities/Keychain.swift,
Utilities/LightningAmountConversion.swift,
Utilities/LocalizeHelpers.swift,
Utilities/Logger.swift,
Utilities/StateLocker.swift,
);
Expand Down Expand Up @@ -570,8 +574,8 @@
inputFileListPaths = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/Frameworks/LDKNodeFFI.framework/_CodeSignature/CodeResources",
"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/Frameworks/vss_rust_client_ffiFFI.framework/_CodeSignature/CodeResources",
"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/Frameworks/LDKNodeFFI.framework/_CodeSignature",
"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/Frameworks/vss_rust_client_ffiFFI.framework/_CodeSignature",
);
name = "Remove Static Framework Stubs";
outputFileListPaths = (
Expand Down Expand Up @@ -765,6 +769,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) BITKIT_NOTIFICATION_EXTENSION";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
Expand Down Expand Up @@ -797,6 +802,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) BITKIT_NOTIFICATION_EXTENSION";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
Expand Down Expand Up @@ -1169,7 +1175,7 @@
repositoryURL = "https://github.com/pubky/paykit-rs";
requirement = {
kind = exactVersion;
version = "0.1.0-rc33";
version = "0.1.0-rc37";
};
};
18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */ = {
Expand All @@ -1193,15 +1199,15 @@
repositoryURL = "https://github.com/synonymdev/ldk-node";
requirement = {
kind = exactVersion;
version = "0.7.0-rc.53";
version = "0.7.0-rc.57";
};
};
96DEA0382DE8BBA1009932BF /* XCRemoteSwiftPackageReference "bitkit-core" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/synonymdev/bitkit-core";
requirement = {
kind = exactVersion;
version = 0.4.1;
version = 0.4.2;
};
};
96E20CD22CB6D91A00C24149 /* XCRemoteSwiftPackageReference "CodeScanner" */ = {
Expand Down

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

19 changes: 15 additions & 4 deletions Bitkit/Components/SettingsRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum SettingsRowRightIcon {

struct SettingsRow: View {
let title: String
let subtitle: String?
let iconName: String?
let iconColor: Color?
let rightText: String?
Expand All @@ -33,6 +34,7 @@ struct SettingsRow: View {

init(
title: String,
subtitle: String? = nil,
iconName: String? = nil,
iconColor: Color? = .brandAccent,
rightText: String? = nil,
Expand All @@ -42,6 +44,7 @@ struct SettingsRow: View {
testIdentifier: String? = nil
) {
self.title = title
self.subtitle = subtitle
self.iconName = iconName
self.iconColor = iconColor
self.rightText = rightText
Expand All @@ -59,9 +62,17 @@ struct SettingsRow: View {
.padding(.trailing, 8)
}

BodyMText(title, textColor: .textPrimary)
.lineLimit(1)
.truncationMode(.tail)
VStack(alignment: .leading, spacing: 4) {
BodyMText(title, textColor: .textPrimary)
.lineLimit(1)
.truncationMode(.tail)

if let subtitle {
BodySText(subtitle, textColor: .textSecondary)
.lineLimit(1)
.truncationMode(.middle)
}
}

Spacer()

Expand Down Expand Up @@ -98,7 +109,7 @@ struct SettingsRow: View {
}
}
}
.frame(height: 50)
.frame(minHeight: subtitle == nil ? 50 : 66)

CustomDivider()
}
Expand Down
5 changes: 4 additions & 1 deletion Bitkit/Components/SheetIntro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct SheetIntro: View {
let accentColor: Color
let accentFont: ((CGFloat) -> Font)?
let testID: String?
let cancelTestID: String?
let continueTestID: String?
let onCancel: (() -> Void)?
let onContinue: () -> Void
Expand All @@ -27,6 +28,7 @@ struct SheetIntro: View {
accentColor: Color = .brandAccent,
accentFont: ((CGFloat) -> Font)? = nil,
testID: String? = nil,
cancelTestID: String? = nil,
continueTestID: String? = nil,
onCancel: (() -> Void)? = nil,
onContinue: @escaping () -> Void
Expand All @@ -40,6 +42,7 @@ struct SheetIntro: View {
self.accentColor = accentColor
self.accentFont = accentFont
self.testID = testID
self.cancelTestID = cancelTestID
self.continueTestID = continueTestID
self.onCancel = onCancel
self.onContinue = onContinue
Expand Down Expand Up @@ -88,7 +91,7 @@ struct SheetIntro: View {
CustomButton(title: cancelText, variant: .secondary) {
onCancel()
}
.accessibilityIdentifier("\(baseTestID)Cancel")
.accessibilityIdentifier(cancelTestID ?? "\(baseTestID)Cancel")

CustomButton(title: continueText) {
onContinue()
Expand Down
3 changes: 2 additions & 1 deletion Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ struct MainNavView: View {
case .electrumSettings: ElectrumSettingsScreen()
case .rgsSettings: RgsSettingsScreen()
case .addressViewer: AddressViewer()
case .watchOnlyAccounts: WatchOnlyAccountsView()
case .devSettings: DevSettingsView()

// Dev settings
Expand Down Expand Up @@ -665,7 +666,7 @@ struct MainNavView: View {
}

private func shouldOpenPaymentSheet(for uri: String) -> Bool {
!SamRockSetupRequest.isProtocolURL(uri)
!SamRockSetupRequest.isProtocolURL(uri) && !PubkyAuthRequest.isProtocolURL(uri)
}

private func sanitizedDeeplinkDescription(_ url: URL) -> String {
Expand Down
12 changes: 11 additions & 1 deletion Bitkit/Managers/ScannerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ class ScannerManager: ObservableObject {

Haptics.play(.scanSuccess)
Comment thread
jvsena42 marked this conversation as resolved.

guard !PubkyAuthRequest.isProtocolURL(uri) else {
app.toast(
type: .error,
title: t("other__qr_error_header"),
description: t("other__qr_error_text")
)
completion(nil)
return
}

do {
if handlePubkyRouteIfNeeded(uri, hiding: .send, reason: "Send scanner routed pubky key") {
completion(nil)
Expand Down Expand Up @@ -162,7 +172,7 @@ class ScannerManager: ObservableObject {
}

private func shouldOpenPaymentFlow(for uri: String) -> Bool {
!SamRockSetupRequest.isProtocolURL(uri)
!SamRockSetupRequest.isProtocolURL(uri) && !PubkyAuthRequest.isProtocolURL(uri)
}

private func handleElectrumScan(_ uri: String) async {
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Models/BackupPayloads.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ struct WalletBackupV1: Codable {
let transfers: [Transfer]
let privatePaykitHighestReservedReceiveIndexByAddressType: [String: UInt32]?
let paykitSdkBackupState: String?
let watchOnlyAccounts: [WatchOnlyAccountRecord]?
let watchOnlyAccountAllocationState: WatchOnlyAccountAllocationState?
}

struct MetadataBackupV1: Codable {
Expand Down
56 changes: 55 additions & 1 deletion Bitkit/Models/PubkyAuthRequest.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import Foundation
import Paykit

enum PubkyAuthClaim: String, Equatable {
case watchOnlyAccountV1 = "watch-only-account-v1"

static let queryParameter = "x-bitkit-claim"
static let watchOnlyAccountCapabilities = "/pub/paykit/v0/bitkit/server/:rw"
}

enum PubkyAuthRequestError: Error, Equatable {
case invalidUrl
case missingBitkitClaim
case duplicateBitkitClaim
case unsupportedBitkitClaim(String)
case invalidBitkitClaimCapabilities
}

// MARK: - PubkyAuth Permission

struct PubkyAuthPermission {
let path: String
let accessLevel: String

var displayPath: String {
path.count > 1 && path.hasSuffix("/") ? String(path.dropLast()) : path
}

var displayAccess: String {
var levels: [String] = []
if accessLevel.contains("r") { levels.append("READ") }
Expand All @@ -24,22 +43,57 @@ struct PubkyAuthRequest {
let capabilities: String
let permissions: [PubkyAuthPermission]
let serviceNames: [String]
let bitkitClaim: PubkyAuthClaim?

static func isProtocolURL(_ value: String) -> Bool {
URLComponents(string: value.trimmingCharacters(in: .whitespacesAndNewlines))?.scheme?.lowercased() == "pubkyauth"
}

static func parse(url: String) throws -> PubkyAuthRequest {
let details = try Paykit.parsePubkyAuthUrl(authUrl: url)
let capabilities = details.capabilities ?? ""
let permissions = parseCapabilities(capabilities)
let serviceNames = permissions.compactMap { extractServiceName($0.path) }
let bitkitClaim = try parseBitkitClaim(url: url, capabilities: capabilities)
return PubkyAuthRequest(
rawUrl: url,
kind: details.kind,
relay: details.relayUrl ?? "",
capabilities: capabilities,
permissions: permissions,
serviceNames: serviceNames
serviceNames: serviceNames,
bitkitClaim: bitkitClaim
)
}

static func parseBitkitClaim(url: String, capabilities: String) throws -> PubkyAuthClaim? {
guard let components = URLComponents(string: url) else {
throw PubkyAuthRequestError.invalidUrl
}

let claimValues = components.queryItems?
.filter { $0.name == PubkyAuthClaim.queryParameter }
.map { $0.value ?? "" } ?? []

guard claimValues.count <= 1 else {
throw PubkyAuthRequestError.duplicateBitkitClaim
}
guard let claimValue = claimValues.first else {
if capabilities == PubkyAuthClaim.watchOnlyAccountCapabilities {
throw PubkyAuthRequestError.missingBitkitClaim
}
return nil
}
guard let claim = PubkyAuthClaim(rawValue: claimValue) else {
throw PubkyAuthRequestError.unsupportedBitkitClaim(claimValue)
}
guard capabilities == PubkyAuthClaim.watchOnlyAccountCapabilities else {
throw PubkyAuthRequestError.invalidBitkitClaimCapabilities
}

return claim
}

static func parseCapabilities(_ caps: String) -> [PubkyAuthPermission] {
caps
.split(separator: ",")
Expand Down
27 changes: 27 additions & 0 deletions Bitkit/Resources/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,14 @@
"pubky_auth__description_prefix" = "A service is requesting permission to access and edit your ";
"pubky_auth__description_suffix" = " data.";
"pubky_auth__requested_permissions" = "REQUESTED PERMISSIONS";
"pubky_auth__watch_only_account_default_name" = "{service} account";
"pubky_auth__watch_only_account_fallback_name" = "Paykit server account";
"pubky_auth__watch_only_account_name_error" = "Enter an account name between 1 and 64 characters.";
"pubky_auth__watch_only_intro_approve" = "Approve";
"pubky_auth__watch_only_intro_description" = "To earn, you need to share a watch-only Bitcoin account with Paykit. It can view sales activity, but cannot spend funds.";
"pubky_auth__watch_only_intro_nav_title" = "Earn";
"pubky_auth__watch_only_intro_title" = "<accent>EARN BITCOIN</accent>\nFROM YOUR\nCONTENT";
"pubky_auth__watch_only_account_xpub_error" = "Bitkit could not create a valid account xpub.";
"pubky_auth__trust_warning" = "Make sure you trust the service, browser, or device before authorizing with your pubky.";
"pubky_auth__authorizing" = "Authorizing...";
"pubky_auth__success_title" = "Authorization Successful";
Expand All @@ -677,6 +685,25 @@
"pubky_auth__use_ring_desc" = "Your identity was created with Pubky Ring. Open Ring to approve this request.";
"pubky_auth__invalid_request" = "Invalid auth request";
"pubky_auth__approval_failed" = "Authorization Failed";
"watch_only_accounts__active_section" = "Active accounts";
"watch_only_accounts__copy_xpub" = "Copy xpub";
"watch_only_accounts__description" = "Each approved Paykit server gets a separate Bitcoin account. Turn tracking off to unload an account without deleting its wallet history.";
"watch_only_accounts__details_title" = "Account details";
"watch_only_accounts__empty_description" = "Accounts appear here after you approve a Paykit server setup request.";
"watch_only_accounts__empty_title" = "No server accounts";
"watch_only_accounts__name" = "ACCOUNT NAME";
"watch_only_accounts__name_placeholder" = "Account name";
"watch_only_accounts__name_saved" = "Account name saved";
"watch_only_accounts__pending_description" = "These accounts were created locally, but setup did not finish. Retry the same authorization to reuse the account.";
"watch_only_accounts__pending_section" = "Incomplete setup";
"watch_only_accounts__save_name" = "Save name";
"watch_only_accounts__setup_not_confirmed" = "Setup not confirmed";
"watch_only_accounts__setup_not_finished" = "Setup did not finish. Retry the same authorization to use this account.";
"watch_only_accounts__title" = "Server Accounts";
"watch_only_accounts__tracking" = "Track account";
"watch_only_accounts__tracking_disabled" = "Account tracking disabled";
"watch_only_accounts__tracking_enabled" = "Account tracking enabled";
"watch_only_accounts__xpub" = "EXTENDED PUBLIC KEY";
"settings__settings" = "Settings";
"settings__dev_enabled_title" = "Dev Options Enabled";
"settings__dev_enabled_message" = "Developer options are now enabled throughout the app.";
Expand Down
Loading
Loading