diff --git a/Bitkit/Services/PaykitPaymentRequestService.swift b/Bitkit/Services/PaykitPaymentRequestService.swift index 3a2d51ebb..4a319d888 100644 --- a/Bitkit/Services/PaykitPaymentRequestService.swift +++ b/Bitkit/Services/PaykitPaymentRequestService.swift @@ -1755,7 +1755,9 @@ final class PaykitPaymentRequestManager { let actionGeneration = stateGeneration let savedPublicKeysSnapshot = savedPublicKeys isCreatingRequest = true + let review = paymentActivity.beginReview() defer { + paymentActivity.endReview(review) if actionGeneration == stateGeneration { isCreatingRequest = false } diff --git a/BitkitTests/PaykitPaymentRequestServiceTests.swift b/BitkitTests/PaykitPaymentRequestServiceTests.swift index 669b7d23c..53366f29e 100644 --- a/BitkitTests/PaykitPaymentRequestServiceTests.swift +++ b/BitkitTests/PaykitPaymentRequestServiceTests.swift @@ -9285,23 +9285,39 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { role: .payee, expiresAt: timestamp(expiresAt) )) - let manager = paymentRequestManager(sdk: sdk) + let activity = PaykitPaymentActivity() + let manager = paymentRequestManager(sdk: sdk, paymentActivity: activity) await manager.refreshEligibleTargets(savedPublicKeys: [publicKey]) let target = try XCTUnwrap(manager.eligibleTargets.first) let wasPaused = await PrivatePaykitService.shared.isBackgroundWorkPaused addTeardownBlock { await PrivatePaykitService.shared.setBackgroundWorkPaused(wasPaused) } + await sdk.pauseNextLinkedPeers() await sdk.pauseNextProposal() + await sdk.pauseNextProcess() let proposal = Task { try await manager.propose( PaykitPaymentRequestDraft(amount: PaykitAmount(asset: .btc, atomic: 1), note: "Coffee", expiresAt: expiresAt), to: target ) } + try await waitUntil { await sdk.linkedPeersIsPaused() } + XCTAssertTrue(activity.shouldDeferRefresh) + XCTAssertFalse(activity.isActive) + await sdk.resumeLinkedPeers() try await waitUntil { await sdk.proposalIsPaused() } + XCTAssertTrue(activity.shouldDeferRefresh) await PrivatePaykitService.shared.setBackgroundWorkPaused(true) await sdk.resumeProposal() + try await waitUntil { await sdk.processIsPaused() } + XCTAssertTrue(activity.shouldDeferRefresh) + let otherReview = activity.beginReview() + await sdk.resumeProcess() let request = try await proposal.value + XCTAssertTrue(activity.shouldDeferRefresh) + activity.endReview(otherReview) + XCTAssertFalse(activity.shouldDeferRefresh) + XCTAssertFalse(manager.isCreatingRequest) XCTAssertEqual(request.amount.atomic, 1) XCTAssertEqual(request.note, "Coffee") @@ -9326,6 +9342,47 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { XCTAssertEqual(priorities["delivery"], [.interactive]) } + func testCancelledProposalReleasesReviewAndAllowsAnotherCreation() async throws { + let publicKey = "pubky\(String(repeating: "y", count: 52))" + let expiresAt = Date(timeIntervalSince1970: 1_900_000_000) + let sdk = PaymentRequestSdkMock(records: []) + await sdk.configureRecipients( + peers: [linkedPeer(counterparty: publicKey, state: .linked)], + requestCapabilitiesByPublicKey: [publicKey: true] + ) + try await sdk.setProposalResult(paymentRequestRecord( + id: "outgoing", counterparty: publicKey, role: .payee, expiresAt: timestamp(expiresAt) + )) + let activity = PaykitPaymentActivity() + let manager = paymentRequestManager(sdk: sdk, paymentActivity: activity) + await manager.refreshEligibleTargets(savedPublicKeys: [publicKey]) + let target = try XCTUnwrap(manager.eligibleTargets.first) + let draft = PaykitPaymentRequestDraft(amount: PaykitAmount(asset: .btc, atomic: 1), note: "", expiresAt: expiresAt) + await sdk.pauseNextLinkedPeers() + let proposal = Task { try await manager.propose(draft, to: target) } + try await waitUntil { await sdk.linkedPeersIsPaused() } + XCTAssertTrue(activity.shouldDeferRefresh) + + proposal.cancel() + await sdk.resumeLinkedPeers() + do { + _ = try await proposal.value + XCTFail("Expected cancelled recipient discovery") + } catch is CancellationError {} + XCTAssertFalse(activity.shouldDeferRefresh) + XCTAssertFalse(manager.isCreatingRequest) + let cancelledSnapshot = await sdk.snapshot() + XCTAssertTrue(cancelledSnapshot.proposedRequests.isEmpty) + XCTAssertTrue(cancelledSnapshot.processedCounterparties.isEmpty) + + let request = try await manager.propose(draft, to: target) + XCTAssertEqual(request.paymentRequestId, "outgoing") + XCTAssertFalse(activity.shouldDeferRefresh) + let snapshot = await sdk.snapshot() + XCTAssertEqual(snapshot.proposedRequests.count, 1) + XCTAssertEqual(snapshot.processedCounterparties, [publicKey]) + } + func testProposalsOnlyInspectAndDrainSelectedSavedTarget() async throws { let publicKey = "pubky\(String(repeating: "y", count: 52))" let unrelatedKey = "pubky\(String(repeating: "a", count: 52))" @@ -9491,7 +9548,8 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { id: "outgoing", counterparty: publicKey, role: .payee, proposalOutboundMessageId: 7 )) await sdk.setPaymentRequestListError(error) - let manager = paymentRequestManager(sdk: sdk) + let activity = PaykitPaymentActivity() + let manager = paymentRequestManager(sdk: sdk, paymentActivity: activity) await manager.refreshEligibleTargets(savedPublicKeys: [publicKey]) let request = try await manager.propose( @@ -9499,6 +9557,8 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { to: XCTUnwrap(manager.eligibleTargets.first) ) + XCTAssertFalse(activity.shouldDeferRefresh) + XCTAssertFalse(manager.isCreatingRequest) XCTAssertEqual(request.deliveryStatus, .queued) XCTAssertEqual(manager.outgoingRequests.map(\.paymentRequestId), ["outgoing"]) let snapshot = await sdk.snapshot() @@ -9615,7 +9675,8 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { } else { await sdk.failNextProcess() } - let manager = paymentRequestManager(sdk: sdk) + let activity = PaykitPaymentActivity() + let manager = paymentRequestManager(sdk: sdk, paymentActivity: activity) await manager.refreshEligibleTargets(savedPublicKeys: [publicKey]) let request = try await manager.propose( @@ -9623,6 +9684,8 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { to: XCTUnwrap(manager.eligibleTargets.first) ) + XCTAssertFalse(activity.shouldDeferRefresh) + XCTAssertFalse(manager.isCreatingRequest) XCTAssertEqual(request.deliveryStatus, .queued) XCTAssertEqual(manager.outgoingRequests.map(\.paymentRequestId), ["outgoing"]) let snapshot = await sdk.snapshot() @@ -9647,7 +9710,8 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { role: .payee, expiresAt: timestamp(expiresAt) )) - let manager = paymentRequestManager(sdk: sdk) + let activity = PaykitPaymentActivity() + let manager = paymentRequestManager(sdk: sdk, paymentActivity: activity) await manager.refreshEligibleTargets(savedPublicKeys: [publicKey]) let target = try XCTUnwrap(manager.eligibleTargets.first) @@ -9662,6 +9726,8 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { } catch let error as PaykitPaymentRequestError { XCTAssertEqual(error, .requestUnavailable) } + XCTAssertFalse(activity.shouldDeferRefresh) + XCTAssertFalse(manager.isCreatingRequest) let snapshot = await sdk.snapshot() XCTAssertTrue(snapshot.proposedRequests.isEmpty) } @@ -9706,7 +9772,8 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { expiresAt: timestamp(expiresAt) )) await sdk.pauseNextProposal() - let manager = paymentRequestManager(sdk: sdk) + let activity = PaykitPaymentActivity() + let manager = paymentRequestManager(sdk: sdk, paymentActivity: activity) await manager.refreshEligibleTargets(savedPublicKeys: [publicKey]) let target = try XCTUnwrap(manager.eligibleTargets.first) @@ -9717,10 +9784,12 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { ) } try await waitUntil { await sdk.proposalIsPaused() } + XCTAssertTrue(activity.shouldDeferRefresh) manager.clear() await sdk.resumeProposal() let committedRequest = try await task.value + XCTAssertFalse(activity.shouldDeferRefresh) XCTAssertEqual(committedRequest.paymentRequestId, "outgoing") XCTAssertTrue(manager.outgoingRequests.isEmpty) XCTAssertTrue(manager.eligibleTargets.isEmpty) @@ -9741,7 +9810,8 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { expiresAt: timestamp(expiresAt) )) await sdk.pauseNextProposal() - let manager = paymentRequestManager(sdk: sdk) + let activity = PaykitPaymentActivity() + let manager = paymentRequestManager(sdk: sdk, paymentActivity: activity) await manager.refreshEligibleTargets(savedPublicKeys: [publicKey]) let target = try XCTUnwrap(manager.eligibleTargets.first) @@ -9752,6 +9822,7 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { ) } try await waitUntil { await sdk.proposalIsPaused() } + XCTAssertTrue(activity.shouldDeferRefresh) await sdk.setActiveIdentity("pubky\(String(repeating: "a", count: 52))") await sdk.resumeProposal() @@ -9761,6 +9832,8 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { } catch let error as PaykitPaymentRequestError { XCTAssertEqual(error, .requestUnavailable) } + XCTAssertFalse(activity.shouldDeferRefresh) + XCTAssertFalse(manager.isCreatingRequest) let snapshot = await sdk.snapshot() XCTAssertTrue(snapshot.proposedRequests.isEmpty) XCTAssertTrue(manager.outgoingRequests.isEmpty)