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
2 changes: 1 addition & 1 deletion Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1182,7 +1182,7 @@
repositoryURL = "https://github.com/pubky/paykit-rs";
requirement = {
kind = exactVersion;
version = "0.1.0-rc43";
version = "0.1.0-rc46";
};
};
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.

82 changes: 67 additions & 15 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ struct AppScene: View {
.environment(paykitPaymentRequestManager)
.onChange(of: pubkyProfile.authState, initial: true) { _, authState in
if authState == .authenticated, let pk = pubkyProfile.publicKey {
paykitPaymentRequestManager.activate(identity: pk)
Task {
try? await contactsManager.loadContacts(for: pk)
await refreshPrivateOnlyPaykitReceiverMarker()
Expand Down Expand Up @@ -235,20 +236,32 @@ struct AppScene: View {
.onReceive(PrivatePaykitService.initialLinkBurstStartedPublisher) {
initialPaykitSyncGeneration += 1
}
.onReceive(sheets.$activeSheetConfiguration) { configuration in
guard configuration == nil else { return }
.onChange(of: sheets.activeSheetConfiguration?.id) { _, activeSheetId in
guard activeSheetId == nil, !sheets.isReplacingSheet else { return }
Task {
try? await Task.sleep(for: .milliseconds(700))
guard !Task.isCancelled,
sheets.activeSheetConfiguration == nil,
!sheets.isReplacingSheet
else { return }
await presentNextIncomingPaykitPaymentRequest()
}
}
.onChange(of: paykitPaymentRequestManager.requestedPresentationId) { _, requestId in
guard requestId != nil else { return }
Task { await presentNextIncomingPaykitPaymentRequest() }
}
.onChange(of: paykitPaymentRequestManager.presentationRetryTrigger) {
Task { await presentNextIncomingPaykitPaymentRequest() }
}
.onChange(of: paykitPaymentRequestManager.pendingRequests) { _, requests in
guard let request = app.contactPaymentContext?.incomingPaymentRequest,
request.isExpired(at: Date()),
!requests.contains(where: { $0.id == request.id }),
!paykitPaymentRequestManager.isApprovedForPayment(request),
sheets.activeSheetConfiguration?.id == .send
else { return }

app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
sheets.hideSheetIfActive(.send, reason: "Incoming payment request expired")
sheets.hideSheetIfActive(.send, reason: "Incoming payment request is no longer available")
}
.onChange(of: navigation.currentRoute) { oldRoute, newRoute in
guard shouldDiscardPendingImport(currentRoute: oldRoute, destination: newRoute) else {
Expand Down Expand Up @@ -752,9 +765,13 @@ struct AppScene: View {
guard PaykitFeatureFlags.isUIEnabled,
wallet.walletExists == true,
pubkyProfile.authState == .authenticated
else { return false }
else {
paykitPaymentRequestManager.clearEligibleTargets()
return false
}

let previousRequests = paykitPaymentRequestManager.pendingRequests
await paykitPaymentRequestManager.refreshEligibleTargets(savedPublicKeys: contactsManager.contacts.map(\.publicKey))
await paykitPaymentRequestManager.refresh()
await presentNextIncomingPaykitPaymentRequest()
return paykitPaymentRequestManager.pendingRequests != previousRequests
Expand Down Expand Up @@ -800,16 +817,23 @@ struct AppScene: View {

private func presentNextIncomingPaykitPaymentRequest() async {
guard sheets.activeSheetConfiguration == nil,
!sheets.isReplacingSheet,
app.contactPaymentContext == nil
else { return }

await paykitPaymentRequestManager.presentRequests { requests in
guard sheets.activeSheetConfiguration == nil, app.contactPaymentContext == nil else { return }
let attemptedPresentation = await paykitPaymentRequestManager.presentRequests { requests in
guard sheets.activeSheetConfiguration == nil, !sheets.isReplacingSheet, app.contactPaymentContext == nil else { return }
for request in requests {
guard paykitPaymentRequestManager.isCurrentPresentation(request) else { return }
do {
let result = try await PrivatePaykitService.shared.beginPaymentRequest(request)
guard sheets.activeSheetConfiguration == nil, app.contactPaymentContext == nil else { return }
guard paykitPaymentRequestManager.isCurrentPresentation(request),
sheets.activeSheetConfiguration == nil,
!sheets.isReplacingSheet,
app.contactPaymentContext == nil
else { return }
guard case let .opened(paymentTarget, privatePaymentContext) = result else {
Logger.debug("Incoming Paykit payment request is waiting for private payment details: \(result)", context: "AppScene")
paykitPaymentRequestManager.deferPresentation(request)
continue
}
Expand All @@ -826,20 +850,28 @@ struct AppScene: View {
paymentTarget,
claimedContactPaymentContext: contactPaymentContext
)
guard app.ownsContactPaymentContext(contactPaymentContext),
sheets.activeSheetConfiguration == nil
else { return }
guard paykitPaymentRequestManager.isCurrentPresentation(request),
app.ownsContactPaymentContext(contactPaymentContext),
sheets.activeSheetConfiguration == nil,
!sheets.isReplacingSheet
else {
if app.ownsContactPaymentContext(contactPaymentContext) {
app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
}
return
}
guard PaymentNavigationHelper.appropriateSendRoute(app: app, currency: currency, settings: settings) != nil else {
app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
paykitPaymentRequestManager.deferPresentation(request)
continue
}

guard paykitPaymentRequestManager.markPresentedIfPending(request) else {
guard paykitPaymentRequestManager.isCurrentPresentation(request) else {
app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
continue
return
}
} catch is CancellationError {
if app.ownsContactPaymentContext(contactPaymentContext) {
Expand All @@ -849,6 +881,11 @@ struct AppScene: View {
return
} catch {
guard app.ownsContactPaymentContext(contactPaymentContext) else { return }
guard paykitPaymentRequestManager.isCurrentPresentation(request) else {
app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
return
}
Logger.warn("Failed to present incoming Paykit payment request: \(error)", context: "AppScene")
app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
Expand All @@ -857,16 +894,31 @@ struct AppScene: View {
}

let route: SendRoute = app.lnurlPayData == nil ? .confirm : .lnurlPayConfirm
guard paykitPaymentRequestManager.isCurrentPresentation(request) else {
app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
return
}
sheets.showSheet(.send, data: SendConfig(view: route))
return
} catch is CancellationError {
return
} catch {
guard paykitPaymentRequestManager.isCurrentPresentation(request) else { return }
Logger.warn("Failed to present incoming Paykit payment request: \(error)", context: "AppScene")
paykitPaymentRequestManager.deferPresentation(request)
}
}
}

guard attemptedPresentation,
paykitPaymentRequestManager.requestedPresentationId != nil ||
!paykitPaymentRequestManager.requestsForPresentation().isEmpty,
sheets.activeSheetConfiguration == nil,
!sheets.isReplacingSheet,
app.contactPaymentContext == nil
else { return }
await presentNextIncomingPaykitPaymentRequest()
}

private func retryPendingPaykitEndpointRemoval() async {
Expand Down
15 changes: 14 additions & 1 deletion Bitkit/Components/DrawerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI
enum DrawerMenuItem: Int, CaseIterable, Identifiable, Hashable {
case wallet
case activity
case paymentRequests
case contacts
case profile
case widgets
Expand All @@ -19,6 +20,7 @@ enum DrawerMenuItem: Int, CaseIterable, Identifiable, Hashable {
switch self {
case .wallet: return "coins"
case .activity: return "activity"
case .paymentRequests: return "file-text"
case .contacts: return "users"
case .profile: return "user-square"
case .widgets: return "stack"
Expand All @@ -33,6 +35,7 @@ enum DrawerMenuItem: Int, CaseIterable, Identifiable, Hashable {
switch self {
case .wallet: return t("wallet__drawer__wallet")
case .activity: return t("wallet__drawer__activity")
case .paymentRequests: return t("wallet__drawer__payment_requests")
case .contacts: return t("wallet__drawer__contacts")
case .profile: return t("wallet__drawer__profile")
case .widgets: return t("wallet__drawer__widgets")
Expand All @@ -56,6 +59,7 @@ enum DrawerMenuItem: Int, CaseIterable, Identifiable, Hashable {
switch self {
case .wallet: return "DrawerWallet"
case .activity: return "DrawerActivity"
case .paymentRequests: return "DrawerPaymentRequests"
case .contacts: return "DrawerContacts"
case .profile: return "DrawerProfile"
case .widgets: return "DrawerWidgets"
Expand All @@ -68,6 +72,8 @@ enum DrawerMenuItem: Int, CaseIterable, Identifiable, Hashable {
}

struct DrawerView: View {
@AppStorage(PaykitFeatureFlags.uiEnabledKey) private var isPaykitUIEnabled = false

@EnvironmentObject private var app: AppViewModel
@EnvironmentObject private var navigation: NavigationViewModel
@EnvironmentObject private var settings: SettingsViewModel
Expand All @@ -78,6 +84,12 @@ struct DrawerView: View {
@State private var showBackdrop = false
@State private var showMenu = false

private var mainMenuItems: [DrawerMenuItem] {
DrawerMenuItem.allCases.filter { item in
item.isMainMenuItem && (item != .paymentRequests || PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled)
}
}

private func closeMenu() {
withAnimation(.easeOut(duration: 0.25)) {
showBackdrop = false
Expand All @@ -104,6 +116,7 @@ struct DrawerView: View {
switch item {
case .wallet: return nil
case .activity: return .activityList
case .paymentRequests: return .paymentRequests
case .contacts: return .contacts
case .profile: return .profile
case .widgets: return nil
Expand Down Expand Up @@ -163,7 +176,7 @@ struct DrawerView: View {
if showMenu {
GeometryReader { geometry in
VStack(alignment: .leading, spacing: 0) {
ForEach(DrawerMenuItem.allCases.filter(\.isMainMenuItem)) { item in
ForEach(mainMenuItems) { item in
Button(action: {
selectDrawerItem(item)
}) {
Expand Down
26 changes: 26 additions & 0 deletions Bitkit/Components/Header.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ struct Header: View {
@EnvironmentObject var app: AppViewModel
@EnvironmentObject var navigation: NavigationViewModel
@EnvironmentObject var pubkyProfile: PubkyProfileManager
@EnvironmentObject private var sheets: SheetViewModel
@Environment(PaykitPaymentRequestManager.self) private var paymentRequests

/// When true, shows the widget edit button (only on the widgets tab).
var showWidgetEditButton: Bool = false
Expand Down Expand Up @@ -40,6 +42,30 @@ struct Header: View {
}
)

if isPaykitUIActive, !paymentRequests.pendingRequests.isEmpty {
Button {
if dismissCalculatorIfNeeded() { return }
sheets.showSheet(.paymentRequests)
} label: {
Image("bell")
.resizable()
.scaledToFit()
.foregroundColor(.brandAccent)
.frame(width: 24, height: 24)
.frame(width: 32, height: 32)
.contentShape(Rectangle())
.shadow(color: .brandAccent.opacity(0.5), radius: 8)
}
.accessibilityLabel(t("wallet__payment_requests"))
.accessibilityValue(
t(
"wallet__payment_requests_pending_count",
variables: ["count": "\(paymentRequests.pendingRequests.count)"]
)
)
.accessibilityIdentifier("PaymentRequestsBell")
}

if showWidgetEditButton {
Button(action: {
if dismissCalculatorIfNeeded() { return }
Expand Down
32 changes: 32 additions & 0 deletions Bitkit/Components/NoteTextEditor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import SwiftUI

struct NoteTextEditor: View {
@Binding var text: String
let placeholder: String
let testIdentifier: String
let isFocused: FocusState<Bool>.Binding

var body: some View {
ZStack(alignment: .topLeading) {
if text.isEmpty {
BodySSBText(placeholder, textColor: .textSecondary)
}

TextEditor(text: $text)
.focused(isFocused)
.font(.custom(Fonts.semiBold, size: 15))
.foregroundColor(.textPrimary)
.accentColor(.brandAccent)
.submitLabel(.done)
.scrollContentBackground(.hidden)
.padding(EdgeInsets(top: -8, leading: -5, bottom: -5, trailing: -5))
.frame(minHeight: 30, maxHeight: 50)
.dismissKeyboardOnReturn(text: $text, isFocused: isFocused)
.accessibilityValue(text)
.accessibilityIdentifier(testIdentifier)
}
.padding()
.background(Color.white06)
.cornerRadius(8)
}
}
10 changes: 10 additions & 0 deletions Bitkit/Components/PubkyContactRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ struct PubkyContactRow: View {
var verticalPadding: CGFloat = 12
var showsDivider = true
var isLoading = false
var isSelected = false
var selectionColor: Color = .brandAccent
let action: () -> Void

var body: some View {
Expand All @@ -25,6 +27,13 @@ struct PubkyContactRow: View {

if isLoading {
ProgressView()
} else if isSelected {
Image("check-mark")
.resizable()
.scaledToFit()
.foregroundColor(selectionColor)
.frame(width: 24, height: 24)
.accessibilityHidden(true)
}
}
.padding(.vertical, verticalPadding)
Expand All @@ -33,6 +42,7 @@ struct PubkyContactRow: View {
.buttonStyle(.plain)
.disabled(isLoading)
.accessibilityLabel(contact.displayName)
.accessibilityAddTraits(isSelected ? .isSelected : [])

if showsDivider {
CustomDivider()
Expand Down
4 changes: 4 additions & 0 deletions Bitkit/Constants/Env.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ enum Env {
infoPlistValue("E2E_HOMEGATE_URL") ?? "http://\(e2eLocalHost):6288"
}

static var e2eHomeserverPubky: String? {
isLocalE2EBackend ? infoPlistValue("E2E_HOMESERVER_PUBKY") : nil
}

static var pubkyLocalTestnetHost: String? {
isLocalE2EBackend ? e2eLocalHost : nil
}
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
<string>$(E2E_BACKEND)</string>
<key>E2E_HOMEGATE_URL</key>
<string>$(E2E_HOMEGATE_URL)</string>
<key>E2E_HOMESERVER_PUBKY</key>
<string>$(E2E_HOMESERVER_PUBKY)</string>
<key>E2E_LOCAL_HOST</key>
<string>$(E2E_LOCAL_HOST)</string>
<key>E2E_NETWORK</key>
Expand Down
Loading
Loading