From 54610cc7b5fb89ba1b84b674d34cc76c01a0a8ad Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 20 Oct 2026 17:24:46 -0600 Subject: [PATCH 1/2] fix: defer passive proof display refreshes --- .../repositories/PaykitPaymentRequestRepo.kt | 9 +- .../java/to/bitkit/viewmodels/AppViewModel.kt | 6 +- .../PaykitPaymentRequestRepoTest.kt | 82 +++++++++++-------- .../viewmodels/AppViewModelSendFlowTest.kt | 79 ++++++++++++++++-- .../next/proof-display-admission.fixed.md | 1 + 5 files changed, 132 insertions(+), 45 deletions(-) create mode 100644 changelog.d/next/proof-display-admission.fixed.md diff --git a/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt b/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt index c08d84c6a5..b6dc749756 100644 --- a/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt @@ -564,10 +564,16 @@ class PaykitPaymentRequestRepo @Inject constructor( mode: PaykitPaymentRequestRefreshMode = PaykitPaymentRequestRefreshMode.FULL, ): Result = refresh(mode, forceFresh = true) + internal suspend fun refreshAfterStateChange( + mode: PaykitPaymentRequestRefreshMode, + priority: Priority, + ): Result = refresh(mode, forceFresh = true, priority = priority) + private suspend fun refresh( mode: PaykitPaymentRequestRefreshMode, forceFresh: Boolean, messagePriority: Priority = Priority.Background, + priority: Priority = if (forceFresh) Priority.Ordered else Priority.Background, ): Result { val generation = stateGeneration.get() val expectedIdentity = activeIdentity @@ -589,13 +595,12 @@ class PaykitPaymentRequestRepo @Inject constructor( completedRefreshGeneration = -1L RefreshCheckpoint(actionVersion, proofVersion) } ?: return@refresh - val priority = if (forceFresh) Priority.Ordered else Priority.Background val snapshot = runSuspendCatching { fetchRequestSnapshot( expectedIdentity, mode, priority, - if (forceFresh) Priority.Ordered else messagePriority, + if (forceFresh) priority else messagePriority, observationOnly = true, ) } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index d4ffe470ba..8c3e702d35 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1141,6 +1141,7 @@ class AppViewModel @Inject constructor( mode: PaykitPaymentRequestRefreshMode = PaykitPaymentRequestRefreshMode.FULL, forceFresh: Boolean = false, messagePriority: Priority = Priority.Background, + refreshPriority: Priority? = null, onlyWhileActive: Boolean = false, reconcileProofs: Boolean = mode == PaykitPaymentRequestRefreshMode.FULL, ): Result { @@ -1169,7 +1170,9 @@ class AppViewModel @Inject constructor( ) { return Result.failure(PaykitPaymentRequestError.RequestUnavailable) } - val result = if (forceFresh) { + val result = if (forceFresh && refreshPriority != null) { + paykitPaymentRequestRepo.refreshAfterStateChange(mode, refreshPriority) + } else if (forceFresh) { paykitPaymentRequestRepo.refreshAfterStateChange(mode) } else { paykitPaymentRequestRepo.refresh(mode, messagePriority) @@ -1428,6 +1431,7 @@ class AppViewModel @Inject constructor( refreshIncomingPaykitPaymentRequests( PaykitPaymentRequestRefreshMode.STORED, forceFresh = true, + refreshPriority = Priority.Background, onlyWhileActive = true, reconcileProofs = true, ) diff --git a/app/src/test/java/to/bitkit/repositories/PaykitPaymentRequestRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PaykitPaymentRequestRepoTest.kt index e337594f4f..50914dd245 100644 --- a/app/src/test/java/to/bitkit/repositories/PaykitPaymentRequestRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PaykitPaymentRequestRepoTest.kt @@ -404,33 +404,42 @@ class PaykitPaymentRequestRepoTest : BaseUnitTest(StandardTestDispatcher()) { @Test fun `warm request observations remain readable while refresh waits and clear on identity change`() = test { - val record = paymentRequestRecord() - whenever(paykitSdkService.allPaymentRequests(anyOrNull())).thenReturn(listOf(record)) - sut.refresh(PaykitPaymentRequestRefreshMode.STORED).getOrThrow() - val pending = sut.pendingRequests.value - val history = sut.paymentRequestHistory.value - assertTrue(pending.isNotEmpty()) - val reading = CompletableDeferred() - val resume = CompletableDeferred() - whenever(paykitSdkService.allPaymentRequests(anyOrNull())).doSuspendableAnswer { - reading.complete(Unit) - resume.await() - listOf(record) + for (forceFresh in listOf(false, true)) { + sut.activate(LOCAL_IDENTITY) + val record = paymentRequestRecord() + whenever(paykitSdkService.allPaymentRequests(anyOrNull())).thenReturn(listOf(record)) + sut.refresh(PaykitPaymentRequestRefreshMode.STORED).getOrThrow() + val pending = sut.pendingRequests.value + val history = sut.paymentRequestHistory.value + assertTrue(pending.isNotEmpty()) + val reading = CompletableDeferred() + val resume = CompletableDeferred() + whenever(paykitSdkService.allPaymentRequests(anyOrNull())).doSuspendableAnswer { + reading.complete(Unit) + resume.await() + listOf(record) + } + val refresh = async { + if (forceFresh) { + sut.refreshAfterStateChange(PaykitPaymentRequestRefreshMode.STORED, Priority.Background) + } else { + sut.refresh(PaykitPaymentRequestRefreshMode.STORED) + } + } + reading.await() + val started = testScheduler.currentTime + assertEquals(pending, sut.pendingRequests.value) + assertEquals(history, sut.paymentRequestHistory.value) + assertEquals(0L, testScheduler.currentTime - started) + assertFalse(refresh.isCompleted) + sut.activate(SECOND_IDENTITY) + assertTrue(sut.pendingRequests.value.isEmpty()) + assertTrue(sut.paymentRequestHistory.value.isEmpty()) + resume.complete(Unit) + refresh.await().getOrThrow() + assertTrue(sut.pendingRequests.value.isEmpty()) + assertTrue(sut.paymentRequestHistory.value.isEmpty()) } - val refresh = async { sut.refresh(PaykitPaymentRequestRefreshMode.STORED) } - reading.await() - val started = testScheduler.currentTime - assertEquals(pending, sut.pendingRequests.value) - assertEquals(history, sut.paymentRequestHistory.value) - assertEquals(0L, testScheduler.currentTime - started) - assertFalse(refresh.isCompleted) - sut.activate(SECOND_IDENTITY) - assertTrue(sut.pendingRequests.value.isEmpty()) - assertTrue(sut.paymentRequestHistory.value.isEmpty()) - resume.complete(Unit) - refresh.await().getOrThrow() - assertTrue(sut.pendingRequests.value.isEmpty()) - assertTrue(sut.paymentRequestHistory.value.isEmpty()) } @Test @@ -513,7 +522,7 @@ class PaykitPaymentRequestRepoTest : BaseUnitTest(StandardTestDispatcher()) { @Test fun `checkpoint reload preserves refresh admission and filters local proofs on retry`() = test { - for (forceFresh in listOf(false, true)) { + for (priority in listOf(null, Priority.Ordered, Priority.Background)) { val record = paymentRequestRecord() val other = paymentRequestRecord(id = "another-request") val records = listOf(record, other) @@ -541,8 +550,8 @@ class PaykitPaymentRequestRepoTest : BaseUnitTest(StandardTestDispatcher()) { }.whenever(paykitSdkService).allPaymentRequests(anyOrNull(), any()) clearInvocations(paykitSdkService) val refresh = async { - if (forceFresh) { - sut.refreshAfterStateChange(PaykitPaymentRequestRefreshMode.STORED) + if (priority != null) { + sut.refreshAfterStateChange(PaykitPaymentRequestRefreshMode.STORED, priority) } else { sut.refresh(PaykitPaymentRequestRefreshMode.STORED) } @@ -554,7 +563,7 @@ class PaykitPaymentRequestRepoTest : BaseUnitTest(StandardTestDispatcher()) { resume.complete(Unit) val result = refresh.await() - if (forceFresh) { + if (priority == Priority.Ordered) { result.getOrThrow() assertEquals(listOf("another-request"), sut.pendingRequests.value.map { it.paymentRequestId }) } else { @@ -563,12 +572,19 @@ class PaykitPaymentRequestRepoTest : BaseUnitTest(StandardTestDispatcher()) { assertEquals(warmHistory, sut.paymentRequestHistory.value) verify(paykitSdkService, never()).linkedPeers(Priority.Background) } - val priority = if (forceFresh) Priority.Ordered else Priority.Background - verify(paykitSdkService).allPaymentRequests(LOCAL_IDENTITY, priority) + verify(paykitSdkService).allPaymentRequests(LOCAL_IDENTITY, priority ?: Priority.Background) paymentSubmissionActive.value = false + val latest = paymentRequestRecord(id = "new-request") + whenever(paykitSdkService.allPaymentRequests(anyOrNull())).thenReturn(records + latest) + proofStateVersion.value += 1 + clearInvocations(paykitSdkService) sut.refresh(PaykitPaymentRequestRefreshMode.STORED).getOrThrow() - assertEquals(listOf("another-request"), sut.pendingRequests.value.map { it.paymentRequestId }) + verify(paykitSdkService).observeState(LOCAL_IDENTITY, Priority.Background) + assertEquals( + listOf("another-request", "new-request"), + sut.pendingRequests.value.map { it.paymentRequestId }, + ) assertTrue(sut.paymentRequestHistory.value.any { it.id == requestId }) } } diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index f4c20b9361..52177e81f7 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -751,6 +751,7 @@ class AppViewModelSendFlowTest : BaseUnitTest() { whenever(paykitPaymentRequestRepo.isCreatingRequest).thenReturn(MutableStateFlow(false)) whenever { paykitPaymentRequestRepo.refreshEligibleTargets(any(), any()) }.thenReturn(Result.success(Unit)) whenever { paykitPaymentRequestRepo.refreshAfterStateChange(any()) }.thenReturn(Result.success(Unit)) + whenever { paykitPaymentRequestRepo.refreshAfterStateChange(any(), any()) }.thenReturn(Result.success(Unit)) whenever { paykitPaymentRequestRepo.isSubscriptionNotificationHandled(any(), any()) }.thenReturn(false) whenever { paykitPaymentRequestRepo.refresh(any(), any()) }.doSuspendableAnswer { paykitPaymentRequestRepo.refresh(it.getArgument(0)) @@ -10783,10 +10784,13 @@ class AppViewModelSendFlowTest : BaseUnitTest() { usdtStateVersion.value++ runCurrent() } - verify(paykitPaymentRequestRepo, never()).refreshAfterStateChange(any()) + verify(paykitPaymentRequestRepo, never()).refreshAfterStateChange(any(), any()) paymentSubmissionActive.value = false runCurrent() - verify(paykitPaymentRequestRepo).refreshAfterStateChange(PaykitPaymentRequestRefreshMode.STORED) + verify(paykitPaymentRequestRepo).refreshAfterStateChange( + PaykitPaymentRequestRefreshMode.STORED, + Priority.Background, + ) } @Test @@ -10800,18 +10804,21 @@ class AppViewModelSendFlowTest : BaseUnitTest() { proofStateVersion.value++ runCurrent() } - verify(paykitPaymentRequestRepo, never()).refreshAfterStateChange(any()) + verify(paykitPaymentRequestRepo, never()).refreshAfterStateChange(any(), any()) verify(paykitPaymentProofRepo, never()).reconcile() paymentSubmissionActive.value = false runCurrent() - verify(paykitPaymentRequestRepo).refreshAfterStateChange(PaykitPaymentRequestRefreshMode.STORED) + verify(paykitPaymentRequestRepo).refreshAfterStateChange( + PaykitPaymentRequestRefreshMode.STORED, + Priority.Background, + ) verify(paykitPaymentProofRepo).reconcile() paymentSubmissionActive.value = true runCurrent() paymentSubmissionActive.value = false runCurrent() - verify(paykitPaymentRequestRepo, times(1)).refreshAfterStateChange(any()) + verify(paykitPaymentRequestRepo, times(1)).refreshAfterStateChange(any(), any()) verify(paykitPaymentProofRepo, times(1)).reconcile() } @@ -10826,19 +10833,70 @@ class AppViewModelSendFlowTest : BaseUnitTest() { proofStateVersion.value++ runCurrent() } - verify(paykitPaymentRequestRepo, never()).refreshAfterStateChange(any()) + verify(paykitPaymentRequestRepo, never()).refreshAfterStateChange(any(), any()) sut.startPaykitPaymentRequestPolling() runCurrent() - verify(paykitPaymentRequestRepo).refreshAfterStateChange(PaykitPaymentRequestRefreshMode.STORED) + verify(paykitPaymentRequestRepo).refreshAfterStateChange( + PaykitPaymentRequestRefreshMode.STORED, + Priority.Background, + ) sut.stopPaykitPaymentRequestPolling() runCurrent() sut.startPaykitPaymentRequestPolling() runCurrent() - verify(paykitPaymentRequestRepo, times(1)).refreshAfterStateChange(any()) + verify(paykitPaymentRequestRepo, times(1)).refreshAfterStateChange(any(), any()) sut.stopPaykitPaymentRequestPolling() } + @Test + fun `proof display refresh defers after admitted reconciliation and recovers on home resume`() = test { + whenever(paykitPaymentRequestRepo.refresh(any())).thenReturn(Result.success(Unit)) + pubkyPublicKey.value = testPublicKey + enablePaykitUi() + runCurrent() + val reconciliation = CompletableDeferred() + var reconciled = false + whenever(paykitPaymentProofRepo.reconcile()).doSuspendableAnswer { + reconciliation.await() + reconciled = true + } + whenever( + paykitPaymentRequestRepo.refreshAfterStateChange( + PaykitPaymentRequestRefreshMode.STORED, + Priority.Background, + ), + ).doSuspendableAnswer { + assertTrue(reconciled) + assertTrue(paymentSubmissionActive.value) + Result.failure(PaykitPaymentRequestError.OperationInProgress) + } + clearInvocations(paykitPaymentRequestRepo, paykitPaymentProofRepo) + + proofStateVersion.value++ + runCurrent() + verify(paykitPaymentProofRepo).reconcile() + assertFalse(reconciled) + paymentSubmissionActive.value = true + reconciliation.complete(Unit) + runCurrent() + + assertTrue(reconciled) + verify(paykitPaymentRequestRepo).refreshAfterStateChange( + PaykitPaymentRequestRefreshMode.STORED, + Priority.Background, + ) + verify(paykitPaymentRequestRepo, never()).refreshAfterStateChange(any()) + paymentSubmissionActive.value = false + runCurrent() + verify(paykitPaymentRequestRepo, times(1)).refreshAfterStateChange(any(), any()) + + sut.onHomeResumed() + runCurrent() + verify(paykitPaymentRequestRepo).refresh(PaykitPaymentRequestRefreshMode.FULL, Priority.Background) + verify(paykitPaymentProofRepo, times(2)).reconcile() + } + @Test fun `paid recurring request refreshes subscription state immediately`() = test { val address = "bcrt1qrecurringpaymentrequest" @@ -10888,7 +10946,10 @@ class AppViewModelSendFlowTest : BaseUnitTest() { confirmCurrentPayment() completionStarted.await() - verify(paykitPaymentRequestRepo).refreshAfterStateChange(PaykitPaymentRequestRefreshMode.STORED) + verify(paykitPaymentRequestRepo).refreshAfterStateChange( + PaykitPaymentRequestRefreshMode.STORED, + Priority.Background, + ) verify(paykitPaymentProofRepo).reconcile() verify(paykitPaymentRequestRepo, never()).refresh(PaykitPaymentRequestRefreshMode.FULL) finishCompletion.complete(Unit) diff --git a/changelog.d/next/proof-display-admission.fixed.md b/changelog.d/next/proof-display-admission.fixed.md new file mode 100644 index 0000000000..7585296c71 --- /dev/null +++ b/changelog.d/next/proof-display-admission.fixed.md @@ -0,0 +1 @@ +Background payment-request updates now give priority to payment review and submission. From ce4db93f33790725cf21a1c1f6601156db7075fd Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 20 Oct 2026 17:27:54 -0600 Subject: [PATCH 2/2] chore: rename changelog fragment --- .../next/{proof-display-admission.fixed.md => 1639.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{proof-display-admission.fixed.md => 1639.fixed.md} (100%) diff --git a/changelog.d/next/proof-display-admission.fixed.md b/changelog.d/next/1639.fixed.md similarity index 100% rename from changelog.d/next/proof-display-admission.fixed.md rename to changelog.d/next/1639.fixed.md