fix(send): resolve indefinite sync overlay wait#590
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0c60f8c170
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Greptile SummaryThis PR resolves an indefinite-wait bug in the send sheet by adding a 20-second timeout to the "Connecting to network" sync overlay. After the timeout, unified BIP21 invoices fall back to onchain payment, while pure Lightning/LNURL invoices switch the overlay to "Connection issues" copy and keep retrying in the background.
Confidence Score: 4/5The change is well-scoped and safe to merge; no payment execution paths are altered and no data loss is possible. The core timeout and onchain-fallback logic follow existing patterns correctly. The main concern is that syncTimedOut resets on any network interruption, causing the overlay to revert from Connection issues back to Connecting with a fresh 20-second countdown after a brief network blip. SendSheet.swift — the .task(id: isSyncTimeoutActive) guard branch that resets syncTimedOut and the try? sleep pattern.
|
| Filename | Overview |
|---|---|
| Bitkit/Views/Wallets/Send/SendSheet.swift | Core change: adds syncTimedOut state, isSyncTimeoutActive computed property, and a .task(id:) modifier that drives the 20 s timeout; handleSyncTimeout branches on unified vs. pure-LN invoice. The logic is sound but syncTimedOut is reset on every network transition, which can surface "Connecting..." again after the user has already seen "Connection issues". |
| Bitkit/Views/Wallets/Send/SendSyncScreen.swift | Adds showLongWait: Bool (required, no default) to switch between "Connecting" and "Connection issues" copy and accent colour. Clean, minimal change; showLongWait lacks a default value which could break future/preview call sites. |
| changelog.d/next/417.fixed.md | New changelog entry accurately describes the fix. |
Sequence Diagram
sequenceDiagram
participant U as User
participant SS as SendSheet
participant SC as SendSyncScreen
participant NM as NetworkMonitor
participant W as WalletViewModel
U->>SS: Open send sheet
SS->>SC: "showLongWait=false"
Note over SS: task(id: true) sleeps 20s
alt Peer connects before timeout
W-->>SS: "hasUsableChannels = true"
SS->>SS: validatePaymentAfterSync()
SS->>U: Payment flow proceeds
else Network drops during wait
NM-->>SS: "isConnected=false, isSyncTimeoutActive=false"
Note over SS: task cancelled, syncTimedOut reset to false
NM-->>SS: "isConnected=true, isSyncTimeoutActive=true"
Note over SS: Fresh 20s window restarts
else Timeout fires, unified invoice
SS->>SS: handleSyncTimeout()
SS->>SS: validatePaymentAfterSync(ignoreChannelWait:true)
SS->>U: Onchain fallback, navigate to Amount
else Timeout fires, pure LN or LNURL
SS->>SS: handleSyncTimeout()
SS->>SC: "showLongWait=true"
Note over SC: Connection issues copy shown
W-->>SS: "hasUsableChannels=true (later)"
SS->>U: Payment flow proceeds
end
Reviews (1): Last reviewed commit: "chore: rename changelog fragment" | Re-trigger Greptile
When a lightning payment starts while channels exist but the peer is not connected, the send sheet's "Connecting to network" overlay waited forever: deferred validation early-returns and is only re-triggered by a hasUsableChannels change that never fires if the peer never connects. The overlay wait is now bounded to 20 seconds while the device is online. On timeout, unified invoices take the existing lightning-to-onchain fallback (the same decision AppViewModel makes at scan time when lightning cannot pay), and everything else swaps the overlay copy to the existing "Connection issues" strings while continuing to listen for the peer, with swipe-down dismissal available throughout.
Review follow-up: the timeout task identity now includes node readiness, so a window that started while the node was still starting restarts at .running and the unified-invoice onchain fallback can still fire with fresh balances (previously the Bool id never flipped again and no second window was scheduled). The long-wait copy is also preserved while the timer is merely paused by an offline blip, resetting only when the overlay itself resolves.
Review feedback: instead of swapping the sync screen's copy, reuse the existing OfflineSheetScreen for the timed-out state. OfflineSheetOverlayModifier gains a forceShow flag so SendSheet can present it when the Lightning peer is unreachable, not only when the device is offline. Also clears stale scanned-invoice state when the send sheet opens fresh at the options route, so closing the sheet during the sync overlay and tapping Send again no longer reopens the overlay. The reset runs in onAppear for the options route rather than onDisappear to avoid wiping a freshly scanned invoice when SheetViewModel hides and re-shows the sheet for an incoming deep link.
Review feedback: with the node running, unified invoices used to fall back to onchain immediately when no channel was usable. They now prefer Lightning and let the send sheet's sync overlay and timeout decide, so a briefly offline peer no longer forces an onchain payment. No usable channels at all, or usable channels without capacity, still fall back immediately.
Found while testing the unified-invoice path with the peer disconnected: calculateRoutingFee computed UInt64(maxSendLightningSats) - buffer, which underflows and traps when maxSendLightningSats is 0 (no usable channel). A zero-amount Lightning invoice routes to this screen, so the app crashed. With no usable outbound capacity there is nothing to estimate, so the routing fee is now 0 and the subtraction is guarded.
66d44f7 to
6ddf78c
Compare
Cleanup pass over the PR's changes: - Collapse the channels-but-unusable check in AppViewModel into a single if-let, matching the idiom already used in the pure-bolt11 branch below. - Use UInt64(clamping:) for the routing-fee capacity conversion instead of a hand-rolled max(0, ...) inside a trapping initializer. - Log shouldShowSyncOverlay on transitions via onChange instead of on every evaluation; the new syncTimeoutPhase property raised the per-render evaluation count, which tripled identical debug lines in the session log.



Fixes #417
Description
This PR bounds the send sheet's "Connecting to network" wait so a Lightning peer that never connects can no longer leave the user waiting indefinitely.
The problem: when a Lightning payment starts while channels exist but the peer is not connected, the send sheet shows a "Connecting to network (±10 seconds)" overlay. If the peer never connects, that spinner runs forever. The code waits for a channel-state change that may never arrive, and nothing else can break the wait.
The fix: stop waiting forever. After 20 seconds, do something sensible based on what kind of payment it is:
The invoice has an onchain fallback (a unified BIP21 invoice carrying both a Lightning invoice and a Bitcoin address): prefer Lightning and give the peer the 20-second window to reconnect; if it does, the payment proceeds over Lightning, otherwise fall back to onchain. This also changes scan-time behavior per review: previously, with the node running and channels unusable, unified invoices skipped Lightning entirely and fell back to onchain immediately. Now they wait behind the sync overlay first, so a briefly-offline peer no longer forces onchain fees.
No fallback is possible (pure bolt11 invoice, LNURL): the payment cannot go any other way, so the honest move is to tell the user what is actually happening. After the timeout, the existing connection issues screen (
OfflineSheetScreen) covers the sheet, via a newforceShowparameter onofflineSheetOverlayas suggested in review. The app keeps listening in the background, so if the peer does come back, the payment flow resumes on its own, and the user can swipe the sheet away at any time.flowchart TD A["Sync overlay visible"] -->|peer connects| B["Existing validation runs, flow proceeds"] A -->|"timeout ~20s, online"| C{"Unified invoice and node running?"} C -->|yes| D["Existing onchain fallback, validate balance, go to amount screen"] C -->|no| E["Show the connection issues screen, keep retrying"] E -->|peer connects later| BAlso fixed, per review: closing the sheet during the sync overlay and tapping Send again no longer reopens the overlay. Fresh Send opens (the Options route, used by the TabBar button) now clear leftover scanned-invoice state. The reset deliberately lives in
onAppearfor the Options route rather thanonDisappear:SheetViewModel.showSheethides and re-shows after 0.7s when another sheet is open, so anonDisappearreset could wipe a freshly scanned invoice when a deep link or push payment arrives while the send sheet is already open.The timeout only counts down while the device is online (when offline, the offline overlay already covers the sheet) and resets whenever the overlay resolves or the sheet is reopened.
While verifying the above on a regtest simulator with the peer disconnected, I hit a separate crash:
SendAmountView.calculateRoutingFee()computesUInt64(maxSendLightningSats) - buffer, which underflows and traps whenmaxSendLightningSatsis 0 (no usable channel). This is a pre-existing crash on master, reachable today by a zero-amount Lightning invoice with an offline peer, and the change in point 1 widens the path to it (a unified zero-amount invoice now prefers Lightning instead of skipping straight to onchain). It is fixed at the root here: with no usable outbound capacity there is nothing to estimate, so the routing fee is 0 and the subtraction is guarded.Known trade-off: if the unusable channels could never cover the invoice amount anyway, the user still waits the 20 seconds before the inevitable onchain fallback. A capacity pre-check on unusable channels could short-circuit that at the cost of approximating fees and limits; happy to add it if preferred.
Linked Issues/Tasks
Fixes #417 (originated from the #401 review)
Screenshot / Video
VERIFICATION_PLACEHOLDER
QA Notes
Manual Tests
regression:healthy channels → paste LN invoice: flow proceeds as before, no overlay change.regression:Receive and Force Transfer sheets offline: offline overlay unchanged.regression:sync overlay in any state → swipe down: sheet dismisses.Automated Checks
swiftformatclean; Debug build for the iOS Simulator passes.