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
204 changes: 191 additions & 13 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,131 @@ import LDKNode
import SwiftUI
import UserNotifications

struct IncomingPaykitPaymentRequestPresentationFeedback: Equatable {
struct Toast: Equatable {
let titleKey: String
let descriptionKey: String
let accessibilityIdentifier: String
}

let diagnosticReason: IncomingPaykitPaymentRequestFailureReason
let toast: Toast?

init(
deferral: PaykitPaymentRequestPresentationDeferral,
fallbackReason: IncomingPaykitPaymentRequestFailureReason
) {
switch deferral {
case .requestedPresentationEnded:
diagnosticReason = fallbackReason
toast = Toast(
titleKey: "wallet__payment_request",
descriptionKey: "wallet__payment_request_unavailable",
accessibilityIdentifier: "PaymentRequestUnavailableToast"
)
case let .requestExpired(wasRequested):
diagnosticReason = .requestExpired
toast = wasRequested ? Toast(
titleKey: "wallet__payment_request",
descriptionKey: "wallet__payment_request_expired",
accessibilityIdentifier: "PaymentRequestExpiredToast"
) : nil
case .retryScheduled, .ignored:
diagnosticReason = fallbackReason
toast = nil
}
}
}

struct IncomingPaykitPaymentRequestPresentationState: Equatable {
let requestedPresentationId: PaykitPaymentRequest.ID?
let retryTrigger: Int
let expirationTrigger: Int
let unavailableTrigger: Int

init(
requestedPresentationId: PaykitPaymentRequest.ID?,
retryTrigger: Int,
expirationTrigger: Int,
unavailableTrigger: Int
) {
self.requestedPresentationId = requestedPresentationId
self.retryTrigger = retryTrigger
self.expirationTrigger = expirationTrigger
self.unavailableTrigger = unavailableTrigger
}

@MainActor
init(_ manager: PaykitPaymentRequestManager) {
self.init(
requestedPresentationId: manager.requestedPresentationId,
retryTrigger: manager.presentationRetryTrigger,
expirationTrigger: manager.requestedPresentationExpirationTrigger,
unavailableTrigger: manager.requestedPresentationUnavailableTrigger
)
}
}

enum IncomingPaykitPaymentRequestPresentationDispatch: Equatable {
case presentFeedback(IncomingPaykitPaymentRequestPresentationFeedback, PaykitPaymentRequest)
case presentNext
}

@MainActor
enum IncomingPaykitPaymentRequestPresentationDispatcher {
static func feedback(
deferring request: PaykitPaymentRequest,
reason: IncomingPaykitPaymentRequestFailureReason,
with manager: PaykitPaymentRequestManager
) -> IncomingPaykitPaymentRequestPresentationFeedback {
IncomingPaykitPaymentRequestPresentationFeedback(
deferral: manager.deferPresentation(request),
fallbackReason: reason
)
}

static func handleStateChange(
from previous: IncomingPaykitPaymentRequestPresentationState,
to current: IncomingPaykitPaymentRequestPresentationState,
manager: PaykitPaymentRequestManager
) -> [IncomingPaykitPaymentRequestPresentationDispatch] {
var dispatches: [IncomingPaykitPaymentRequestPresentationDispatch] = []
if current.expirationTrigger != previous.expirationTrigger {
while let request = manager.consumeExpiredRequestedPresentation() {
dispatches.append(
.presentFeedback(
IncomingPaykitPaymentRequestPresentationFeedback(
deferral: .requestExpired(wasRequested: true),
fallbackReason: .resolutionFailed
),
request
)
)
}
}
if current.unavailableTrigger != previous.unavailableTrigger {
while let request = manager.consumeUnavailableRequestedPresentation() {
dispatches.append(
.presentFeedback(
IncomingPaykitPaymentRequestPresentationFeedback(
deferral: .requestedPresentationEnded,
fallbackReason: .resolutionFailed
),
request
)
)
}
}
if current.retryTrigger != previous.retryTrigger ||
Comment thread
ovitrif marked this conversation as resolved.
previous.requestedPresentationId != current.requestedPresentationId && current.requestedPresentationId != nil ||
current.expirationTrigger != previous.expirationTrigger
{
dispatches.append(.presentNext)
}
return dispatches
}
}

struct AppScene: View {
private static let paykitPaymentRequestRefreshIntervals: [Duration] = [.seconds(30), .seconds(60), .seconds(120)]
private static let initialPaykitSyncRetryDelays = Array(repeating: Duration.seconds(2), count: 14)
Expand Down Expand Up @@ -249,12 +374,8 @@ struct AppScene: View {
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: incomingPaykitPaymentRequestPresentationState) { previous, current in
handleIncomingPaykitPaymentRequestPresentationStateChange(from: previous, to: current)
}
.onChange(of: paykitPaymentRequestManager.pendingRequests) { _, requests in
guard let request = app.contactPaymentContext?.incomingPaymentRequest,
Expand Down Expand Up @@ -837,8 +958,10 @@ struct AppScene: View {
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)
deferIncomingPaykitPaymentRequestPresentation(
request,
reason: result.incomingPaymentRequestFailureReason ?? .resolutionFailed
)
continue
}

Expand Down Expand Up @@ -869,7 +992,7 @@ struct AppScene: View {
guard PaymentNavigationHelper.appropriateSendRoute(app: app, currency: currency, settings: settings) != nil else {
app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
paykitPaymentRequestManager.deferPresentation(request)
deferIncomingPaykitPaymentRequestPresentation(request, reason: .paymentTargetNotRoutable)
continue
}

Expand All @@ -891,10 +1014,9 @@ struct AppScene: View {
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)
paykitPaymentRequestManager.deferPresentation(request)
deferIncomingPaykitPaymentRequestPresentation(request, reason: .invalidPaymentTarget)
continue
}

Expand All @@ -906,6 +1028,7 @@ struct AppScene: View {
else {
app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
deferIncomingPaykitPaymentRequestPresentation(request, reason: .paymentTargetNotRoutable)
return
}
sheets.showSheet(.send, data: SendConfig(view: route))
Expand All @@ -914,8 +1037,7 @@ struct AppScene: View {
return
} catch {
guard paykitPaymentRequestManager.isCurrentPresentation(request) else { return }
Logger.warn("Failed to present incoming Paykit payment request: \(error)", context: "AppScene")
paykitPaymentRequestManager.deferPresentation(request)
deferIncomingPaykitPaymentRequestPresentation(request, reason: .resolutionFailed)
}
}
}
Expand All @@ -930,6 +1052,62 @@ struct AppScene: View {
await presentNextIncomingPaykitPaymentRequest()
}

private func deferIncomingPaykitPaymentRequestPresentation(
_ request: PaykitPaymentRequest,
reason: IncomingPaykitPaymentRequestFailureReason
) {
presentIncomingPaykitPaymentRequestFeedback(
IncomingPaykitPaymentRequestPresentationDispatcher.feedback(
deferring: request,
reason: reason,
with: paykitPaymentRequestManager
),
for: request
)
}

private var incomingPaykitPaymentRequestPresentationState: IncomingPaykitPaymentRequestPresentationState {
IncomingPaykitPaymentRequestPresentationState(paykitPaymentRequestManager)
}

private func handleIncomingPaykitPaymentRequestPresentationStateChange(
from previous: IncomingPaykitPaymentRequestPresentationState,
to current: IncomingPaykitPaymentRequestPresentationState
) {
for dispatch in IncomingPaykitPaymentRequestPresentationDispatcher.handleStateChange(
from: previous,
to: current,
manager: paykitPaymentRequestManager
) {
switch dispatch {
case let .presentFeedback(feedback, request):
presentIncomingPaykitPaymentRequestFeedback(feedback, for: request)
case .presentNext:
Task { await presentNextIncomingPaykitPaymentRequest() }
}
}
}

private func presentIncomingPaykitPaymentRequestFeedback(
_ feedback: IncomingPaykitPaymentRequestPresentationFeedback,
for request: PaykitPaymentRequest
) {
Logger.warn(
"Rejected incoming Paykit payment request presentation: category=\(feedback.diagnosticReason.category) " +
"reason=\(feedback.diagnosticReason.rawValue) " +
"counterparty=\(PaykitPaymentRequestDiagnostics.redactedCounterparty(request.counterparty))",
context: "AppScene"
)

guard let toast = feedback.toast else { return }
app.toast(
type: .error,
title: t(toast.titleKey),
description: t(toast.descriptionKey),
accessibilityIdentifier: toast.accessibilityIdentifier
)
}

private func retryPendingPaykitEndpointRemoval() async {
if PublicPaykitService.isCleanupPending {
do {
Expand Down
Loading
Loading