diff --git a/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt b/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt index 80a3dd5316..15ce9920cb 100644 --- a/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt @@ -574,10 +574,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 @@ -599,13 +605,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 71e1b39188..e6063708ef 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1154,6 +1154,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 { @@ -1186,7 +1187,9 @@ class AppViewModel @Inject constructor( // held from presentation until the allowances are refreshed too. val releaseCoverage = paykitAllowanceRepo.beginCoverageUpdate() val result = try { - val refreshed = if (forceFresh) { + val refreshed = if (forceFresh && refreshPriority != null) { + paykitPaymentRequestRepo.refreshAfterStateChange(mode, refreshPriority) + } else if (forceFresh) { paykitPaymentRequestRepo.refreshAfterStateChange(mode) } else { paykitPaymentRequestRepo.refresh(mode, messagePriority) @@ -1540,6 +1543,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 3c1481a844..9ef178d7ba 100644 --- a/app/src/test/java/to/bitkit/repositories/PaykitPaymentRequestRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PaykitPaymentRequestRepoTest.kt @@ -408,33 +408,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 @@ -517,7 +526,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) @@ -545,8 +554,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) } @@ -558,7 +567,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 { @@ -567,12 +576,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 88fcf23a62..790eda25dd 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -758,6 +758,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)) @@ -11133,10 +11134,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 @@ -11150,18 +11154,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() } @@ -11176,19 +11183,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" @@ -11238,7 +11296,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/1639.fixed.md b/changelog.d/next/1639.fixed.md new file mode 100644 index 0000000000..7585296c71 --- /dev/null +++ b/changelog.d/next/1639.fixed.md @@ -0,0 +1 @@ +Background payment-request updates now give priority to payment review and submission.