diff options
author | Özgür Kesim <oec-taler@kesim.org> | 2023-10-06 16:33:05 +0200 |
---|---|---|
committer | Özgür Kesim <oec-taler@kesim.org> | 2023-10-06 16:33:05 +0200 |
commit | fe7b51ef2736edbf04f5bbd9d19f2a2d04baccc2 (patch) | |
tree | 66c68c8d6a666f6e74dc663c9ee4f07879f6626c /packages/demobank-ui/src/pages/BankFrame.tsx | |
parent | 35611f0bf9cf67638b171c2a300fab1797d3d8f0 (diff) | |
parent | 97d7be7503168f4f3bbd05905d32aa76ca1636b2 (diff) |
Merge branch 'master' into age-withdraw
Diffstat (limited to 'packages/demobank-ui/src/pages/BankFrame.tsx')
-rw-r--r-- | packages/demobank-ui/src/pages/BankFrame.tsx | 609 |
1 files changed, 370 insertions, 239 deletions
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index dc61f1302..6ab6ba3e4 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -14,283 +14,362 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Logger, TranslatedString } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { ComponentChildren, Fragment, h, VNode } from "preact"; -import { StateUpdater, useEffect, useState } from "preact/hooks"; -import talerLogo from "../assets/logo-white.svg"; -import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js"; +import { Amounts, Logger, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util"; +import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { useEffect, useErrorBoundary, useState } from "preact/hooks"; +import logo from "../assets/logo-2021.svg"; +import { Attention } from "../components/Attention.js"; +import { CopyButton } from "../components/CopyButton.js"; +import { LangSelector } from "../components/LangSelector.js"; import { useBackendContext } from "../context/backend.js"; -import { useBusinessAccountDetails } from "../hooks/circuit.js"; -import { bankUiSettings } from "../settings.js"; +import { useAccountDetails } from "../hooks/access.js"; import { useSettings } from "../hooks/settings.js"; -import { ErrorMessage, onNotificationUpdate } from "../hooks/notification.js"; +import { bankUiSettings } from "../settings.js"; +import { RenderAmount } from "./PaytoWireTransferForm.js"; -const IS_PUBLIC_ACCOUNT_ENABLED = false; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; const versionText = VERSION ? GIT_HASH - ? `Version ${VERSION} (${GIT_HASH.substring(0, 8)})` + ? <a href={`https://git.taler.net/wallet-core.git/tree/?id=${GIT_HASH}`} target="_blank" rel="noreferrer noopener"> + Version {VERSION} ({GIT_HASH.substring(0, 8)}) + </a> : VERSION : ""; -const logger = new Logger("BankFrame"); - -function MaybeBusinessButton({ - account, - onClick, -}: { - account: string; - onClick: () => void; -}): VNode { - const { i18n } = useTranslationContext(); - const result = useBusinessAccountDetails(account); - if (!result.ok) return <Fragment />; - return ( - <a - href="#" - class="pure-button pure-button-primary" - onClick={(e) => { - e.preventDefault(); - onClick(); - }} - >{i18n.str`Business Profile`}</a> - ); -} export function BankFrame({ children, - goToBusinessAccount, + account, }: { + account?: string, children: ComponentChildren; - goToBusinessAccount?: () => void; }): VNode { const { i18n } = useTranslationContext(); const backend = useBackendContext(); const [settings, updateSettings] = useSettings(); + const [open, setOpen] = useState(false) - const demo_sites = []; - for (const i in bankUiSettings.demoSites) - demo_sites.push( - <a href={bankUiSettings.demoSites[i][1]}> - {bankUiSettings.demoSites[i][0]} - </a>, - ); + const [error, resetError] = useErrorBoundary(); - return ( - <Fragment> - <header - class="demobar" - style="display: flex; flex-direction: row; justify-content: space-between;" - > - <a href="#main" class="skip">{i18n.str`Skip to main content`}</a> - <div style="max-width: 50em; margin-left: 2em; margin-right: 2em;"> - <h1> - <span class="it"> - <a href="/">{bankUiSettings.bankName}</a> - </span> - </h1> - {maybeDemoContent( - <p> - {IS_PUBLIC_ACCOUNT_ENABLED ? ( - <i18n.Translate> - This part of the demo shows how a bank that supports Taler - directly would work. In addition to using your own bank - account, you can also see the transaction history of some{" "} - <a href="/public-accounts">Public Accounts</a>. - </i18n.Translate> - ) : ( - <i18n.Translate> - This part of the demo shows how a bank that supports Taler - directly would work. - </i18n.Translate> - )} - </p>, - )} - </div> - </header> - <div style="display:flex; flex-direction: column;" class="navcontainer"> - <nav class="demolist"> - {maybeDemoContent(<Fragment>{demo_sites}</Fragment>)} - {backend.state.status === "loggedIn" ? ( - <Fragment> - {goToBusinessAccount && !backend.state.isUserAdministrator ? ( - <MaybeBusinessButton - account={backend.state.username} - onClick={goToBusinessAccount} - /> - ) : undefined} - - <LangSelector /> - - <a - href="#" - class="pure-button logout-button" - onClick={() => { - backend.logOut(); - updateSettings("currentWithdrawalOperationId", undefined); - }} - >{i18n.str`Logout`}</a> - </Fragment> - ) : undefined} - </nav> - </div> - <section id="main" class="content"> - <StatusBanner /> - {children} - </section> - <section id="footer" class="footer"> - <hr /> - <div> - <p> - You can learn more about GNU Taler on our{" "} - <a href="https://taler.net">main website</a>. - </p> - </div> - <div style="flex-grow:1" /> - <p> - Copyright © 2014—2022 Taler Systems SA. {versionText}{" "} - <TestingTag /> - </p> - </section> - </Fragment> - ); -} + useEffect(() => { + if (error) { + const desc = (error instanceof Error ? error.stack : String(error)) as TranslatedString + if (error instanceof Error) { + notifyException(i18n.str`Internal error, please report.`, error) + } else { + notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString) + } + resetError() + } + }, [error]) -function maybeDemoContent(content: VNode): VNode { - if (bankUiSettings.showDemoNav) { - return content; + const demo_sites = []; + if (bankUiSettings.demoSites) { + for (const i in bankUiSettings.demoSites) + demo_sites.push( + <a href={bankUiSettings.demoSites[i][1]}> + {bankUiSettings.demoSites[i][0]} + </a>, + ); } - return <Fragment />; -} -export function ErrorBannerFloat({ - error, - onClear, -}: { - error: ErrorMessage; - onClear?: () => void; -}): VNode { - return ( - <div - style={{ - position: "fixed", - top: 10, - zIndex: 200, - width: "90%", - }} - > - <ErrorBanner error={error} onClear={onClear} /> - </div> - ); -} + return (<div class="min-h-full flex flex-col m-0" style="min-height: 100vh;"> + <div class="bg-indigo-600 pb-32"> + <nav class=""> + <div class="mx-auto max-w-7xl px-2 sm:px-4 lg:px-8"> + <div class="relative flex h-16 items-center justify-between "> + <div class="flex items-center px-2 lg:px-0"> + <div class="flex-shrink-0 bg-white rounded-lg"> + <a href={bankUiSettings.iconLinkURL ?? "#"}> + <img + class="h-8 w-auto" + src={logo} + alt="Taler" + style={{ height: "1.5rem", margin: ".5rem" }} + /> + </a> + </div> + {bankUiSettings.demoSites && + <div class="hidden sm:block lg:ml-10 "> + <div class="flex space-x-4"> + {/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */} + {bankUiSettings.demoSites.map(([name, url]) => { + return <a href={url} class="text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md py-2 px-3 text-sm font-medium">{name}</a> + })} + </div> + </div> + } + </div> -function ErrorBanner({ - error, - onClear, -}: { - error: ErrorMessage; - onClear?: () => void; -}): VNode { - return ( - <div - class="informational informational-fail" - style={{ - marginTop: 8, - paddingLeft: 16, - paddingRight: 16, - }} - > - <div style={{ display: "flex", justifyContent: "space-between" }}> - <p> - <b>{error.title}</b> - </p> - <div style={{ marginTop: "auto", marginBottom: "auto" }}> - {onClear && ( - <input - type="button" - class="pure-button" - value="Clear" - onClick={(e) => { - e.preventDefault(); - onClear(); - }} - /> - )} + <div class="flex"> + <button type="button" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false" + onClick={(e) => { + setOpen(!open) + }}> + <span class="absolute -inset-0.5"></span> + <span class="sr-only">Open settings</span> + <svg class="block h-10 w-10" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> + </svg> + </button> + </div> + </div> </div> - </div> - <p>{error.description}</p> - </div> - ); -} -function StatusBanner(): VNode | null { - const [info, setInfo] = useState<TranslatedString>(); - const [error, setError] = useState<ErrorMessage>(); - useEffect(() => { - return onNotificationUpdate((newValue) => { - if (newValue === undefined) { - setInfo(undefined); - setError(undefined); - } else { - if (newValue.type === "error") { - setError(newValue.error); - } else { - setInfo(newValue.info); + {open && + <div class="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true" + onClick={() => { + setOpen(false) + }}> + <div class="fixed inset-0"></div> + + <div class="fixed inset-0 overflow-hidden"> + <div class="absolute inset-0 overflow-hidden"> + <div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10"> + <div class="pointer-events-auto w-screen max-w-md" > + <div class="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl" onClick={(e) => { + //do not trigger close if clicking inside the sidebar + e.stopPropagation(); + }}> + <div class="px-4 sm:px-6" > + <div class="flex items-start justify-between" > + <h2 class="text-base font-semibold leading-6 text-gray-900" id="slide-over-title"> + <i18n.Translate>Preferences</i18n.Translate> + </h2> + <div class="ml-3 flex h-7 items-center"> + <button type="button" class="relative rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" + onClick={(e) => { + setOpen(false) + }} + + > + <span class="absolute -inset-2.5"></span> + <span class="sr-only"> + <i18n.Translate>Close panel</i18n.Translate> + </span> + <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + </div> + </div> + <div class="relative mt-6 flex-1 px-4 sm:px-6"> + <nav class="flex flex-1 flex-col" aria-label="Sidebar"> + <ul role="list" class="flex flex-1 flex-col gap-y-7"> + <li> + <a href="#" + class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold" + onClick={() => { + backend.logOut(); + setOpen(false) + updateSettings("currentWithdrawalOperationId", undefined); + }} + > + <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> + </svg> + <i18n.Translate>Log out</i18n.Translate> + </a> + </li> + <li> + <LangSelector /> + </li> + {bankUiSettings.demoSites && + <li class="sm:hidden"> + <div class="text-xs font-semibold leading-6 text-gray-400"> + <i18n.Translate>Sites</i18n.Translate> + </div> + <ul role="list" class="-mx-2 mt-2 space-y-1"> + {bankUiSettings.demoSites.map(([name, url]) => { + return <li> + <a href={url} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> + <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">></span> + <span class="truncate">{name}</span> + </a> + </li> + })} + </ul> + </li> + } + <li> + <ul role="list" class="space-y-1"> + <li class="mt-2"> + <div class="flex items-center justify-between"> + <span class="flex flex-grow flex-col"> + <span class="text-sm text-black font-medium leading-6 " id="availability-label"> + <i18n.Translate>Show withdrawal confirmation</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={settings.showWithdrawalSuccess} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description" + + onClick={() => { + updateSettings("showWithdrawalSuccess", !settings.showWithdrawalSuccess); + }}> + <span aria-hidden="true" data-enabled={settings.showWithdrawalSuccess} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span> + </button> + </div> + </li> + <li class="mt-2"> + <div class="flex items-center justify-between"> + <span class="flex flex-grow flex-col"> + <span class="text-sm text-black font-medium leading-6 " id="availability-label"> + <i18n.Translate>Show demo description</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={settings.showDemoDescription} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description" + + onClick={() => { + updateSettings("showDemoDescription", !settings.showDemoDescription); + }}> + <span aria-hidden="true" data-enabled={settings.showDemoDescription} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span> + </button> + </div> + </li> + <li class="mt-2"> + <div class="flex items-center justify-between"> + <span class="flex flex-grow flex-col"> + <span class="text-sm text-black font-medium leading-6 " id="availability-label"> + <i18n.Translate>Show debug info</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={settings.showDebugInfo} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description" + + onClick={() => { + updateSettings("showDebugInfo", !settings.showDebugInfo); + }}> + <span aria-hidden="true" data-enabled={settings.showDebugInfo} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span> + </button> + </div> + </li> + <li class="mt-2"> + <div class="flex items-center justify-between"> + <span class="flex flex-grow flex-col"> + <span class="text-sm text-black font-medium leading-6 " id="availability-label"> + <i18n.Translate>Show install wallet first</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={settings.showInstallWallet} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description" + onClick={() => { + updateSettings("showInstallWallet", !settings.showInstallWallet); + }}> + <span aria-hidden="true" data-enabled={settings.showInstallWallet} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span> + </button> + </div> + </li> + <li class="mt-2"> + <div class="flex items-center justify-between"> + <span class="flex flex-grow flex-col"> + <span class="text-sm text-black font-medium leading-6 " id="availability-label"> + <i18n.Translate>Use fast withdrawal</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={settings.fastWithdrawal} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description" + onClick={() => { + updateSettings("fastWithdrawal", !settings.fastWithdrawal); + }}> + <span aria-hidden="true" data-enabled={settings.fastWithdrawal} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span> + </button> + </div> + </li> + </ul> + </li> + </ul> + </nav> + </div> + </div> + </div> + </div> + </div> + </div> + </div> } - } - }); - }, []); - return ( - <div - style={{ - position: "fixed", - top: 10, - zIndex: 200, - width: "90%", - }} - > - {!info ? undefined : ( - <div - class="informational informational-ok" - style={{ marginTop: 8, paddingLeft: 16, paddingRight: 16 }} - > - <div style={{ display: "flex", justifyContent: "space-between" }}> - <p> - <b>{info}</b> - </p> - <div> - <input - type="button" - class="pure-button" - value="Clear" - onClick={async () => { - setInfo(undefined); - }} - /> + </nav > + + {account && + <header class="py-5 border-t border-indigo-300 border-opacity-25 bg-indigo-600 lg:border-t lg:border-indigo-400 lg:border-opacity-25"> + <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> + <div class=" flex flex-wrap items-center justify-between sm:flex-nowrap"> + <h3 class="text-2xl font-bold tracking-tight text-white"><WelcomeAccount account={account} /></h3> + <div> + <h3 class="text-2xl font-bold tracking-tight text-white"><AccountBalance account={account} /></h3> + </div> </div> </div> + + </header> + } + </div > + + <StatusBanner /> + <main class="-mt-32 flex-1"> + <div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8"> + <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6"> + {children} </div> - )} - {!error ? undefined : ( - <ErrorBanner - error={error} - onClear={() => { - setError(undefined); - }} - /> - )} - </div> + </div> + </main> + + <Footer /> + </div > + ); } +function MaybeShowDebugInfo({ info }: { info: any }): VNode { + const [settings] = useSettings() + if (settings.showDebugInfo) { + return <pre class="whitespace-break-spaces "> + {info} + </pre> + } + return <Fragment /> +} + + +function StatusBanner(): VNode { + const notifs = useNotifications() + if (notifs.length === 0) return <Fragment /> + return <div class="fixed z-20 w-full p-4"> { + notifs.map(n => { + switch (n.message.type) { + case "error": + return <Attention type="danger" title={n.message.title} onClose={() => { + n.remove() + }}> + {n.message.description && + <div class="mt-2 text-sm text-red-700"> + {n.message.description} + </div> + } + <MaybeShowDebugInfo info={n.message.debug} /> + {/* <a href="#" class="text-gray-500"> + show debug info + </a> + {n.message.debug && + <div class="mt-2 text-sm text-red-700 font-mono break-all"> + {n.message.debug} + </div> + } */} + </Attention> + case "info": + return <Attention type="success" title={n.message.title} onClose={() => { + n.remove(); + }} /> + } + })} + </div> + +} + function TestingTag(): VNode { const testingUrl = localStorage.getItem("bank-base-url"); if (!testingUrl) return <Fragment />; return ( - <span style={{ color: "gray" }}> + <p class="text-xs leading-5 text-gray-300"> Testing with {testingUrl}{" "} <a href="" @@ -302,6 +381,58 @@ function TestingTag(): VNode { > stop testing </a> - </span> + </p> + ); +} + +function Footer() { + const { i18n } = useTranslationContext() + return ( + <footer class="bottom-4 mb-4"> + <div class="mt-8 mx-8 md:order-1 md:mt-0"> + <div> + <p class="text-xs leading-5 text-gray-400"> + <i18n.Translate> + Learn more about <a target="_blank" rel="noreferrer noopener" class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a> + </i18n.Translate> + </p> + </div> + <div style="flex-grow:1" /> + <p class="text-xs leading-5 text-gray-400"> + Copyright © 2014—2023 Taler Systems SA. {versionText}{" "} + <TestingTag /> + </p> + </div> + </footer> ); } + +function WelcomeAccount({ account }: { account: string }): VNode { + const { i18n } = useTranslationContext(); + + const result = useAccountDetails(account); + if (!result.ok) return <div /> + + const payto = parsePaytoUri(result.data.payto_uri) + if (!payto) return <div /> + + const accountNumber = !payto.isKnown ? undefined : payto.targetType === "iban" ? payto.iban : payto.targetType === "x-taler-bank" ? payto.account : undefined; + return <i18n.Translate> + Welcome, {account} {accountNumber !== undefined ? + <span> + (<a href={result.data.payto_uri}>{accountNumber}</a> <CopyButton getContent={() => result.data.payto_uri} />) + </span> + : <Fragment />}! + </i18n.Translate> + +} + +function AccountBalance({ account }: { account: string }): VNode { + const result = useAccountDetails(account); + if (!result.ok) return <div /> + + return <RenderAmount + value={Amounts.parseOrThrow(result.data.balance.amount)} + negative={result.data.balance.credit_debit_indicator === "debit"} + /> +} |