Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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