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, + } }