diff --git a/pages/wallets/[...slug].js b/pages/wallets/[...slug].js index 31059d72..d453545f 100644 --- a/pages/wallets/[...slug].js +++ b/pages/wallets/[...slug].js @@ -1,20 +1,33 @@ import { getGetServerSideProps } from '@/api/ssrApollo' +import { useData } from '@/components/use-data' import { WalletForms as WalletFormsComponent } from '@/wallets/client/components' +import { WALLET } from '@/wallets/client/fragments' +import { useDecryptedWallet } from '@/wallets/client/hooks' import { unurlify } from '@/wallets/lib/util' -import { useParams } from 'next/navigation' +import { useQuery } from '@apollo/client' +import { useRouter } from 'next/router' -export const getServerSideProps = getGetServerSideProps({ authRequired: true }) +const variablesFunc = params => { + const id = Number(params.slug[0]) + return !Number.isNaN(id) ? { id } : { name: unurlify(params.slug[0]) } +} +export const getServerSideProps = getGetServerSideProps({ query: WALLET, variables: variablesFunc, authRequired: true }) -export default function WalletForms () { - const params = useParams() - const walletName = unurlify(params.slug[0]) +export default function WalletForms ({ ssrData }) { + const router = useRouter() + const variables = variablesFunc(router.query) + // this will print the following warning in the console: + // Warning: fragment with name WalletTemplateFields already exists. + // graphql-tag enforces all fragment names across your application to be unique + // this is not a problem because the warning is only meant to avoid overwriting fragments but we're reusing it + const { data, refetch } = useQuery(WALLET, { variables }) + const dat = useData(data, ssrData) - // if the wallet name is a number, we are showing a configured wallet - // otherwise, we are showing a template - const isNumber = !Number.isNaN(Number(walletName)) - if (isNumber) { - return + const decryptedWallet = useDecryptedWallet(dat?.wallet) + const wallet = decryptedWallet ?? ssrData?.wallet + if (!wallet) { + return null } - return + return } diff --git a/wallets/client/components/card.js b/wallets/client/components/card.js index 26a061a6..be8678f7 100644 --- a/wallets/client/components/card.js +++ b/wallets/client/components/card.js @@ -7,7 +7,7 @@ import Link from 'next/link' import RecvIcon from '@/svgs/arrow-left-down-line.svg' import SendIcon from '@/svgs/arrow-right-up-line.svg' import DragIcon from '@/svgs/draggable.svg' -import { useWalletImage, useWalletSupport, useWalletStatus, WalletStatus } from '@/wallets/client/hooks' +import { useWalletImage, useWalletSupport, useWalletStatus, WalletStatus, useProtocolTemplates } from '@/wallets/client/hooks' import { isWallet, urlify, walletDisplayName } from '@/wallets/lib/util' import { Draggable } from '@/wallets/client/components' @@ -56,8 +56,14 @@ export function WalletCard ({ wallet, draggable = false, index, ...props }) { function WalletLink ({ wallet, children }) { const support = useWalletSupport(wallet) - const sendRecvParam = support.send ? 'send' : 'receive' - const href = '/wallets' + (isWallet(wallet) ? `/${wallet.id}` : `/${urlify(wallet.name)}`) + `/${sendRecvParam}` + const protocols = useProtocolTemplates(wallet) + const firstSend = protocols.find(p => p.send) + const firstRecv = protocols.find(p => !p.send) + + let href = '/wallets' + href += isWallet(wallet) ? `/${wallet.id}` : `/${urlify(wallet.name)}` + href += support.send ? `/send/${urlify(firstSend.name)}` : `/receive/${urlify(firstRecv.name)}` + return {children} } diff --git a/wallets/client/components/forms.js b/wallets/client/components/forms.js index f3f30c61..e9f3ad6a 100644 --- a/wallets/client/components/forms.js +++ b/wallets/client/components/forms.js @@ -1,4 +1,4 @@ -import { useEffect, useCallback, useMemo, createContext, useContext } from 'react' +import { useCallback, useMemo, createContext, useContext } from 'react' import { Button, InputGroup, Nav } from 'react-bootstrap' import Link from 'next/link' import { useParams, usePathname } from 'next/navigation' @@ -9,7 +9,7 @@ import styles from '@/styles/wallet.module.css' import navStyles from '@/styles/nav.module.css' import { Checkbox, Form, Input, PasswordInput, SubmitButton } from '@/components/form' import CancelButton from '@/components/cancel-button' -import { useWalletProtocolUpsert, useWalletProtocolRemove, useWalletQuery, TemplateLogsProvider } from '@/wallets/client/hooks' +import { useWalletProtocolUpsert, useWalletProtocolRemove, useProtocolTemplates, TemplateLogsProvider } from '@/wallets/client/hooks' import { useToast } from '@/components/toast' import Text from '@/components/text' import Info from '@/components/info' @@ -17,11 +17,7 @@ import classNames from 'classnames' const WalletFormsContext = createContext() -export function WalletForms ({ id, name }) { - // TODO(wallet-v2): handle loading and error states - const { data, refetch } = useWalletQuery({ name, id }) - const wallet = data?.wallet - +export function WalletForms ({ wallet, refetch }) { return (
@@ -81,9 +77,14 @@ function WalletFormSelector () { } function WalletSendRecvSelector () { + const wallet = useWallet() const path = useWalletPathname() + const protocols = useProtocolTemplates(wallet) const selected = useSendRecvParam() + const firstSend = protocols.find(p => p.send) + const firstRecv = protocols.find(p => !p.send) + // TODO(wallet-v2): if you click a nav link again, it will update the URL // but not run the effect again to select the first protocol by default return ( @@ -93,12 +94,12 @@ function WalletSendRecvSelector () { activeKey={selected} > - + SEND - + RECEIVE @@ -109,17 +110,11 @@ function WalletSendRecvSelector () { function WalletProtocolSelector () { const walletPath = useWalletPathname() const sendRecvParam = useSendRecvParam() + const selected = useWalletProtocolParam() const path = `${walletPath}/${sendRecvParam}` - const protocols = useWalletProtocols() - const selected = useWalletProtocolParam() - const router = useRouter() - - useEffect(() => { - if (!selected && protocols.length > 0) { - router.replace(`/${path}/${urlify(protocols[0].name)}`, null, { shallow: true }) - } - }, [path]) + const wallet = useWallet() + const protocols = useProtocolTemplates(wallet).filter(p => sendRecvParam === 'send' ? p.send : !p.send) if (protocols.length === 0) { // TODO(wallet-v2): let user know how to request support if the wallet actually does support sending @@ -179,7 +174,7 @@ function WalletProtocolForm () { return } // we just created a new user wallet from a template - router.replace(`/wallets/${upsert.id}/${sendRecvParam}`, null, { shallow: true }) + router.replace(`/wallets/${upsert.id}/${sendRecvParam}/${urlify(protocol.name)}`, null, { shallow: true }) toaster.success('wallet attached', { persistOnNavigate: true }) }, [upsertWalletProtocol, toaster, wallet, router]) @@ -303,17 +298,6 @@ function useWalletProtocolParam () { return name ? unurlify(name) : null } -function useWalletProtocols () { - const wallet = useWallet() - const sendRecvParam = useSendRecvParam() - if (!sendRecvParam) return [] - - const protocolFilter = p => sendRecvParam === 'send' ? p.send : !p.send - return isWallet(wallet) - ? wallet.template.protocols.filter(protocolFilter) - : wallet.protocols.filter(protocolFilter) -} - function useSelectedProtocol () { const wallet = useWallet() const sendRecvParam = useSendRecvParam() diff --git a/wallets/client/hooks/query.js b/wallets/client/hooks/query.js index 71cf7a0e..c196f067 100644 --- a/wallets/client/hooks/query.js +++ b/wallets/client/hooks/query.js @@ -1,5 +1,4 @@ import { - WALLET, UPSERT_WALLET_RECEIVE_BLINK, UPSERT_WALLET_RECEIVE_CLN_REST, UPSERT_WALLET_RECEIVE_LIGHTNING_ADDRESS, @@ -55,8 +54,7 @@ export function useWalletsQuery () { Promise.all( query.data?.wallets.map(w => decryptWallet(w)) ) - .then(wallets => wallets.map(protocolCheck)) - .then(wallets => wallets.map(undoFieldAlias)) + .then(wallets => wallets.map(server2Client)) .then(wallets => { setWallets(wallets) setError(null) @@ -79,41 +77,6 @@ export function useWalletsQuery () { }), [query, error, wallets]) } -function protocolCheck (wallet) { - if (isTemplate(wallet)) return wallet - - const protocols = wallet.protocols.map(protocol => { - return { - ...protocol, - enabled: protocol.enabled && protocolAvailable(protocol) - } - }) - - const sendEnabled = protocols.some(p => p.send && p.enabled) - const receiveEnabled = protocols.some(p => !p.send && p.enabled) - - return { - ...wallet, - send: !sendEnabled ? WalletStatus.DISABLED : wallet.send, - receive: !receiveEnabled ? WalletStatus.DISABLED : wallet.receive, - protocols - } -} - -function undoFieldAlias ({ id, ...wallet }) { - // Just like for encrypted fields, we have to use a field alias for the name field of templates - // because of https://github.com/graphql/graphql-js/issues/53. - // We undo this here so this only affects the GraphQL layer but not the rest of the code. - if (isTemplate(wallet)) { - return { ...wallet, name: id } - } - - if (!wallet.template) return wallet - - const { id: templateId, ...template } = wallet.template - return { id, ...wallet, template: { name: templateId, ...template } } -} - function useRefetchOnChange (refetch) { const { me } = useMe() const walletsUpdatedAt = useWalletsUpdatedAt() @@ -125,29 +88,62 @@ function useRefetchOnChange (refetch) { }, [refetch, me?.id, walletsUpdatedAt]) } -export function useWalletQuery ({ id, name }) { - const { me } = useMe() - const query = useQuery(WALLET, { variables: { id, name }, skip: !me }) - const [wallet, setWallet] = useState(null) - +export function useDecryptedWallet (wallet) { const { decryptWallet, ready } = useWalletDecryption() + const [decryptedWallet, setDecryptedWallet] = useState(server2Client(wallet)) useEffect(() => { - if (!query.data?.wallet || !ready) return - decryptWallet(query.data?.wallet) - .then(protocolCheck) - .then(undoFieldAlias) - .then(wallet => setWallet(wallet)) + if (!ready || !wallet) return + decryptWallet(wallet) + .then(server2Client) + .then(wallet => setDecryptedWallet(wallet)) .catch(err => { console.error('failed to decrypt wallet:', err) }) - }, [query.data, decryptWallet, ready]) + }, [decryptWallet, wallet, ready]) - return useMemo(() => ({ - ...query, - loading: !wallet, - data: wallet ? { wallet } : null - }), [query, wallet]) + return decryptedWallet +} + +function server2Client (wallet) { + // some protocols require a specific client environment + // e.g. WebLN requires a browser extension + function checkProtocolAvailability (wallet) { + if (isTemplate(wallet)) return wallet + + const protocols = wallet.protocols.map(protocol => { + return { + ...protocol, + enabled: protocol.enabled && protocolAvailable(protocol) + } + }) + + const sendEnabled = protocols.some(p => p.send && p.enabled) + const receiveEnabled = protocols.some(p => !p.send && p.enabled) + + return { + ...wallet, + send: !sendEnabled ? WalletStatus.DISABLED : wallet.send, + receive: !receiveEnabled ? WalletStatus.DISABLED : wallet.receive, + protocols + } + } + + // Just like for encrypted fields, we have to use a field alias for the name field of templates + // because of https://github.com/graphql/graphql-js/issues/53. + // We undo this here so this only affects the GraphQL layer but not the rest of the code. + function undoFieldAlias ({ id, ...wallet }) { + if (isTemplate(wallet)) { + return { ...wallet, name: id } + } + + if (!wallet.template) return wallet + + const { id: templateId, ...template } = wallet.template + return { id, ...wallet, template: { name: templateId, ...template } } + } + + return wallet ? undoFieldAlias(checkProtocolAvailability(wallet)) : wallet } export function useWalletProtocolUpsert (wallet, protocol) { diff --git a/wallets/client/hooks/wallet.js b/wallets/client/hooks/wallet.js index 8a439825..18447454 100644 --- a/wallets/client/hooks/wallet.js +++ b/wallets/client/hooks/wallet.js @@ -53,3 +53,9 @@ export function useWalletsUpdatedAt () { const { me } = useMe() return me?.privates?.walletsUpdatedAt } + +export function useProtocolTemplates (wallet) { + return useMemo(() => { + return isWallet(wallet) ? wallet.template.protocols : wallet.protocols + }, [wallet]) +} diff --git a/wallets/lib/util.js b/wallets/lib/util.js index 9c606adb..eabb754f 100644 --- a/wallets/lib/util.js +++ b/wallets/lib/util.js @@ -1,6 +1,7 @@ import * as yup from 'yup' import wallets from '@/wallets/lib/wallets.json' import protocols from '@/wallets/lib/protocols' +import { SSR } from '@/lib/constants' function walletJson (name) { return wallets.find(wallet => wallet.name === name) @@ -88,7 +89,7 @@ export function protocolFields ({ name, send }) { export function protocolAvailable ({ name, send }) { const { isAvailable } = protocol({ name, send }) - if (typeof isAvailable === 'function') { + if (!SSR && typeof isAvailable === 'function') { return isAvailable() } diff --git a/wallets/server/resolvers/wallet.js b/wallets/server/resolvers/wallet.js index e07368d5..c1902a25 100644 --- a/wallets/server/resolvers/wallet.js +++ b/wallets/server/resolvers/wallet.js @@ -96,6 +96,8 @@ async function wallet (parent, { id, name }, { me, models }) { protocols: true } }) + if (!wallet) throw new GqlInputError('wallet not found') + return mapWalletResolveTypes(wallet) }