Skip to content

fix: keep incoming request refreshes together - #1645

Open
ben-kaufman wants to merge 1 commit into
fix/paykit-proof-refresh-displayfrom
codex/paykit-inbox-observation
Open

ben-kaufman wants to merge 1 commit into
fix/paykit-proof-refresh-displayfrom
codex/paykit-inbox-observation

Conversation

@ben-kaufman

Copy link
Copy Markdown
Contributor

Incoming requests can wait behind unrelated publishing after the inbox has received them. This PR keeps receipt and fresh request reads in one queued operation.

Stacked on #1639. Related to #1407 and #1419; native timing comparisons are still in progress.

Description

  • Keep the receive and fresh-read pair together for automatic Background refreshes, after the existing pending-message drain when needed.
  • Keep automatic contact-related forced refreshes at Background priority so payment review can defer them while retaining fresh-read bookkeeping and later polling.
  • Preserve receive backup accounting and intake diagnostics when the later read fails or yields to a payment review, cancellation, identity change, or wallet wipe.

Out of Scope

None.

Design

N/A — no UI changes.

Preview

Native recording pending; no layout changes.

QA Notes

Journeys

  • updated automatic-presentation.xml — foreground resume receives each request once and keeps the current sheet intact.
  • updated review-refresh.xml — a second request arriving during review leaves the first unchanged and becomes available after cancellation and ordinary synchronization.

Manual Tests

  • regression: delay a saved-contact callback until Payment Request confirmation is open → release it, cancel the unpaid review, and allow normal foreground polling → the original review stays intact and the latest request becomes available without an outgoing payment — controlled saved-contact callback timing not in Capabilities.

Automated Checks

  • updated PaykitSdkServiceTest.kt — verifies receive/read ordering, receive-only backup accounting, cancellation, review and wipe admission, and no replay.
  • updated PaykitPaymentRequestRepoTest.kt — keeps the pending drain before the pair, preserves intake errors and warm requests, and excludes forced or explicit refreshes.
  • updated AppViewModelSendFlowTest.kt — covers contact and endpoint refresh events, typed deferral, and recovery through the next 10-second inbox poll.
  • updated PaykitPaymentRequestRepoSubscriptionTest.kt — adapts the existing subscription fixture to the composed read without changing its assertions.
  • ran a separate-operation negative control — the unchanged ordering regression fails because queued maintenance runs between receive and observation; exact production source was restored afterward.

Final source passed required compile, all 4,432 unit tests, Detekt/format and Android Lint, plus 769 affected caller, repository and SDK tests. Complete diagnostics were inspected; retained parent CloseGuard stack blocks have different ordering and suite attribution. Native comparison remains pending; these tests establish ordering and safeguards, not a measured speedup.

@ben-kaufman
ben-kaufman marked this pull request as ready for review October 21, 2026 01:56
@ben-kaufman
ben-kaufman requested a review from jvsena42 October 21, 2026 01:56
@greptile-apps

greptile-apps Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

Keeps automatic incoming-request receipt and the following fresh read inside one queued SDK operation.

  • Automatic inbox refreshes receive messages and read state together.
  • Contact-triggered request refreshes yield to payment review.

No actionable issues found. Tests were inspected, not run during this review.

Diagram

sequenceDiagram
    participant App as Automatic refresh
    participant Repo as Request repository
    participant SDK as SDK service
    participant Queue as Other queued work
    App->>Repo: Refresh inbox or full state
    opt Full refresh
        Repo->>SDK: Drain pending messages
    end
    Repo->>SDK: Receive and observe together
    activate SDK
    SDK->>SDK: Check admission and identity
    SDK->>SDK: Receive messages and track backup changes
    SDK->>SDK: Report intake failures
    SDK->>SDK: Recheck admission, then read state
    SDK-->>Repo: Fresh requests and linked peers
    deactivate SDK
    Queue->>SDK: Next queued operation
    Repo->>Repo: Check identity, proof, and action versions
    Repo-->>App: Publish current requests
Loading

Reviews (1) · Last reviewed commit: "fix: keep incoming request refreshes tog..." · Reviewed by Greptile

@ovitrif ovitrif added this to the 3.2.0 milestone Oct 21, 2026
@ovitrif

ovitrif commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Milestone 3.2.0: this is Paykit request latency work, the same track as #1407 (3.2.0), and it is stacked on the draft #1639. It is not a 3.1.0 fix.

@talosmachina talosmachina left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No findings. Runs the automatic Background inbox receive and the fresh observeState read under one operationLock hold, and moves the two contact-driven forced refreshes to Background priority so a payment review defers them. Reviewed 8564ba2, full tier, against the stack base ce4db93 (#1639).

What I checked, and 6 candidates I ruled out

Read in full: the diffs of PaykitSdkService.kt, PaykitPaymentRequestRepo.kt, AppViewModel.kt, plus PaykitSdkOperationLock.kt, withPaykitBackupStateTracking, refreshPaykitKey, the old observeState and receivePrivateMessagesFromLinkedPeers
Call sites traced: refresh(mode, forceFresh, messagePriority, priority) and every refreshIncomingPaykitPaymentRequests caller in AppViewModel, to confirm only non-forced refreshes with Background on both priorities take the combined path
Not built here: Bitkit Android branches are not built on this box, so this is reasoned from the code and the tests in the diff. At review time build, build-local and lint were still pending

Ruled out

  • Lock dropped between receive and observe: withoutLock inside withLock only re-checks wipe admission, it never releases the held lock, so queued work cannot run between the two calls.
  • Weaker identity check than the old observeState: the old path matched the observation against cachedPaykitKey; the new one matches key.publicKey and observation.publicKey against expectedIdentity, compares keyGeneration, and checks cachedPaykitKey == key before observing, which is equivalent.
  • Wipe overtaking the held lock: withWalletWipe bumps the generation before it waits, so the withoutLock admission before observe throws the wipe error.
  • Review opening after receive loses the request: observe throws OperationInProgress, completedRefreshGeneration was already reset to -1 before the fetch, so the next poll is not skipped as fresh, and the received messages are already in SDK state.
  • Cancellation after receive skews backup accounting: completeSdkCall fails after the SDK call returned, the tracking finally sees succeeded = false and marks the backup dirty once, as on the old path.
  • Deferred contact-sync refresh drops presentation: on OperationInProgress the contact-sync and endpoint paths still run refreshPaymentRequestTargets(force = true), and the next 10 s INBOX poll presents through presentNextIncomingPaykitPaymentRequest, covered by automatic contact refresh defers without losing the next poll.

Merge confidence: 4/5, not built here and CI build and lint still pending; the ordering is covered by PaykitSdkServiceTest.kt and PaykitPaymentRequestRepoTest.kt.

@github-actions

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 8564ba2 (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ✅ Approve

Findings: None.

QA for 8564ba2: Two Android API 35 wallets on staging Pubky and regtest. Videos run at 2× speed, with payer left and requester right.

Test 1: automatic payment request presentation ✅ Preparation cancellation, retry, sheet ownership and insufficient-funds rejection passed on 8564ba2 plus a timing hook.

I cancelled the 21,000-sat request while preparation was held, then released it: it stayed pending without reopening. Explicit retry showed the same disabled preparation sheet, then became ready in place. Receive retained ownership while a 5,000-sat request arrived; closing it presented that request. The drawer also retained ownership. Pending rows stayed unique, and opening/closing Receive did not reopen reviewed requests. A 1,000,000-sat request failed with insufficient Savings on both automatic presentation and explicit retry, without enabling payment or sending funds.

J1.2x.mp4
Test hook and deciding logs

A debug-only gate paused incoming preparation before real native resolution until explicit release. The callback hook present in this binary remained unarmed. The patch was not pushed. SDK results were not mocked.

diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt
index 2f2b47d66..e6452d208 100644
--- a/app/src/main/java/to/bitkit/ui/MainActivity.kt
+++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt
@@ -288,6 +288,10 @@ class MainActivity : FragmentActivity() {
     }
 
     private fun handleLaunchIntent(intent: Intent) {
+        if (to.bitkit.BuildConfig.DEBUG && intent.getBooleanExtra("review_hook_refresh", false)) { // test hook
+            intent.removeExtra("review_hook_refresh") // test hook
+            appViewModel.reviewHookRefreshContacts() // test hook
+        } // test hook
         if (intent.getBooleanExtra(EXTRA_PAYKIT_SUBSCRIPTION_PAYMENT_DUE, false)) {
             intent.removeExtra(EXTRA_PAYKIT_SUBSCRIPTION_PAYMENT_DUE)
             appViewModel.onPaykitSubscriptionNotificationTapped(
diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
index 9655350fd..b439e8c3b 100644
--- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
+++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@@ -914,6 +914,10 @@ class AppViewModel @Inject constructor(
         }
     }
 
+    fun reviewHookRefreshContacts() { // test hook
+        if (BuildConfig.DEBUG) viewModelScope.launch { pubkyRepo.refreshContacts() } // test hook
+    } // test hook
+
     private suspend fun synchronizePaykitContacts(state: PaykitContactSyncState) {
         if (!state.isPaykitEnabled || state.publicKey == null) {
             isPaymentRequestIdentityActivating = true
@@ -943,6 +947,13 @@ class AppViewModel @Inject constructor(
             )
         }
 
+        if (BuildConfig.DEBUG && java.io.File(context.filesDir, "review-hook-arm").exists()) { // test hook
+            java.io.File(context.filesDir, "review-hook-arm").delete() // test hook
+            Logger.info("Held saved-contact callback before activation and request refresh", context = "ReviewHook") // test hook
+            while (!java.io.File(context.filesDir, "review-hook-release").exists()) delay(100) // test hook
+            java.io.File(context.filesDir, "review-hook-release").delete() // test hook
+            Logger.info("Released saved-contact callback", context = "ReviewHook") // test hook
+        } // test hook
         isPaymentRequestIdentityActivating = true
         val requestRefresh = try {
             paykitPaymentRequestRepo.activate(state.publicKey)
@@ -956,6 +967,7 @@ class AppViewModel @Inject constructor(
                 refreshPriority = Priority.Background,
                 onlyWhileActive = true,
             )
+            if (BuildConfig.DEBUG) Logger.info("Saved-contact refresh deferred='${result.exceptionOrNull() == PaykitPaymentRequestError.OperationInProgress}' pending='${paykitPaymentRequestRepo.pendingRequests.value.size}'", context = "ReviewHook") // test hook
             if (!isPaymentRequestPollingStopped.value) refreshPaymentRequestTargets(force = true)
             result
         } finally {
@@ -1600,6 +1612,13 @@ class AppViewModel @Inject constructor(
             }
             if (reopenRetainedHardwarePayment(request, generation, preparation)) return true
             val started = timeSource.markNow()
+            if (BuildConfig.DEBUG && java.io.File(context.filesDir, "prepare-hook-arm").exists()) { // test hook
+                java.io.File(context.filesDir, "prepare-hook-arm").delete() // test hook
+                Logger.info("Held incoming preparation before native resolution", context = "ReviewHook") // test hook
+                while (!java.io.File(context.filesDir, "prepare-hook-release").exists()) delay(100) // test hook
+                java.io.File(context.filesDir, "prepare-hook-release").delete() // test hook
+                Logger.info("Released incoming preparation", context = "ReviewHook") // test hook
+            } // test hook
             val presentationResult = privatePaykitRepo.beginPaymentRequest(request)
             val resolvedAfter = started.elapsedNow()
             val result = finishIncomingPaymentRequestPreparation(request, generation, preparation, presentationResult)
10-21 05:33:41.310  8856  8856 I APP     : 2026-10-21 03:33:41.307 INFO    [AppViewModel.kt:1617]               Held incoming preparation before native resolution - ReviewHook
10-21 05:33:57.714  8856  8856 I APP     : 2026-10-21 03:33:57.714 INFO    [AppViewModel.kt:1620]               Released incoming preparation - ReviewHook
10-21 05:34:30.090  8856  8856 I APP     : 2026-10-21 03:34:30.090 INFO    [AppViewModel.kt:1617]               Held incoming preparation before native resolution - ReviewHook
10-21 05:34:47.823  8856  8856 I APP     : 2026-10-21 03:34:47.823 INFO    [AppViewModel.kt:1620]               Released incoming preparation - ReviewHook
10-21 05:39:12.129  8856  8856 I APP     : 2026-10-21 03:39:12.129 INFO    [AppViewModel.kt:1617]               Held incoming preparation before native resolution - ReviewHook
10-21 05:39:30.474  8856  8856 I APP     : 2026-10-21 03:39:30.474 INFO    [AppViewModel.kt:1620]               Released incoming preparation - ReviewHook
Test 2: payment review refresh deferral ✅ The original 1,000-sat review survived a second request and cancellation, then paid exactly once on pristine 8564ba2.

The Savings confirmation and its 141-sat fee stayed intact for over 30 seconds and while a second 2,000-sat request arrived. I swiped, waited at payment PIN for over 30 seconds, then cancelled PIN and closed the sheet: Activity still showed only the funding credit. Ordinary foreground polling exposed the second request without restarting. I reopened the first, backgrounded/resumed and unlocked, verified the original amount and fee, then authorized one payment. “Bitcoin Sent” appeared; Activity contained one outgoing payment. The requester received 1,000 sats, and the original request recorded “Proof submitted.” The second remained “Waiting for payment.”

Both devices' LDK logs identify the same transaction, e0a32671329b7f7a9fb4202bfcdc6191b0d86d822e3268e1cbd72c0f7d15bbf7: recipient output 1,000 sats, change 98,859 sats and fee 141 sats. No hook was installed for this case.

⚠️ Ordinary synchronization took approximately 32 seconds after closing review. This run establishes recovery and preserved payment terms; it does not establish a latency improvement against the base.

J2.2x.mp4
Test 3: delayed saved-contact callback during review ✅ The actual Background refresh deferred, retained the original review, and later polling exposed the new request on 8564ba2 plus a callback hook.

With two warm pending rows, I held the saved-contact callback before activation and opened the original 21,000-sat confirmation. The requester sent a new 3,000-sat request. Releasing the callback left the original amount and ready swipe intact; the log confirmed the actual refresh returned the typed OperationInProgress deferral with both warm rows retained. After closing review unpaid, ordinary foreground synchronization presented the new 3,000-sat request without restarting. All three pending amounts remained correct; balance and Activity showed no outgoing payment.

M1.2x.mp4
Test hook and deciding logs

A debug-only entry point invoked the real saved-contact callback with the current repository snapshot and held it before activation until explicit release. The preparation gate remained unarmed. The patch was not pushed. SDK results were not mocked.

diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt
index 2f2b47d66..e6452d208 100644
--- a/app/src/main/java/to/bitkit/ui/MainActivity.kt
+++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt
@@ -288,6 +288,10 @@ class MainActivity : FragmentActivity() {
     }
 
     private fun handleLaunchIntent(intent: Intent) {
+        if (to.bitkit.BuildConfig.DEBUG && intent.getBooleanExtra("review_hook_refresh", false)) { // test hook
+            intent.removeExtra("review_hook_refresh") // test hook
+            appViewModel.reviewHookRefreshContacts() // test hook
+        } // test hook
         if (intent.getBooleanExtra(EXTRA_PAYKIT_SUBSCRIPTION_PAYMENT_DUE, false)) {
             intent.removeExtra(EXTRA_PAYKIT_SUBSCRIPTION_PAYMENT_DUE)
             appViewModel.onPaykitSubscriptionNotificationTapped(
diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
index 9655350fd..01a4b4b1b 100644
--- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
+++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@@ -914,6 +914,17 @@ class AppViewModel @Inject constructor(
         }
     }
 
+    fun reviewHookRefreshContacts() { // test hook
+        if (BuildConfig.DEBUG) viewModelScope.launch { // test hook
+            synchronizePaykitContacts(PaykitContactSyncState( // test hook
+                publicKey = pubkyRepo.publicKey.value, // test hook
+                contactKeys = pubkyRepo.contacts.value.map { it.publicKey }.toSet(), // test hook
+                contactsLoaded = pubkyRepo.contactsLoadVersion.value > 0L, // test hook
+                isPaykitEnabled = isPaykitEnabled.value, // test hook
+            )) // test hook
+        } // test hook
+    } // test hook
+
     private suspend fun synchronizePaykitContacts(state: PaykitContactSyncState) {
         if (!state.isPaykitEnabled || state.publicKey == null) {
             isPaymentRequestIdentityActivating = true
@@ -943,6 +954,13 @@ class AppViewModel @Inject constructor(
             )
         }
 
+        if (BuildConfig.DEBUG && java.io.File(context.filesDir, "review-hook-arm").exists()) { // test hook
+            java.io.File(context.filesDir, "review-hook-arm").delete() // test hook
+            Logger.info("Held saved-contact callback before activation and request refresh", context = "ReviewHook") // test hook
+            while (!java.io.File(context.filesDir, "review-hook-release").exists()) delay(100) // test hook
+            java.io.File(context.filesDir, "review-hook-release").delete() // test hook
+            Logger.info("Released saved-contact callback", context = "ReviewHook") // test hook
+        } // test hook
         isPaymentRequestIdentityActivating = true
         val requestRefresh = try {
             paykitPaymentRequestRepo.activate(state.publicKey)
@@ -956,6 +974,7 @@ class AppViewModel @Inject constructor(
                 refreshPriority = Priority.Background,
                 onlyWhileActive = true,
             )
+            if (BuildConfig.DEBUG) Logger.info("Saved-contact refresh deferred='${result.exceptionOrNull() == PaykitPaymentRequestError.OperationInProgress}' pending='${paykitPaymentRequestRepo.pendingRequests.value.size}'", context = "ReviewHook") // test hook
             if (!isPaymentRequestPollingStopped.value) refreshPaymentRequestTargets(force = true)
             result
         } finally {
@@ -1600,6 +1619,13 @@ class AppViewModel @Inject constructor(
             }
             if (reopenRetainedHardwarePayment(request, generation, preparation)) return true
             val started = timeSource.markNow()
+            if (BuildConfig.DEBUG && java.io.File(context.filesDir, "prepare-hook-arm").exists()) { // test hook
+                java.io.File(context.filesDir, "prepare-hook-arm").delete() // test hook
+                Logger.info("Held incoming preparation before native resolution", context = "ReviewHook") // test hook
+                while (!java.io.File(context.filesDir, "prepare-hook-release").exists()) delay(100) // test hook
+                java.io.File(context.filesDir, "prepare-hook-release").delete() // test hook
+                Logger.info("Released incoming preparation", context = "ReviewHook") // test hook
+            } // test hook
             val presentationResult = privatePaykitRepo.beginPaymentRequest(request)
             val resolvedAfter = started.elapsedNow()
             val result = finishIncomingPaymentRequestPreparation(request, generation, preparation, presentationResult)
10-21 05:47:30.374 10711 10711 I APP     : 2026-10-21 03:47:30.373 INFO    [AppViewModel.kt:959]                Held saved-contact callback before activation and request refresh - ReviewHook
10-21 05:48:53.697 10711 10711 I APP     : 2026-10-21 03:48:53.697 INFO    [AppViewModel.kt:962]                Released saved-contact callback - ReviewHook
10-21 05:48:54.013 10711 10711 I APP     : 2026-10-21 03:48:54.013 INFO    [AppViewModel.kt:977]                Saved-contact refresh deferred='true' pending='2' - ReviewHook
Test 4: pristine build and focused regressions ✅ The unmodified head built and all four focused suites passed, checked from Gradle and JUnit output.

assembleDevDebug and the complete PaykitSdkServiceTest, PaykitPaymentRequestRepoTest, PaykitPaymentRequestRepoSubscriptionTest and AppViewModelSendFlowTest suites passed with no failures, errors or skipped tests. These cover the receive/read lock boundary, receive-only backup accounting, cancellation, review/wipe/identity admission, pending-drain ordering, intake errors and polling recovery. The installed pristine APK's SHA-256 matched the build used for this check.

I reviewed the full ce4db93…8564ba2 range, including all changed production code, tests and journeys and the reached lock/admission helpers. No planned case was left unrun. Wipe, identity-change and cancellation interleavings were checked through source and automated tests rather than native fault injection; no base/head latency benchmark was run. Prior source report: Talos review.

Twin: synonymdev/bitkit-ios#1070 fix: keep Paykit inbox observation together at b1d65a7 matches the receive/read composition, Background contact refresh deferral and updated journeys in source. iOS was not device-tested in this Android review.


Reviewed by gpt-6.1-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest

@ovitrif ovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utAck

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA review

Scope: Full review of the PR diff against merge base ce4db93, at 8564ba2. This covers only the commits on this pull request, not the stacked parent #1639.

No new actionable code findings.

Automatic Background inbox refreshes receive linked-peer messages and read fresh request state under one operation-lock hold, after the existing full-refresh pending drain. Contact-sync and private-endpoint refreshes stay forced, but run at Background priority so an open payment review defers them; the next inbox poll still does the combined read. A deferred or failed read leaves the previous request list in place, and backup accounting stays tied to receive. The same production contract matches bitkit-ios#1070 at b1d65a7 for that composition and deferral only.

Validation: source and unit tests were inspected, not executed here. CI build and lint succeeded on this head.

Device testing: not performed in this review. A separate review reports Android runs on this head; those runs were not re-executed here.

Ready for device testing.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code scan at 8564ba2: no issues found. Reviewed this PR's own commit on top of #1639 (ce4db93). Reviewed as a Paykit sync change.

  • No deadlock: the SDK lock isn't reentrant, and nothing inside the combined receive+observe hold takes it again.
  • Stale-state checks: wipe, runtime and identity generations are checked before the lock, before receive and before observe. The key generation is checked against the observation, which is stricter than the old observe path.
  • Narrow scope: only non-forced Background refreshes use the combined path. Forced and explicit refreshes keep the separate calls.
  • Partial failure: if observe defers or fails after a successful receive, nothing is lost or shown twice. The refresh generation was already reset, so the next inbox poll still runs.
  • No payment path touched.
  • Parity: this side moves contact sync and endpoint refresh to Background. iOS moves one, because it has no forced refresh after endpoint refresh. See synonymdev/bitkit-ios#1070.

Device gate (partial): 8564ba2 — not run yet. Pending:

  • automatic-presentation.xml and review-refresh.xml;
  • time how long an incoming request takes to appear while a background publication is queued;
  • re-check the resync time after closing review.

Device results will follow in a separate review.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at 8564ba2a1a96e4d1ac56c4b084143f619968d48a based on the current code review and recorded current-head device and regression evidence: presentation ownership, cancellation, delayed callback deferral, polling recovery and one confirmed payment. Rechecked head/base, CI, E2E and review threads before approval; no new blocking signal.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants