-
Notifications
You must be signed in to change notification settings - Fork 3
feat: support incoming paykit requests #637
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ import SwiftUI | |
| import UserNotifications | ||
|
|
||
| struct AppScene: View { | ||
| private static let paykitPaymentRequestRefreshInterval: Duration = .seconds(30) | ||
|
|
||
| @Environment(\.scenePhase) var scenePhase | ||
| @EnvironmentObject private var session: SessionManager | ||
|
|
||
|
|
@@ -35,6 +37,8 @@ struct AppScene: View { | |
| @State private var trezorViewModel: TrezorViewModel | ||
| @State private var hwWalletManager: HwWalletManager | ||
| @State private var calculatorInputManager = CalculatorInputManager() | ||
| @State private var paykitPaymentRequestManager = PaykitPaymentRequestManager() | ||
| @State private var isPresentingPaykitPaymentRequest = false | ||
|
|
||
| @State private var hideSplash = false | ||
| @State private var removeSplash = false | ||
|
|
@@ -133,6 +137,7 @@ struct AppScene: View { | |
| config in AppUpdateSheet(config: config) | ||
| } | ||
| .task(priority: .userInitiated, setupTask) | ||
| .task(id: scenePhase) { await pollIncomingPaykitPaymentRequests() } | ||
| .onChange(of: currency.hasStaleData) { _, newValue in handleCurrencyStaleData(newValue) } | ||
| .onChange(of: wallet.walletExists) { _, newValue in handleWalletExistsChange(newValue) } | ||
| .onChange(of: wallet.nodeLifecycleState) { _, newValue in handleNodeLifecycleChange(newValue) } | ||
|
|
@@ -191,17 +196,20 @@ struct AppScene: View { | |
| .environment(trezorViewModel) | ||
| .environment(hwWalletManager) | ||
| .environment(calculatorInputManager) | ||
| .environment(paykitPaymentRequestManager) | ||
| .onChange(of: pubkyProfile.authState, initial: true) { _, authState in | ||
| if authState == .authenticated, let pk = pubkyProfile.publicKey { | ||
| Task { | ||
| try? await contactsManager.loadContacts(for: pk) | ||
| await refreshPrivateOnlyPaykitReceiverMarker() | ||
| await refreshIncomingPaykitPaymentRequests() | ||
| if !PaykitFeatureFlags.isUIEnabled, wallet.walletExists == true { | ||
| await retryPendingPaykitEndpointRemoval() | ||
| } | ||
| } | ||
| } else if authState == .idle { | ||
| contactsManager.reset() | ||
| paykitPaymentRequestManager.clear() | ||
| } | ||
| } | ||
| .onReceive(contactsManager.$contacts) { contacts in | ||
|
|
@@ -212,8 +220,24 @@ struct AppScene: View { | |
| let publicKeys = contacts.map(\.publicKey) | ||
| Task { | ||
| await PrivatePaykitService.shared.prepareSavedContacts(publicKeys, wallet: wallet) | ||
| await refreshIncomingPaykitPaymentRequests() | ||
| } | ||
| } | ||
| .onReceive(sheets.$activeSheetConfiguration) { configuration in | ||
| guard configuration == nil else { return } | ||
| 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 }), | ||
| sheets.activeSheetConfiguration?.id == .send | ||
| else { return } | ||
|
|
||
| app.resetSendState() | ||
| wallet.resetSendState(speed: settings.defaultTransactionSpeed) | ||
| sheets.hideSheetIfActive(.send, reason: "Incoming payment request expired") | ||
| } | ||
| .onChange(of: navigation.currentRoute) { oldRoute, newRoute in | ||
| guard shouldDiscardPendingImport(currentRoute: oldRoute, destination: newRoute) else { | ||
| return | ||
|
|
@@ -639,6 +663,7 @@ struct AppScene: View { | |
| contactsManager.contacts.map(\.publicKey), | ||
| wallet: wallet | ||
| ) | ||
| await refreshIncomingPaykitPaymentRequests() | ||
| } | ||
| } else { | ||
| if case .errorStarting = state { | ||
|
|
@@ -680,6 +705,7 @@ struct AppScene: View { | |
| savedPublicKeys: contactPublicKeys, | ||
| wallet: wallet | ||
| ) | ||
| await refreshIncomingPaykitPaymentRequests() | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -699,6 +725,100 @@ struct AppScene: View { | |
| } | ||
| } | ||
|
|
||
| private func refreshIncomingPaykitPaymentRequests() async { | ||
| guard PaykitFeatureFlags.isUIEnabled, | ||
| wallet.walletExists == true, | ||
| pubkyProfile.authState == .authenticated | ||
| else { return } | ||
|
|
||
| await paykitPaymentRequestManager.refresh() | ||
| await presentNextIncomingPaykitPaymentRequest() | ||
| } | ||
|
|
||
| private func pollIncomingPaykitPaymentRequests() async { | ||
| guard scenePhase == .active else { return } | ||
|
|
||
| while !Task.isCancelled { | ||
| do { | ||
| try await Task.sleep(for: Self.paykitPaymentRequestRefreshInterval) | ||
| } catch { | ||
| return | ||
| } | ||
| await refreshIncomingPaykitPaymentRequests() | ||
| } | ||
| } | ||
|
|
||
| private func presentNextIncomingPaykitPaymentRequest() async { | ||
| guard !isPresentingPaykitPaymentRequest, | ||
| sheets.activeSheetConfiguration == nil, | ||
| app.contactPaymentContext == nil | ||
| else { return } | ||
|
|
||
| let requests = paykitPaymentRequestManager.requestsForPresentation() | ||
| guard !requests.isEmpty else { return } | ||
|
|
||
| isPresentingPaykitPaymentRequest = true | ||
| defer { isPresentingPaykitPaymentRequest = false } | ||
|
|
||
| for request in requests { | ||
| do { | ||
| let result = try await PrivatePaykitService.shared.beginPaymentRequest(request) | ||
| guard sheets.activeSheetConfiguration == nil, app.contactPaymentContext == nil else { return } | ||
| guard case let .opened(paymentTarget, privatePaymentContext) = result else { continue } | ||
|
Comment on lines
+763
to
+767
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if beginPaymentRequest never returns
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is also no reject path |
||
|
|
||
| let contactPaymentContext = ContactPaymentContext( | ||
| publicKey: request.counterparty, | ||
| privatePaymentContext: privatePaymentContext, | ||
| incomingPaymentRequest: request | ||
| ) | ||
| guard app.claimContactPaymentContext(contactPaymentContext) else { return } | ||
|
|
||
| do { | ||
| try await app.handleScannedData( | ||
| paymentTarget, | ||
| claimedContactPaymentContext: contactPaymentContext | ||
| ) | ||
| guard app.ownsContactPaymentContext(contactPaymentContext), | ||
| sheets.activeSheetConfiguration == nil | ||
| else { return } | ||
| guard PaymentNavigationHelper.appropriateSendRoute(app: app, currency: currency, settings: settings) != nil else { | ||
| app.resetSendState() | ||
| wallet.resetSendState(speed: settings.defaultTransactionSpeed) | ||
| continue | ||
| } | ||
|
|
||
| guard paykitPaymentRequestManager.markPresentedIfPending(request) else { | ||
| app.resetSendState() | ||
| wallet.resetSendState(speed: settings.defaultTransactionSpeed) | ||
| continue | ||
| } | ||
| } catch is CancellationError { | ||
| if app.ownsContactPaymentContext(contactPaymentContext) { | ||
| app.resetSendState() | ||
| wallet.resetSendState(speed: settings.defaultTransactionSpeed) | ||
| } | ||
| return | ||
| } catch { | ||
| guard app.ownsContactPaymentContext(contactPaymentContext) else { return } | ||
| Logger.warn("Failed to present incoming Paykit payment request: \(error)", context: "AppScene") | ||
| app.resetSendState() | ||
| wallet.resetSendState(speed: settings.defaultTransactionSpeed) | ||
| continue | ||
| } | ||
|
|
||
| wallet.sendAmountSats = request.amountSats | ||
|
|
||
| let route: SendRoute = app.lnurlPayData == nil ? .confirm : .lnurlPayConfirm | ||
| sheets.showSheet(.send, data: SendConfig(view: route)) | ||
| return | ||
| } catch is CancellationError { | ||
| return | ||
| } catch { | ||
| Logger.warn("Failed to present incoming Paykit payment request: \(error)", context: "AppScene") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func retryPendingPaykitEndpointRemoval() async { | ||
| if PublicPaykitService.isCleanupPending { | ||
| do { | ||
|
|
@@ -753,6 +873,7 @@ struct AppScene: View { | |
| // to display balances (MoneyText returns "0" if rates are nil) | ||
| Task { | ||
| await currency.refresh() | ||
| await refreshIncomingPaykitPaymentRequests() | ||
| } | ||
|
|
||
| // Restart node if necessary (e.g. create/restore was skipped due to offline) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method perform multiple requests every 30 seconds for as log the app is in foreground, even if nothings has changed
Maybe should back off the requests when nothing has been returned or scaling the interval by linked-peer count