Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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 @@ -1179,7 +1179,7 @@
repositoryURL = "https://github.com/pubky/paykit-rs";
requirement = {
kind = exactVersion;
version = "0.1.0-rc43";
version = "0.1.0-rc44";
};
};
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
47 changes: 47 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 All @@ -18,6 +20,10 @@ struct Header: View {
PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled
}

private var hasPaymentRequests: Bool {
!paymentRequests.pendingRequests.isEmpty || !paymentRequests.sentRequests.isEmpty
}

init(showWidgetEditButton: Bool = false, isEditingWidgets: Binding<Bool> = .constant(false)) {
self.showWidgetEditButton = showWidgetEditButton
_isEditingWidgets = isEditingWidgets
Expand All @@ -40,6 +46,47 @@ struct Header: View {
}
)

if isPaykitUIActive, hasPaymentRequests {
Button {
if dismissCalculatorIfNeeded() { return }
if paymentRequests.pendingRequests.isEmpty {
navigation.navigate(.paymentRequests)
} else {
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)
.overlay(alignment: .topTrailing) {
if !paymentRequests.pendingRequests.isEmpty {
CaptionMText(
paymentRequests.pendingRequests.count > 99 ? "99+" : "\(paymentRequests.pendingRequests.count)",
textColor: .black
)
.frame(minWidth: 16, minHeight: 16)
.background(Color.brandAccent)
.clipShape(Capsule())
.offset(x: 4, y: -4)
.accessibilityHidden(true)
}
}
}
.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
9 changes: 9 additions & 0 deletions Bitkit/Components/PubkyContactRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ struct PubkyContactRow: View {
var verticalPadding: CGFloat = 12
var showsDivider = true
var isLoading = false
var isSelected = false
let action: () -> Void

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

if isLoading {
ProgressView()
} else if isSelected {
Image("check-mark")
.resizable()
.scaledToFit()
.foregroundColor(.brandAccent)
.frame(width: 24, height: 24)
.accessibilityHidden(true)
}
}
.padding(.vertical, verticalPadding)
Expand All @@ -33,6 +41,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
12 changes: 12 additions & 0 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ struct MainNavView: View {
) {
config in NotificationsSheet(config: config)
}
.sheet(
item: $sheets.paymentRequestsSheetItem,
onDismiss: {
sheets.hideSheetIfActive(.paymentRequests, reason: "Payment requests sheet dismissed")
}
) {
config in PaymentRequestsSheet(config: config)
}
.sheet(
item: $sheets.receiveSheetItem,
onDismiss: {
Expand Down Expand Up @@ -174,6 +182,8 @@ struct MainNavView: View {
.sheet(
item: $sheets.sendSheetItem,
onDismiss: {
app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
sheets.hideSheetIfActive(.send, reason: "Send sheet dismissed")
}
) {
Expand Down Expand Up @@ -530,6 +540,8 @@ struct MainNavView: View {
if isPaykitUIActive { EditProfileView() } else { paykitDisabledRedirectView }
case .payContacts:
if isPaykitUIActive { PayContactsView() } else { paykitDisabledRedirectView }
case .paymentRequests:
if isPaykitUIActive { PaymentRequestsView() } else { paykitDisabledRedirectView }

// Shop
case .shopIntro: ShopIntro()
Expand Down
12 changes: 9 additions & 3 deletions Bitkit/Managers/PubkyProfileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -255,14 +255,20 @@ class PubkyProfileManager: ObservableObject {
let (publicKeyZ32, secretKeyHex) = try await deriveKeys()

_ = try await Task.detached {
let homegate = try await Self.fetchHomegateSignupCode()
let signupDetails: (homeserverPubky: String, signupCode: String?)
if let homeserverPubky = Env.e2eHomeserverPubky {
signupDetails = (homeserverPubky, nil)
} else {
let homegate = try await Self.fetchHomegateSignupCode()
signupDetails = (homegate.homeserverPubky, homegate.signupCode)
}

var session: String
do {
session = try await PubkyService.signUp(
secretKeyHex: secretKeyHex,
homeserverZ32: homegate.homeserverPubky,
signupCode: homegate.signupCode
homeserverZ32: signupDetails.homeserverPubky,
signupCode: signupDetails.signupCode
)
} catch {
Logger.info("signUp failed (likely already registered), trying signIn: \(error)", context: "PubkyProfileManager")
Expand Down
24 changes: 24 additions & 0 deletions Bitkit/Resources/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,30 @@
"wallet__payment_request_expired" = "The payment request has expired.";
"wallet__payment_request_in_progress" = "The payment request is already being processed.";
"wallet__payment_request_mismatch" = "The payment details did not match the request. Payment cancelled.";
"wallet__payment_requests" = "Payment Requests";
"wallet__payment_requests_pending_count" = "{count} pending";
"wallet__payment_requests_review" = "Review each request, then pay or dismiss.";
"wallet__payment_request_reject" = "Reject";
"wallet__payment_requests_not_now" = "Not Now";
"wallet__payment_requests_see_all" = "See All";
"wallet__payment_requests_incoming" = "Incoming";
"wallet__payment_requests_sent" = "Sent";
"wallet__payment_requests_empty" = "No active payment requests";
"wallet__payment_request_send" = "Send Payment Request";
"wallet__payment_request_amount" = "Amount";
"wallet__payment_request_expires" = "Expires In";
"wallet__payment_request_expiry_hour" = "1 hour";
"wallet__payment_request_expiry_day" = "1 day";
"wallet__payment_request_expiry_week" = "1 week";
"wallet__payment_request_expiry_month" = "1 month";
"wallet__payment_request_choose_recipient" = "Choose Recipient";
"wallet__payment_request_send_request" = "Send Request";
"wallet__payment_request_sent_title" = "Sent";
"wallet__payment_request_sent_headline" = "Payment <accent>Requested</accent>";
"wallet__payment_request_sent_description" = "You have sent a payment request";
"wallet__payment_request_queued_description" = "Your payment request is queued and will send automatically";
"wallet__payment_request_sending" = "Sending request";
"wallet__payment_request_waiting" = "Waiting for payment";
"wallet__instant_payment_received" = "Received Instant Bitcoin";
"wallet__error_create_tx" = "Transaction Creation Failed";
"wallet__error_create_tx_msg" = "An error occurred. Please try again {raw}";
Expand Down
Loading
Loading