Skip to content

fix(send): resolve indefinite sync overlay wait#590

Open
CypherPoet wants to merge 7 commits into
synonymdev:masterfrom
CypherPoet:fix/send-sync-overlay-timeout
Open

fix(send): resolve indefinite sync overlay wait#590
CypherPoet wants to merge 7 commits into
synonymdev:masterfrom
CypherPoet:fix/send-sync-overlay-timeout

Conversation

@CypherPoet

@CypherPoet CypherPoet commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

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:

  1. 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.

  2. 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 new forceShow parameter on offlineSheetOverlay as 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| B
Loading

Also 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 onAppear for the Options route rather than onDisappear: SheetViewModel.showSheet hides and re-shows after 0.7s when another sheet is open, so an onDisappear reset 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() computes UInt64(maxSendLightningSats) - buffer, which underflows and traps when maxSendLightningSats is 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

  • 1. Open channel → Settings → Advanced → Lightning Node → Disconnect peer → Send → paste pure bolt11 invoice: sync overlay → after ~20s the connection issues screen covers the sheet.
  • 2. Same setup → reconnect peer (or wait for auto-reconnect): overlay resolves on its own → Confirm or QuickPay.
  • 3. Peer disconnected → paste unified BIP21+bolt11 invoice: sync overlay → after ~20s lands on Amount in onchain mode (or insufficient-savings toast if the onchain balance cannot cover it).
  • 4. Close the sheet during the sync overlay → tap Send: opens at Options, no overlay.
  • 5. During the wait → enable airplane mode: offline screen covers the sheet; disable it: the sync overlay returns and the window restarts.
  • 6. Peer disconnected → paste a zero-amount LN invoice: no crash (previously crashed in routing-fee estimation); sync overlay shows.
  • 7. regression: healthy channels → paste LN invoice: flow proceeds as before, no overlay change.
  • 8. regression: Receive and Force Transfer sheets offline: offline overlay unchanged.
  • 9. regression: sync overlay in any state → swipe down: sheet dismisses.

Automated Checks

  • No unit coverage exists for the send sheet's deferred-validation flow, and none is added here; the timeout decision lives in the view. Extracting it into a testable policy function would be a reasonable follow-up.
  • Local: swiftformat clean; Debug build for the iOS Simulator passes.
  • CI: standard build checks run by the PR bot.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread Bitkit/Views/Wallets/Send/SendSheet.swift Outdated
@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown

Greptile Summary

This 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.

  • Timeout mechanism: Uses .task(id: isSyncTimeoutActive) — a SwiftUI task that restarts whenever isSyncTimeoutActive changes — to sleep 20 s then call handleSyncTimeout(), which either triggers the existing onchain fallback or flips syncTimedOut = true to swap the overlay copy.
  • SendSyncScreen update: Adds a showLongWait: Bool property that switches the headline/description strings and accent color (purple → yellow) when the connection issues state is reached.

Confidence Score: 4/5

The 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.

Important Files Changed

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
Loading

Reviews (1): Last reviewed commit: "chore: rename changelog fragment" | Re-trigger Greptile

Comment thread Bitkit/Views/Wallets/Send/SendSheet.swift Outdated
Comment thread Bitkit/Views/Wallets/Send/SendSheet.swift Outdated
Comment thread Bitkit/Views/Wallets/Send/SendSyncScreen.swift Outdated
@pwltr pwltr self-requested a review June 11, 2026 07:23
@pwltr

pwltr commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Tested on iPhone 17 simulator

  • Create wallet
  • Open channel
  • Disconnect peer in Settings -> Advanced -> Lightning Node
  • Open SendSheet
  • Paste Invoice

Findings:

Instead of changing only the text we could show the existing OfflineSheetScreen, you may have to modify OfflineSheetOverlayModifier for this so it's not conditional on network connection only.

Simulator.Screen.Recording.-.iPhone.17.-.2026-06-11.at.12.37.34.mov
Send Bitcoin connection issues

In the scenario where peers/channels are unusable, unified BIP21 invoices fall back to onchain immediately in AppViewModel. The new SendSheet timeout fallback only applies to deferred cases, mainly when the invoice was scanned before the node reached .running. So the changelog/PR description should avoid implying unified invoices generally wait for the timeout before falling back. Alternatively, I think your proposed change makes more sense, but it needs some additional changes.

Simulator.Screen.Recording.-.iPhone.17.-.2026-06-11.at.12.43.01.mov

If a Lightning-only invoice opens the sync overlay, closing the sheet and tapping Send again can reopen the sync overlay because the scanned invoice state remains. This does not appear to be caused by this PR, but I think it can be easily fixed in this context by calling app.resetSendState() in SendSheet onDisappear().

Simulator.Screen.Recording.-.iPhone.17.-.2026-06-11.at.13.39.11.mov

@pwltr pwltr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.
@CypherPoet CypherPoet force-pushed the fix/send-sync-overlay-timeout branch from 66d44f7 to 6ddf78c Compare June 12, 2026 01:42
@CypherPoet

CypherPoet commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! All three points addressed and verified on a regtest simulator (open channel, then Settings > Advanced > Lightning Node > remove peer).

1. Show the OfflineSheetScreen instead of swapping text. Done. OfflineSheetOverlayModifier now takes a forceShow flag so it is no longer driven solely by network connectivity, and SendSheet forces it on after the timeout. SendSyncScreen is reverted to its original copy. Verified: after ~20s with the peer offline, a pure Lightning invoice shows the connection issues screen (the phone-with-rings screen, same as offline).

2. Unified invoices falling back immediately. AppViewModel now prefers Lightning for unified invoices when channels exist but none are usable, instead of skipping to onchain. The send sheet's overlay and timeout then decide: peer reconnects -> pays over Lightning; timeout -> onchain fallback. "No channels at all" and "usable but no capacity" still fall back immediately. The PR description is updated so it no longer implies unified invoices generally wait for the timeout. Verified: unified invoice with peer offline lands on the onchain amount screen after the timeout.

3. Reopening the sheet shows the stale overlay. Fixed by clearing scanned-invoice state on a fresh send open. I put the reset in onAppear gated on the options route rather than onDisappear, because SheetViewModel.showSheet hides and re-shows the sheet after 0.7s when another sheet is open, so an onDisappear reset could wipe a just-scanned invoice arriving from a deep link or push while the send sheet is open. The options route only opens from the plain Send button, which never carries invoice state. Verified: closing during the overlay and tapping Send reopens at the options list.

Bonus, a crash I hit while testing. With the peer offline, pasting a zero-amount Lightning invoice crashed in SendAmountView.calculateRoutingFee(): UInt64(maxSendLightningSats) - buffer underflows when maxSendLightningSats is 0. It is pre-existing on master (reachable today via a zero-amount invoice with an offline peer), and point 2 widens the path to it, so I fixed it at the root in the same PR. Guarded so zero outbound capacity yields a 0 fee with no subtraction.

Screenshots

Pure Lightning invoice showing the connection-issues screen after the timeout

pr590-finding1-connection-issues

Unified invoice falling back to the onchain amount screen

pr590-finding2-onchain-fallback

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.
@pwltr pwltr self-requested a review June 12, 2026 08:07
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.

Potential infinite wait state in SendSheet.swift

2 participants