Repository navigation
[Bug]: Payment request briefly reappears on Android after sending while recipient is backgrounded #1534
Description
What happened?
During manual RC2 testing, a payment request was created on iOS and paid from Android while the iOS app was closed/backgrounded. After the initial send, Android briefly showed what looked like a second payment request, then it disappeared after a moment without another user action.
The reporter explicitly confirmed that the transient second request appeared on Android, not on iOS after reopening. The exact surface (list entry versus request sheet) and duration were not captured. Treat this as an observed transient request presentation; a second request creation or duplicate payment has not been established.
Expected behavior
A request that has been handled should not briefly look like a new actionable payment request. Payment completion, pending proof delivery and request reconciliation should produce a stable presentation. If an operation is still resolving, show an explicit pending state rather than briefly presenting an apparent second request.
Steps to Reproduce
Observed sequence; not yet a deterministic reproduction:
- On iOS Bitkit, create and send a payment request to the Android contact.
- Open the incoming request on Android.
- Close/background Bitkit on the receiving iPhone.
- Send the requested payment from Android.
- Observe Android immediately afterward: an apparent second payment request briefly appears, then disappears by itself.
The reporter performed the payment. The agent only collected logs and inspected source; no additional payment was initiated for this investigation.
Logs / Screenshots / Recordings
Sanitized excerpts from the connected devices, 16 October 2026. All times below are Europe/Warsaw (CEST, UTC+02:00); iOS and app-internal UTC timestamps have been converted. Wallet/contact identifiers, payment payloads, addresses and transaction identifiers are omitted.
17:03:52.200 iOS Hiding sheet receive reason: Payment request created
17:04:39.158 Android Starting scan from 'scan result': 'incoming payment request target'
17:04:48.218 iOS Scene phase changed: background
17:06:37.492 Android Rejected incoming Paykit payment request presentation:
category='resolution' reason='resolution_failed'
counterparty=[REDACTED]
17:06:59.066 Android Starting scan from 'scan result': 'incoming payment request target'
17:07:07.359 Android Queued a Paykit payment proof for private delivery
Evidence limits: the Android presentation lines do not contain request IDs, so the two openings cannot be proven to belong to the same request. There is no timestamped recording of the brief extra presentation. The collected iOS foreground log ends after the 17:04:48 background transition and cannot show subsequent receipt/reconciliation. Queuing a proof is not by itself evidence of final settlement or proof delivery. These lines are surrounding evidence, not a proven mapping of every UI action above.
No screenshot/recording captures this specific transient symptom. Raw device logs remain private because they contain wallet data; only the sanitized excerpt is included.
Bitkit Version
Android 3.0.0 RC2 (193), mainnet, release revision 19449fb0d729f51b9875eee2f66acfc0671e11da.
Request creator: iOS 3.0.0 RC2 (200), revision 59278ad871381f8ac4b9d4d0f70953643d59243e.
Device / OS
Samsung Galaxy S22 (SM-S901B), Android 16 (API 36).
Counterparty: iPhone 13; iOS version not captured in this report.
Reproducibility
Rarely (once or twice) — one reported occurrence in this sequence. No repeat rate or exact transient duration measured.
Additional context
Investigation, not a confirmed root cause: Android's finishIncomingPaymentRequestPreparation defers a request presentation when payment-detail resolution does not open a usable target. deferPaymentRequestPresentation schedules retries. The observed resolution failure makes presentation retry / stale request reconciliation worth investigating, but does not prove that retry created the apparent duplicate.
Correlate request IDs, lifecycle states, local pending/presented membership, in-flight/completed proof protection and refresh generations around send completion and sheet cleanup. Check whether a stale request is re-presented before a later refresh removes it. Do not assume that the backgrounded recipient created a second request or that funds were sent twice.
Related reports, not confirmed duplicates:
- Android #1348 — Failed Paykit payment request stays stuck and looks paid (closed): insufficient-funds failure and persistent stuck state; different observed trigger/outcome.
- iOS #737 — finishPayment can re-queue a just-paid request (closed): historical source-level race; useful investigation context, but its iOS-specific mechanism is not established on current Android.
- Android #1524 — Contact payment options temporarily disappear after cancelling confirmation (open): funding-method availability after cancellation, rather than a transient extra incoming request.
Activity
piotr-iohk commentedon Oct 16, 2026
Additional RC2 observation: stale request after initial USDT subscription payment
During the same paired mainnet test, the reporter observed this recurring-payment sequence on both Android and iOS: accept subscription → complete initial USDT payment → payment-request notification remains briefly tappable → tapping it eventually removes it with a no-longer-available toast. No second payment was observed.
This broadens the observed scope of #1534; it does not prove that the earlier ordinary-request reappearance and this subscription notification share a root cause. The on-chain subscription controls were one-step; stale-notification parity for on-chain was not separately confirmed.
Sanitized logs, 2026-10-16 Europe/Warsaw:
Android proof queue precedes the resolution failure by 34.271 seconds. The Android messages omit request IDs, so the later presentation cannot be proven to concern the same request. iOS independently records no-pending-request cleanup 8.223 seconds after queuing the proof; that is not an exact measurement of notification visibility.
Investigate recurring-period request visibility and completed/in-flight proof protection during refresh, alongside presentation retry and send-sheet cleanup. Do not claim duplicate payments or a confirmed requeue race.
Related: historical iOS #737; Android #1407 latency; shared-runtime latency #1419. Current iOS stale-subscription counterpart: iOS #982. Paired two-step-USDT reports: Android #1540 / iOS #981. They are separate behaviors, not proven shared causes.
Raw logs remain private; sanitized selected events only. No new agent payment or runtime reproduction was performed.
Current related reports
These reports track separate behaviors; a shared root cause is not established.
ben-kaufman commentedon Oct 18, 2026
I checked the current request refresh, presentation retry and local proof-protection paths, including the matching iOS report. The existing Android refresh-race tests pass, but they do not reproduce the reported post-subscription notification window.
Keeping this open separately from the UI fixes in #1564. We still need a deterministic reproduction correlating the same request ID/billing period with submission, proof queuing, pending membership and presentation generation. I have not established a duplicate payment or a shared cause with contact-method availability.
piotr-iohk commentedon Oct 19, 2026
Still observed on 3.0.1 — 2026-10-19
During today's manual testing with Android 3.0.1 (195) on Samsung Galaxy S22 and iOS 3.0.1 (202), Piotr observed a duplicate-looking payment request on Android for a request sent from iOS.
The duplicate-looking request cannot be paid: attempting to act on it shows “payment request not available.” This is consistent with the stale request presentation described in this issue and remains observable in 3.0.1.
This observation does not establish a second request being created or a duplicate payment; the attempted action on the stale entry is rejected. No new request-ID correlation or root-cause confirmation is available from this observation.
ovitrif commentedon Oct 20, 2026
Reproduced on mainnet with Bitkit Android 3.0.1 (195), and this time the request came back payable:
That raises this from a transient display glitch to a double-payment risk: a buyer could pay a Shop order twice. It's a 3.0.x hotfix candidate once fixed.
Likely related: iOS fixed the same class of bug in synonymdev/bitkit-ios#737 ("finishPayment can re-queue a just-paid request, enabling a second payment"). Check whether Android lacks the equivalent guard.
Expected: a request that's paid, or broadcast and pending, never resurfaces as payable, and shows as paid in Payments.
ovitrif commentedon Oct 20, 2026
More detail on the mainnet case: on the "Bitcoin Sent" sheet, tapping Details opened the details screen, but the stale Payment Request sheet for the paid order sat on top of it, then disappeared on its own a few seconds later. So it looks like a stale request sheet left in the stack after payment, removed once the app syncs. It's still payable while it's visible.
ovitrif commentedon Oct 20, 2026
Root cause found for the mainnet case, fix in #1635 (still open on master; not fixed by anything merged since 3.0.1).
What happens
PaykitPaymentRequestRepo.updateRequestremoves it from the pending list, anddiscardExpiredRequestsLocked→prunePresentedRequestIdsdrops its "presented" mark, since it is no longer pending.markOnchainPaymentStarted, inbeforeSendAttempt). A request refresh that lands between acceptance and that point reads the SDK record asACCEPTEDwith no started proof, so it puts the request back in the pending list with no presented mark (applyRequestSnapshotLocked, theoneTimeIncomingfilter).observePaymentProofChangesis gated onisPaymentSubmissionActive), and the submission includes the Bitcoin Sent sheet and the wallet sync after it. On mainnet that sync takes tens of seconds, so the stale entry stays.clearActiveContactPaymentContextandpresentNextIncomingPaykitPaymentRequest, which readautomaticPendingRequests()and open the request again with Swipe To Pay. Once the submission ends, the queued refresh drops the request and the sheet closes on its own, which matches the "disappeared a few seconds later" observation.Android has no
finishPaymentre-queue like iOS had in synonymdev/bitkit-ios#737; the gap is this refresh and presentation path.Would a second swipe have paid? Probably not, but not verified on a device:
PaykitPaymentProofRepo.preparerefuses a request with a started local proof or a proof already in the SDK record. The PR adds a guard in front of it anyway:isPending, which the swipe checks, now refuses any request with a started or completed local payment.Fix (#1635)
isPendingand automatic presentation read the local proof store live, so a broadcast-pending or paid request is never payable even while the pending list predates it.The subscription stale-notification case from the RC2 comment above uses separate due-request logic and is not covered by this fix.