Skip to content
Merged
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
55 changes: 30 additions & 25 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,27 +151,26 @@ enum IncomingPaykitPaymentRequestPresentationDispatcher {
}
}

enum PaykitPaymentRequestPollingRound: Equatable {
case skip
case refreshInbox
case refreshInboxAndMaintenance
}

struct PaykitPaymentRequestPollingSchedule {
private static let refreshIntervals: [Duration] = [.seconds(5), .seconds(10), .seconds(15), .seconds(30)]
let nextDelay: Duration = .seconds(10)
private static let maintenanceIntervals: [Duration] = [.seconds(30), .seconds(60), .seconds(120)]
private var refreshIntervalIndex = 0
private var maintenanceIntervalIndex = 0
private var maintenanceDelay = Self.maintenanceIntervals[0]

var nextDelay: Duration {
Self.refreshIntervals[refreshIntervalIndex]
}
mutating func takeRound(isConnected: Bool) -> PaykitPaymentRequestPollingRound {
guard isConnected else { return .skip }

mutating func takeMaintenanceIfDue() -> Bool {
maintenanceDelay -= nextDelay
guard maintenanceDelay <= .zero else { return false }
guard maintenanceDelay <= .zero else { return .refreshInbox }
maintenanceIntervalIndex = min(maintenanceIntervalIndex + 1, Self.maintenanceIntervals.count - 1)
maintenanceDelay = Self.maintenanceIntervals[maintenanceIntervalIndex]
return true
}

mutating func recordRefresh(requestsChanged: Bool) {
refreshIntervalIndex = requestsChanged ? 0 : min(refreshIntervalIndex + 1, Self.refreshIntervals.count - 1)
return .refreshInboxAndMaintenance
}
}

Expand Down Expand Up @@ -317,7 +316,7 @@ struct AppScene: View {
config in AppUpdateSheet(config: config)
}
.task(priority: .userInitiated, setupTask)
.task(id: scenePhase) { await pollIncomingPaykitPaymentRequests() }
.task(id: [scenePhase == .active, network.isConnected]) { await pollIncomingPaykitPaymentRequests() }
.task(id: initialPaykitSyncGeneration) { await pollIncomingPaykitPaymentRequestsDuringInitialSync() }
.task { await handlePendingPaykitSubscriptionNotification() }
.onChange(of: currency.hasStaleData) { _, newValue in handleCurrencyStaleData(newValue) }
Expand Down Expand Up @@ -981,28 +980,25 @@ struct AppScene: View {
}
}

@discardableResult
private func refreshIncomingPaykitPaymentRequests(presentItems: Bool = true, refreshMaintenance: Bool = true) async -> Bool {
private func refreshIncomingPaykitPaymentRequests(presentItems: Bool = true, refreshMaintenance: Bool = true) async {
guard PaykitFeatureFlags.isUIEnabled,
wallet.walletExists == true,
pubkyProfile.authState == .authenticated
else {
paykitPaymentRequestManager.clearEligibleTargets()
return false
return
}

if refreshMaintenance {
await PaykitPaymentProofService.shared.reconcile()
}
let previousRequests = paykitPaymentRequestManager.pendingRequests
await paykitPaymentRequestManager.refresh()
if presentItems {
await presentNextIncomingPaykitItem()
}
if refreshMaintenance {
await paykitPaymentRequestManager.refreshEligibleTargets(savedPublicKeys: contactsManager.contacts.map(\.publicKey))
}
return paykitPaymentRequestManager.pendingRequests != previousRequests
}

private func associateResolvedPaykitOnchainPayment(_ resolution: PaykitOnchainPaymentResolution) async {
Expand Down Expand Up @@ -1034,26 +1030,33 @@ struct AppScene: View {
}

private func pollIncomingPaykitPaymentRequests() async {
guard scenePhase == .active else { return }
guard scenePhase == .active, network.isConnected else { return }

if network.isConnected { await PubkyService.republishIdentityIfNeeded(publicKey: pubkyProfile.publicKey) }
await PubkyService.republishIdentityIfNeeded(publicKey: pubkyProfile.publicKey)
var schedule = PaykitPaymentRequestPollingSchedule()
while !Task.isCancelled {
do {
try await Task.sleep(for: schedule.nextDelay)
} catch {
return
}
let refreshMaintenance = schedule.takeMaintenanceIfDue()
let refreshMaintenance: Bool
switch schedule.takeRound(isConnected: network.isConnected) {
case .skip:
continue
case .refreshInbox:
refreshMaintenance = false
case .refreshInboxAndMaintenance:
refreshMaintenance = true
}
if refreshMaintenance {
if network.isConnected { await PubkyService.republishIdentityIfNeeded(publicKey: pubkyProfile.publicKey) }
await PubkyService.republishIdentityIfNeeded(publicKey: pubkyProfile.publicKey)
await PrivatePaykitService.shared.refreshKnownSavedContactEndpoints(
wallet: wallet,
reason: "payment request polling"
)
}
let requestsChanged = await refreshIncomingPaykitPaymentRequests(refreshMaintenance: refreshMaintenance)
schedule.recordRefresh(requestsChanged: requestsChanged)
await refreshIncomingPaykitPaymentRequests(refreshMaintenance: refreshMaintenance)
}
}

Expand Down Expand Up @@ -1370,7 +1373,9 @@ struct AppScene: View {
// to display balances (MoneyText returns "0" if rates are nil)
Task {
await currency.refresh()
if scenePhase == .active { await PubkyService.republishIdentityIfNeeded(publicKey: pubkyProfile.publicKey) }
if scenePhase == .active {
await PubkyService.republishIdentityIfNeeded(publicKey: pubkyProfile.publicKey)
}
if PaykitFeatureFlags.isUIEnabled {
let contactPublicKeys = contactsManager.contacts.map(\.publicKey)
await PrivatePaykitService.shared.startInitialLinkBurst(
Expand Down
38 changes: 17 additions & 21 deletions BitkitTests/PaykitPaymentRequestPollingScheduleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,34 @@
import XCTest

final class PaykitPaymentRequestPollingScheduleTests: XCTestCase {
func testIdleInboxAndMaintenanceBackOffIndependently() {
func testInboxKeepsTenSecondChecksAndSlowerMaintenance() {
Comment thread
ovitrif marked this conversation as resolved.
var schedule = PaykitPaymentRequestPollingSchedule()
var elapsed: Duration = .zero
var maintenanceTimes: [Duration] = []

for delay in [5, 10, 15, 30, 30, 30, 30, 30, 30] {
XCTAssertEqual(schedule.nextDelay, .seconds(delay))
for _ in 0 ..< 21 {
XCTAssertEqual(schedule.nextDelay, .seconds(10))
elapsed += schedule.nextDelay
if schedule.takeMaintenanceIfDue() { maintenanceTimes.append(elapsed) }
schedule.recordRefresh(requestsChanged: false)
switch schedule.takeRound(isConnected: true) {
case .skip:
XCTFail("Connected polling rounds should refresh the inbox")
case .refreshInbox:
break
case .refreshInboxAndMaintenance:
maintenanceTimes.append(elapsed)
}
}

XCTAssertEqual(maintenanceTimes, [.seconds(30), .seconds(90), .seconds(210)])
}

func testRequestChangesResetInboxWithoutResettingMaintenance() {
func testOfflineRoundSkipsWorkWithoutAdvancingMaintenance() {
var schedule = PaykitPaymentRequestPollingSchedule()
var elapsed: Duration = .zero
var maintenanceTimes: [Duration] = []

for _ in 0 ..< 4 {
elapsed += schedule.nextDelay
if schedule.takeMaintenanceIfDue() { maintenanceTimes.append(elapsed) }
schedule.recordRefresh(requestsChanged: elapsed == .seconds(60))
}
while elapsed < .seconds(210) {
XCTAssertEqual(schedule.nextDelay, .seconds(5))
elapsed += schedule.nextDelay
if schedule.takeMaintenanceIfDue() { maintenanceTimes.append(elapsed) }
schedule.recordRefresh(requestsChanged: true)
}

XCTAssertEqual(maintenanceTimes, [.seconds(30), .seconds(90), .seconds(210)])
XCTAssertEqual(schedule.takeRound(isConnected: false), .skip)
XCTAssertEqual(schedule.nextDelay, .seconds(10))
XCTAssertEqual(schedule.takeRound(isConnected: true), .refreshInbox)
XCTAssertEqual(schedule.takeRound(isConnected: true), .refreshInbox)
XCTAssertEqual(schedule.takeRound(isConnected: true), .refreshInboxAndMaintenance)
}
}
34 changes: 33 additions & 1 deletion BitkitTests/PaykitPaymentRequestServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,27 @@ final class PaykitPaymentRequestServiceTests: XCTestCase {

await manager.refresh()

XCTAssertEqual(manager.pendingRequests.count, 1)
await sdk.setReceiveError(nil)
await manager.refresh()
XCTAssertTrue(manager.pendingRequests.isEmpty)
}

func testPeerIntakeFailureDoesNotDropReceivedRequests() async throws {
let record = try paymentRequestRecord()
let sdk = PaymentRequestSdkMock(records: [record])
await sdk.setReceiveReports([
PrivateStreamCounterpartyIntakeReport(
counterparty: record.counterparty,
counterpartyReceiverPath: record.counterpartyReceiverPath,
report: nil,
error: PaymentRequestIntakeError(noPointer: .init())
),
])
let manager = paymentRequestManager(sdk: sdk)

await manager.refresh()

XCTAssertEqual(manager.pendingRequests.count, 1)
}

Expand Down Expand Up @@ -3292,6 +3313,7 @@ private actor PaymentRequestSdkMock: PaykitPaymentRequestSdkHandling {
private var isProcessPaused = false
private var processContinuation: CheckedContinuation<Void, Never>?
private var receiveError: PaymentRequestSdkMockError?
private var receiveReports: [PrivateStreamCounterpartyIntakeReport] = []
private var acceptedRequests: [PaymentRequestInvocation] = []
private var rejectedRequests: [PaymentRequestInvocation] = []
private var acceptFailuresAfterRemoval = 0
Expand Down Expand Up @@ -3336,7 +3358,7 @@ private actor PaymentRequestSdkMock: PaykitPaymentRequestSdkHandling {
if let receiveError {
throw receiveError
}
return []
return receiveReports
}

func paymentRequests() async -> [PaymentRequestRecord] {
Expand Down Expand Up @@ -3619,6 +3641,10 @@ private actor PaymentRequestSdkMock: PaykitPaymentRequestSdkHandling {
receiveError = error
}

func setReceiveReports(_ reports: [PrivateStreamCounterpartyIntakeReport]) {
receiveReports = reports
}

func snapshot() -> PaymentRequestSdkSnapshot {
PaymentRequestSdkSnapshot(
uploadCount: uploadCount,
Expand Down Expand Up @@ -3790,6 +3816,12 @@ private actor PaykitSubscriptionNotificationCenterMock: PaykitSubscriptionNotifi
}
}

private final class PaymentRequestIntakeError: PrivateOperationError, @unchecked Sendable {
override func redactedContext() -> String {
"transport failure"
}
}

@MainActor
private func waitUntil(
timeout: Duration = .seconds(2),
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/769.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Incoming payment requests are checked every ten seconds while the app is open and online.
Loading