From 700947ea3c4a82d238be661557735e644424fb88 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sun, 2 Aug 2026 07:43:24 -0400 Subject: [PATCH] feat(wallet): app icons, mobile layout, and a funding mode on the Lightning modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Icons: each node choice now shows its app icon (lnd.png; Core Lightning's is vendored from the Umbrel gallery as core-lightning.svg). Vendored rather than hotlinked on purpose — these nodes run offline/airgapped, and a remote image would both break there and leak a request to a third-party host on every render. A missing asset falls back to a neutral bolt glyph so a row can never render a broken-image box. Mobile: the choice row keeps icon + name + blurb together and drops the action to its own full-width line under 26rem, instead of squeezing the description into a two-word column next to a button. Funding mode: a node that is running but has no funds / no inbound liquidity is neither "install one" nor "start it", so the same modal gains a third mode that explains it and routes to the run-lightning-node goal, where funding and channel-opening already live — reusing that flow rather than duplicating it. It fires where the user actually meets the problem: on a failed attempt. handleLightningFailure() maps a running node's send/receive failure onto the funding modal, matched on message text because LND surfaces "no route", "no channels" and "insufficient balance" as plain strings with no distinct code — and all three mean the same thing to a user: fund me. Verified: 5 gate tests; npm run build clean. Co-Authored-By: Claude Opus 5 (1M context) --- .../assets/img/app-icons/core-lightning.svg | 56 +++++++++++ .../src/components/LightningRequiredModal.vue | 94 +++++++++++++++++-- .../src/components/ReceiveBitcoinModal.vue | 3 + neode-ui/src/components/SendBitcoinModal.vue | 2 + .../src/composables/useLightningRequired.ts | 53 ++++++++++- 5 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 neode-ui/public/assets/img/app-icons/core-lightning.svg diff --git a/neode-ui/public/assets/img/app-icons/core-lightning.svg b/neode-ui/public/assets/img/app-icons/core-lightning.svg new file mode 100644 index 00000000..94e8b1e2 --- /dev/null +++ b/neode-ui/public/assets/img/app-icons/core-lightning.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/neode-ui/src/components/LightningRequiredModal.vue b/neode-ui/src/components/LightningRequiredModal.vue index 3d9d9ab8..f41728a5 100644 --- a/neode-ui/src/components/LightningRequiredModal.vue +++ b/neode-ui/src/components/LightningRequiredModal.vue @@ -5,12 +5,17 @@ same reasoning as ExternalExplorerModal. --> -

+

+ Your Lightning node is running, but it has no funds or inbound liquidity + yet — so it can't send or be paid. The Lightning setup walks you through + funding it and opening a channel. +

+

Lightning payments need a Lightning node that's actually running. Yours is installed but isn't running right now — start it from My Apps and try again. @@ -21,13 +26,27 @@ it'll be installed for you.

-
+
-
+
+
+ + + + + +
{{ node.name }} @@ -41,7 +60,7 @@
@@ -71,12 +90,17 @@ class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium" @click="openApps" >Open My Apps +
+ + diff --git a/neode-ui/src/components/ReceiveBitcoinModal.vue b/neode-ui/src/components/ReceiveBitcoinModal.vue index 81edfd8d..4b77e29e 100644 --- a/neode-ui/src/components/ReceiveBitcoinModal.vue +++ b/neode-ui/src/components/ReceiveBitcoinModal.vue @@ -191,6 +191,9 @@ async function receive() { emit('received') } } catch (err: unknown) { + // A running node with no inbound liquidity is a funding problem, not a + // failure — reuse the Lightning modal in its funding mode instead. + if (receiveMethod.value === 'lightning' && lightning.handleLightningFailure(err)) return error.value = receiveMethod.value === 'onchain' ? explainReceiveAddressFailure(err) : err instanceof Error ? err.message : 'Failed' diff --git a/neode-ui/src/components/SendBitcoinModal.vue b/neode-ui/src/components/SendBitcoinModal.vue index 9a01cb0c..84c5ff32 100644 --- a/neode-ui/src/components/SendBitcoinModal.vue +++ b/neode-ui/src/components/SendBitcoinModal.vue @@ -665,6 +665,8 @@ async function send() { // Success pane (or the token pane for ecash mints) takes over the modal. confirming.value = false } catch (err: unknown) { + // Running node with nothing to pay with -> funding modal, not a raw string. + if (effectiveMethod.value === 'lightning' && lightning.handleLightningFailure(err)) return error.value = err instanceof Error ? err.message : t('web5.sendFailed') } finally { processing.value = false diff --git a/neode-ui/src/composables/useLightningRequired.ts b/neode-ui/src/composables/useLightningRequired.ts index 2f1ac434..cca8e95e 100644 --- a/neode-ui/src/composables/useLightningRequired.ts +++ b/neode-ui/src/composables/useLightningRequired.ts @@ -28,8 +28,10 @@ export const LIGHTNING_NODE_APP_IDS = ['lnd'] as const /** `absent` — nothing installed, offer to install one. * `stopped` — installed but not running, point the user at My Apps. + * `no-funds` — running, but there is nothing to pay with / no inbound + * liquidity to be paid into; send the user to the Lightning setup goal. * `running` — good to go. */ -export type LightningStatus = 'absent' | 'stopped' | 'running' +export type LightningStatus = 'absent' | 'stopped' | 'running' | 'no-funds' // Module-scope: one source of truth shared by every caller and the single // global modal mounted in App.vue. @@ -73,5 +75,52 @@ export function useLightningRequired() { show.value = false } - return { show, status, lightningStatus, hasLightningNode, requireLightningNode, close } + /** + * Raise the same modal in its funding mode: the node is installed and + * running, but has no usable balance/liquidity yet. Reuses this modal + * rather than inventing a second one, and routes to the Lightning setup + * goal where funding and channel-opening already live. + */ + function openLightningFunding() { + status.value = 'no-funds' + show.value = true + } + + /** + * Map a failed Lightning attempt onto the funding modal when the node is + * running but has nothing to pay with / no inbound liquidity. Returns true + * when it handled the error, so the caller can skip showing a raw string. + * + * Matched on the message because LND surfaces these as plain text: there is + * no distinct error code for "no channels" vs "no route" vs "insufficient + * balance", and all three mean the same thing to the user — fund me. + */ + function handleLightningFailure(err: unknown): boolean { + if (lightningStatus() !== 'running') return false + const msg = (err instanceof Error ? err.message : String(err ?? '')).toLowerCase() + const fundingRelated = [ + 'no route', + 'no routes', + 'insufficient', + 'no channel', + 'not enough', + 'balance', + 'unable to find a path', + 'no path', + ].some((needle) => msg.includes(needle)) + if (!fundingRelated) return false + openLightningFunding() + return true + } + + return { + show, + status, + lightningStatus, + hasLightningNode, + requireLightningNode, + openLightningFunding, + handleLightningFailure, + close, + } }