fix: expose rejected incoming payment requests - #721
Conversation
This comment has been minimized.
This comment has been minimized.
70cd263 to
ac81ad1
Compare
ovitrif
left a comment
There was a problem hiding this comment.
The rewritten head preserves the earlier expiry fix. I found two non-blocking coverage and journey-documentation gaps.
ac81ad1 to
8172d6d
Compare
8172d6d to
1f75bfd
Compare
jvsena42
left a comment
There was a problem hiding this comment.
One nit, arising from reviewing the Android counterpart (#1217) rather than from this PR's own code: the iOS-only label on this suite stops being true once #1217 lands.
1f75bfd to
2f3ef18
Compare
jvsena42
left a comment
There was a problem hiding this comment.
Reviewed this against the Android twin (synonymdev/bitkit-android#1217), where I confirmed a real regression. iOS is structurally better and does not have it — worth recording why.
Android bumped its presentation generation unconditionally when a request turned out expired, including for automatic presentations, so an expired request A invalidated the batch and request B behind it was fully resolved over the network and then discarded. Here, presentRequests snapshots activePresentationGeneration for the closure (:888-901) and isCurrentPresentation (:905-912) is checked at AppScene.swift:950 — before the beginPaymentRequest network call at :952 — and again at :953-957, :983, :1012, :1027, :1038. So even when the generation does move, no resolution work is wasted. The new expired branch also returns at :946 before the bump, and discardExpiredRequests only bumps when requestedPresentationId itself is dropped (:1110-1113), which is nil for automatic presentations.
The other two Android bugs are absent too: .expired is effectively unreachable because the SDK derives ProposalExpired at query time and parse checks state != .proposed first (returning .nonActionableState, which is excluded from logging), and synchronizationDate is sampled before the await. And the toast queues can't both be non-empty in one transition — performRefresh guards on requestedId != handledRequestedExpirationId (:1019) and deferPresentation passes it through (:942-948).
Also verified: the expiry double-now() race is fixed (one presentationDate threaded into discardExpiredRequests(at:), :940-945); all five new toast strings exist in en.lproj/Localizable.strings:1477-1481; persistPresentedRequestIds() runs before every discardExpiredRequests return path.
Two non-blocking logging notes inline.
jvsena42
left a comment
There was a problem hiding this comment.
Re-reviewed at 5e4bebd. No HIGH/MEDIUM. Nothing new to file — the two candidates I chased both died on verification (details below so they aren't re-chased).
Fund safety / authorization — clean. Amount and counterparty are pinned at approve time and this PR does not move that. SendSheet calls markPresentedIfPending(request) (clearing requestedPresentationId) and sets sendAmountSats from the request; confirmation goes prepareIncomingPaymentRequest → prepareForPayment → perform, which re-checks expiry, pending membership, and single-flight via processingRequestIds. approvedPaymentRequestIds is set only after service.accept succeeds. No path added here re-enters payment: every deferPresentation return value only affects retry scheduling and toasts, and .requestedPresentationEnded marks the request presented and drops the id, so a new Pay tap must go through requestPresentation again. Because markPresentedIfPending nils the id on send-sheet open, the perform expiry path can't enqueue a second "expired" toast alongside the send flow's own error.
Trust boundary — clean, and this was the main thing I wanted to check given the PR surfaces counterparty-side failures. Toasts render only static localization keys; logs carry enum raw values, safeCode ([a-z0-9_-], ≤64 bytes), a metatype name, and the 12-char redacted pubkey prefix. PaykitError.context is dropped everywhere. No homeserver- or counterparty-supplied string reaches the UI or a log verbatim.
Also traced clean: no persisted schema change (PresentationStore.State and PaykitPaymentRequest.ID untouched; ParseFailure and IncomingPaykitPaymentRequestFailureReason are never persisted), so no migration risk from the previously shipped build. parse accepts exactly the set base accepted for both actionable and history records — only failure reporting changed. Both feedback queues drain with while let under a single trigger comparison, so SwiftUI coalescing several increments into one onChange can't drop a toast. Every deferIncomingPaykitPaymentRequestPresentation call site is preceded by isCurrentPresentation(request) with no await in between, so there's no TOCTOU on the requester across the suspension. No seed-derived material touched.
Android parity (#1217): the sheet-restore-cancelled-by-hideSheet bug is structurally absent here — the sheet hides itself before requesting presentation and never restores, terminal feedback lands as a toast on whatever is underneath. The peer-supplied-invoice-logged-on-decode-failure issue is absent too; this PR actually removed the base Logger.warn("...: \(error)") at that site in favour of reason=invalid_payment_target, which is the right direction. Expiry-during-backoff is present on both and handled here via recordRequestedPresentationExpiration + expirationTrigger.
Still dev/QA-facing today (isUIEnabled defaults false, Dev Settings only), so none of this is user-reachable until the flag flips.
| _ feedback: IncomingPaykitPaymentRequestPresentationFeedback, | ||
| for request: PaykitPaymentRequest | ||
| ) { | ||
| Logger.warn( |
There was a problem hiding this comment.
Every presentation retry reaches this logger, including .retryScheduled, because logging happens before the toast == nil guard. An automatic request without expiry can generate 15 warnings during the rapid retry phase, then another every 120 seconds indefinitely. Could we log only terminal outcomes, or deduplicate or rate-limit non-terminal retries by request ID and reason?
Fixes #714
Expose rejected incoming payment requests through privacy-safe diagnostics and terminal user feedback while preserving automatic recovery.
Description
Linked Issues/Tasks
Preview
pr721-payment-request-unavailable-2x.mp4
QA Notes
Manual Tests
category=resolution reason=no_supported_endpointand redact the counterparty.PaymentRequestUnavailableToastshowsPayment RequestandThe payment request is no longer available.PaymentRequestRow-7abfa801-a3bd-4d74-b75a-18be91d2ddbf:PaymentRequestPay-7abfa801-a3bd-4d74-b75a-18be91d2ddbfremains enabled for another attempt.Automated Checks
PaykitPaymentRequestServiceTestsandPrivatePaykitServiceTests.PaykitPaymentRequestServiceTestsandPublicPaykitServiceTests.requested-resolution-failure.xmljourney confirmed atb79c43d.5e4bebdon master215a9f1.5e4bebd.