diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 766bab1a..79765993 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -678,9 +678,12 @@ export const walletLogger = ({ wallet, models }) => { payment_hash: decoded.id, created_at: decoded.created_at, expires_at: decoded.expires_at, - description: decoded.description + description: decoded.description, + // payments should affect wallet status + status: true } } + context.recv = true await models.walletLog.create({ data: { diff --git a/components/log-message.js b/components/log-message.js index b1593027..73f76263 100644 --- a/components/log-message.js +++ b/components/log-message.js @@ -19,7 +19,16 @@ export default function LogMessage ({ showWallet, wallet, level, message, contex className = 'text-info' } - const hasContext = context && Object.keys(context).length > 0 + const filtered = context + ? Object.keys(context) + .filter(key => !['send', 'recv', 'status'].includes(key)) + .reduce((obj, key) => { + obj[key] = context[key] + return obj + }, {}) + : {} + + const hasContext = context && Object.keys(filtered).length > 0 const handleClick = () => { if (hasContext) { setShow(show => !show) } @@ -37,16 +46,17 @@ export default function LogMessage ({ showWallet, wallet, level, message, contex {message} {indicator} - {show && hasContext && Object.entries(context).map(([key, value], i) => { - const last = i === Object.keys(context).length - 1 - return ( - - - {key} - {value} - - ) - })} + {show && hasContext && Object.entries(filtered) + .map(([key, value], i) => { + const last = i === Object.keys(filtered).length - 1 + return ( + + + {key} + {value} + + ) + })} ) } diff --git a/components/wallet-buttonbar.js b/components/wallet-buttonbar.js index 76b60e92..46572919 100644 --- a/components/wallet-buttonbar.js +++ b/components/wallet-buttonbar.js @@ -11,7 +11,7 @@ export default function WalletButtonBar ({ return (
- {isConfigured(wallet) && + {isConfigured(wallet) && wallet.def.requiresConfig && } {children}
diff --git a/components/wallet-card.js b/components/wallet-card.js index eff71d06..a964f8d6 100644 --- a/components/wallet-card.js +++ b/components/wallet-card.js @@ -1,23 +1,34 @@ -import { Badge, Card } from 'react-bootstrap' +import { Card } from 'react-bootstrap' import styles from '@/styles/wallet.module.css' import Plug from '@/svgs/plug.svg' import Gear from '@/svgs/settings-5-fill.svg' import Link from 'next/link' import { Status, isConfigured } from '@/wallets/common' import DraggableIcon from '@/svgs/draggable.svg' +import RecvIcon from '@/svgs/arrow-left-down-line.svg' +import SendIcon from '@/svgs/arrow-right-up-line.svg' +import useDarkMode from './dark-mode' +import { useEffect, useState } from 'react' + +const statusToClass = status => { + switch (status) { + case Status.Enabled: return styles.success + case Status.Disabled: return styles.disabled + case Status.Error: return styles.error + case Status.Warning: return styles.warning + } +} export default function WalletCard ({ wallet, draggable, onDragStart, onDragEnter, onDragEnd, onTouchStart, sourceIndex, targetIndex, index }) { - const { card: { title, badges } } = wallet.def + const [dark] = useDarkMode() + const { card: { title, image } } = wallet.def + const [imgSrc, setImgSrc] = useState(image?.src) - let indicator = styles.disabled - switch (wallet.status) { - case Status.Enabled: - indicator = styles.success - break - default: - indicator = styles.disabled - break - } + useEffect(() => { + if (!imgSrc) return + // wallet.png <-> wallet-dark.png + setImgSrc(dark ? image?.src.replace(/\.([a-z]{3})$/, '-dark.$1') : image?.src) + }, [dark]) return (
- {wallet.status === Status.Enabled && } -
+
+ {wallet.status.any !== Status.Disabled && } + {wallet.support.recv && } + {wallet.support.send && } +
- {title} - - {badges?.map( - badge => { - let style = '' - switch (badge) { - case 'receive': style = styles.receive; break - case 'send': style = styles.send; break - } - return ( - - {badge} - - ) - })} - +
+ {image + ? {title} + : {title}} +
diff --git a/components/wallet-logger.js b/components/wallet-logger.js index e709443c..7beadeb0 100644 --- a/components/wallet-logger.js +++ b/components/wallet-logger.js @@ -174,9 +174,12 @@ export function useWalletLogger (wallet, setLogs) { payment_hash: decoded.tagsObject.payment_hash, description: decoded.tagsObject.description, created_at: new Date(decoded.timestamp * 1000).toISOString(), - expires_at: new Date(decoded.timeExpireDate * 1000).toISOString() + expires_at: new Date(decoded.timeExpireDate * 1000).toISOString(), + // payments should affect wallet status + status: true } } + context.send = true appendLog(wallet, level, message, context) console[level !== 'error' ? 'info' : 'error'](`[${tag(wallet)}]`, message) diff --git a/pages/settings/wallets/[wallet].js b/pages/settings/wallets/[wallet].js index 891307ce..cbaa0173 100644 --- a/pages/settings/wallets/[wallet].js +++ b/pages/settings/wallets/[wallet].js @@ -67,10 +67,16 @@ export default function WalletSettings () { } }, [wallet.def]) + const { card: { image, title, subtitle } } = wallet?.def || { card: {} } + return ( -

{wallet?.def.card.title}

-
{wallet?.def.card.subtitle}
+ {image + ? typeof image === 'object' + ? {title} + : {title} + :

{title}

} +
{subtitle}
} diff --git a/public/wallets/blink-dark.svg b/public/wallets/blink-dark.svg new file mode 100644 index 00000000..b62d73f3 --- /dev/null +++ b/public/wallets/blink-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/wallets/blink.svg b/public/wallets/blink.svg new file mode 100644 index 00000000..01dadddd --- /dev/null +++ b/public/wallets/blink.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/wallets/cln-dark.svg b/public/wallets/cln-dark.svg new file mode 100644 index 00000000..4a51b36b --- /dev/null +++ b/public/wallets/cln-dark.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/wallets/cln.svg b/public/wallets/cln.svg new file mode 100644 index 00000000..0e92b7e4 --- /dev/null +++ b/public/wallets/cln.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/wallets/lnbits-dark.svg b/public/wallets/lnbits-dark.svg new file mode 100644 index 00000000..53120791 --- /dev/null +++ b/public/wallets/lnbits-dark.svg @@ -0,0 +1,13 @@ + + Group 6 + + + + + + + + + + + \ No newline at end of file diff --git a/public/wallets/lnbits.svg b/public/wallets/lnbits.svg new file mode 100644 index 00000000..97a2ec17 --- /dev/null +++ b/public/wallets/lnbits.svg @@ -0,0 +1,13 @@ + + Group 6 + + + + + + + + + + + \ No newline at end of file diff --git a/public/wallets/lnd-dark.png b/public/wallets/lnd-dark.png new file mode 100644 index 00000000..169d2772 Binary files /dev/null and b/public/wallets/lnd-dark.png differ diff --git a/public/wallets/lnd.png b/public/wallets/lnd.png new file mode 100644 index 00000000..169d2772 Binary files /dev/null and b/public/wallets/lnd.png differ diff --git a/public/wallets/phoenixd-dark.png b/public/wallets/phoenixd-dark.png new file mode 100644 index 00000000..93aa9336 Binary files /dev/null and b/public/wallets/phoenixd-dark.png differ diff --git a/public/wallets/phoenixd.png b/public/wallets/phoenixd.png new file mode 100644 index 00000000..59d1c7cc Binary files /dev/null and b/public/wallets/phoenixd.png differ diff --git a/scripts/genwallet.sh b/scripts/genwallet.sh index 5d0556c5..e14609ec 100644 --- a/scripts/genwallet.sh +++ b/scripts/genwallet.sh @@ -58,7 +58,6 @@ $(todo) export const card = { title: '$wallet', subtitle: '', - badges: [] } $(todo) diff --git a/styles/wallet.module.css b/styles/wallet.module.css index 133734d4..f968297f 100644 --- a/styles/wallet.module.css +++ b/styles/wallet.module.css @@ -20,7 +20,7 @@ height: 180px; } -.cardMeta { +.indicators { position: absolute; width: 100%; display: grid; @@ -59,23 +59,20 @@ } .indicator { - width: 10px; - height: 10px; - border-radius: 50%; + width: 14px; + height: 14px; } .indicator.success { color: var(--bs-green) !important; background-color: var(--bs-green) !important; border: 1px solid var(--bs-success); - filter: drop-shadow(0 0 2px #20c997); } .indicator.error { color: var(--bs-red) !important; background-color: var(--bs-red) !important; border: 1px solid var(--bs-danger); - filter: drop-shadow(0 0 2px #c92020); } .indicator.warning { diff --git a/svgs/arrow-left-down-line.svg b/svgs/arrow-left-down-line.svg new file mode 100644 index 00000000..81d43660 --- /dev/null +++ b/svgs/arrow-left-down-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svgs/arrow-right-up-line.svg b/svgs/arrow-right-up-line.svg new file mode 100644 index 00000000..93d2f337 --- /dev/null +++ b/svgs/arrow-right-up-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/wallets/README.md b/wallets/README.md index 166900f4..9ad23f8a 100644 --- a/wallets/README.md +++ b/wallets/README.md @@ -152,9 +152,9 @@ The card title. The subtitle that is shown below the title if you enter the configuration form of a wallet. -- `badges: string[]` +- `image: { src: string, ... }` -The badges that are shown inside the card. +The image props that will be used to show an image inside the card. Should contain at least the `src` property. ### client.js diff --git a/wallets/blink/index.js b/wallets/blink/index.js index 65304369..d870592c 100644 --- a/wallets/blink/index.js +++ b/wallets/blink/index.js @@ -67,5 +67,5 @@ export const fields = [ export const card = { title: 'Blink', subtitle: 'use [Blink](https://blink.sv/) for payments', - badges: ['send', 'receive'] + image: { src: '/wallets/blink.svg' } } diff --git a/wallets/cln/index.js b/wallets/cln/index.js index 14525f97..dc8523d1 100644 --- a/wallets/cln/index.js +++ b/wallets/cln/index.js @@ -63,5 +63,5 @@ export const fields = [ export const card = { title: 'CLN', subtitle: 'autowithdraw to your Core Lightning node via [CLNRest](https://docs.corelightning.org/docs/rest)', - badges: ['receive'] + image: { src: '/wallets/cln.svg' } } diff --git a/wallets/common.js b/wallets/common.js index c88b4655..c36d70aa 100644 --- a/wallets/common.js +++ b/wallets/common.js @@ -2,7 +2,9 @@ import walletDefs from '@/wallets/client' export const Status = { Enabled: 'Enabled', - Disabled: 'Disabled' + Disabled: 'Disabled', + Error: 'Error', + Warning: 'Warning' } export function getWalletByName (name) { @@ -89,12 +91,24 @@ function isReceiveConfigured ({ def, config }) { return fields.length > 0 && checkFields({ fields, config }) } +export function supportsSend ({ def, config }) { + return !!def.sendPayment +} + +export function supportsReceive ({ def, config }) { + return def.fields.some(f => f.serverOnly) +} + export function canSend ({ def, config }) { - return !!def.sendPayment && isSendConfigured({ def, config }) + return ( + supportsSend({ def, config }) && + isSendConfigured({ def, config }) && + (def.requiresConfig || config?.enabled) + ) } export function canReceive ({ def, config }) { - return def.fields.some(f => f.serverOnly) && isReceiveConfigured({ def, config }) + return supportsReceive({ def, config }) && isReceiveConfigured({ def, config }) } export function siftConfig (fields, config) { @@ -161,3 +175,31 @@ export async function saveWalletLocally (name, config, userId) { const storageKey = getStorageKey(name, userId) window.localStorage.setItem(storageKey, JSON.stringify(config)) } + +export const statusFromLog = (wallet, logs) => { + if (wallet.status.any === Status.Disabled) return wallet + + // override status depending on if there have been warnings or errors in the logs recently + // find first log from which we can derive status (logs are sorted by recent first) + const walletLogs = logs.filter(l => l.wallet === wallet.def.name) + const sendLevel = walletLogs.find(l => l.context?.status && l.context?.send)?.level + const recvLevel = walletLogs.find(l => l.context?.status && l.context?.recv)?.level + + const levelToStatus = (level) => { + switch (level?.toLowerCase()) { + case 'ok': + case 'success': return Status.Enabled + case 'error': return Status.Error + case 'warn': return Status.Warning + } + } + + return { + ...wallet, + status: { + ...wallet.status, + send: levelToStatus(sendLevel) || wallet.status.send, + recv: levelToStatus(recvLevel) || wallet.status.recv + } + } +} diff --git a/wallets/config.js b/wallets/config.js index 13011613..6227605c 100644 --- a/wallets/config.js +++ b/wallets/config.js @@ -54,7 +54,7 @@ export function useWalletConfigurator (wallet) { if (transformedConfig) { serverConfig = Object.assign(serverConfig, transformedConfig) } - } else { + } else if (wallet.def.requiresConfig) { throw new Error('configuration must be able to send or receive') } diff --git a/wallets/index.js b/wallets/index.js index 719e230f..c2e83eed 100644 --- a/wallets/index.js +++ b/wallets/index.js @@ -3,9 +3,9 @@ import { SET_WALLET_PRIORITY, WALLETS } from '@/fragments/wallet' import { SSR } from '@/lib/constants' import { useApolloClient, useMutation, useQuery } from '@apollo/client' import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' -import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend, isConfigured, upsertWalletVariables, siftConfig, saveWalletLocally } from './common' +import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend, isConfigured, upsertWalletVariables, siftConfig, saveWalletLocally, canReceive, supportsReceive, supportsSend, statusFromLog } from './common' import useVault from '@/components/vault/use-vault' -import { useWalletLogger } from '@/components/wallet-logger' +import { useWalletLogger, useWalletLogs } from '@/components/wallet-logger' import { decode as bolt11Decode } from 'bolt11' import walletDefs from '@/wallets/client' import { generateMutation } from './graphql' @@ -67,6 +67,7 @@ export function WalletsProvider ({ children }) { const [setWalletPriority] = useMutation(SET_WALLET_PRIORITY) const [serverWallets, setServerWallets] = useState([]) const client = useApolloClient() + const { logs } = useWalletLogs() const { data, refetch } = useQuery(WALLETS, SSR ? {} : { nextFetchPolicy: 'cache-and-network' }) @@ -111,7 +112,10 @@ export function WalletsProvider ({ children }) { const merged = {} for (const wallet of [...walletDefsOnly, ...localWallets, ...serverWallets]) { merged[wallet.def.name] = { - def: wallet.def, + def: { + ...wallet.def, + requiresConfig: wallet.def.fields.length > 0 + }, config: { ...merged[wallet.def.name]?.config, ...Object.fromEntries( @@ -128,8 +132,21 @@ export function WalletsProvider ({ children }) { // sort by priority, then add status field return Object.values(merged) .sort(walletPrioritySort) - .map(w => ({ ...w, status: w.config?.enabled ? Status.Enabled : Status.Disabled })) - }, [serverWallets, localWallets]) + .map(w => { + return { + ...w, + support: { + recv: supportsReceive(w), + send: supportsSend(w) + }, + status: { + any: w.config?.enabled && isConfigured(w) ? Status.Enabled : Status.Disabled, + send: w.config?.enabled && canSend(w) ? Status.Enabled : Status.Disabled, + recv: w.config?.enabled && canReceive(w) ? Status.Enabled : Status.Disabled + } + } + }).map(w => statusFromLog(w, logs)) + }, [serverWallets, localWallets, logs]) const settings = useMemo(() => { return { diff --git a/wallets/lightning-address/index.js b/wallets/lightning-address/index.js index e4c16bc1..e2bb7e52 100644 --- a/wallets/lightning-address/index.js +++ b/wallets/lightning-address/index.js @@ -22,6 +22,5 @@ export const fields = [ export const card = { title: 'lightning address', - subtitle: 'autowithdraw to a lightning address', - badges: ['receive'] + subtitle: 'autowithdraw to a lightning address' } diff --git a/wallets/lnbits/index.js b/wallets/lnbits/index.js index edcadead..492086f7 100644 --- a/wallets/lnbits/index.js +++ b/wallets/lnbits/index.js @@ -55,5 +55,5 @@ export const fields = [ export const card = { title: 'LNbits', subtitle: 'use [LNbits](https://lnbits.com/) for payments', - badges: ['send', 'receive'] + image: { src: '/wallets/lnbits.svg' } } diff --git a/wallets/lnc/client.js b/wallets/lnc/client.js index 72eab585..03f04152 100644 --- a/wallets/lnc/client.js +++ b/wallets/lnc/client.js @@ -38,11 +38,11 @@ export async function testSendPayment (credentials, { logger }) { logger.info('connecting ...') await lnc.connect() - logger.ok('connected') + logger.info('connected') logger.info('validating permissions ...') await validateNarrowPerms(lnc) - logger.ok('permissions ok') + logger.info('permissions ok') return lnc.credentials.credentials } finally { diff --git a/wallets/lnc/index.js b/wallets/lnc/index.js index 395762bf..c039678b 100644 --- a/wallets/lnc/index.js +++ b/wallets/lnc/index.js @@ -60,6 +60,5 @@ export const fields = [ export const card = { title: 'LNC', - subtitle: 'use Lightning Node Connect for LND payments', - badges: ['send', 'budgetable'] + subtitle: 'use Lightning Node Connect for LND payments' } diff --git a/wallets/lnd/index.js b/wallets/lnd/index.js index e1d6395c..dc55aa84 100644 --- a/wallets/lnd/index.js +++ b/wallets/lnd/index.js @@ -50,5 +50,5 @@ export const fields = [ export const card = { title: 'LND', subtitle: 'autowithdraw to your Lightning Labs node', - badges: ['receive'] + image: { src: '/wallets/lnd.png' } } diff --git a/wallets/nwc/index.js b/wallets/nwc/index.js index 49794d92..5bab2300 100644 --- a/wallets/nwc/index.js +++ b/wallets/nwc/index.js @@ -30,8 +30,7 @@ export const fields = [ export const card = { title: 'NWC', - subtitle: 'use Nostr Wallet Connect for payments', - badges: ['send', 'receive', 'budgetable'] + subtitle: 'use Nostr Wallet Connect for payments' } export async function nwcCall ({ nwcUrl, method, params }, { logger, timeout } = {}) { diff --git a/wallets/phoenixd/index.js b/wallets/phoenixd/index.js index 7bbec9eb..ad7f19ee 100644 --- a/wallets/phoenixd/index.js +++ b/wallets/phoenixd/index.js @@ -38,5 +38,5 @@ export const fields = [ export const card = { title: 'phoenixd', subtitle: 'use [phoenixd](https://phoenix.acinq.co/server) for payments', - badges: ['send', 'receive'] + image: { src: '/wallets/phoenixd.png' } } diff --git a/wallets/webln/index.js b/wallets/webln/index.js index e59f51bc..cce91750 100644 --- a/wallets/webln/index.js +++ b/wallets/webln/index.js @@ -12,6 +12,5 @@ export const fields = [] export const card = { title: 'WebLN', - subtitle: 'use a [WebLN provider](https://www.webln.guide/ressources/webln-providers) for payments', - badges: ['send'] + subtitle: 'use a [WebLN provider](https://www.webln.guide/ressources/webln-providers) for payments' }