taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 4028baaa968d4ed6991a1bd50e68afb0a9565b44
parent bd37a0b04123d734e1e3fae105f0d9c24279629f
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 22 Nov 2023 13:31:11 -0300

cashout

Diffstat:
Apackages/demobank-ui/src/Routing.tsx | 325+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/demobank-ui/src/components/Routing.tsx | 325-------------------------------------------------------------------------------
Mpackages/demobank-ui/src/components/Transactions/views.tsx | 4++--
Mpackages/demobank-ui/src/components/app.tsx | 2+-
Mpackages/demobank-ui/src/pages/LoginForm.tsx | 2+-
Mpackages/demobank-ui/src/pages/PaytoWireTransferForm.tsx | 7++++---
Mpackages/demobank-ui/src/pages/PublicHistoriesPage.tsx | 2+-
Mpackages/demobank-ui/src/pages/RegistrationPage.tsx | 2+-
Dpackages/demobank-ui/src/pages/ShowAccountDetails.tsx | 155-------------------------------------------------------------------------------
Dpackages/demobank-ui/src/pages/UpdateAccountPassword.tsx | 226-------------------------------------------------------------------------------
Apackages/demobank-ui/src/pages/WireTransfer.tsx | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/demobank-ui/src/pages/account/CashoutListForAccount.tsx | 40++++++++++++++++++++++++++++++++++++++++
Apackages/demobank-ui/src/pages/account/ShowAccountDetails.tsx | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/demobank-ui/src/pages/admin/Account.tsx | 53-----------------------------------------------------
Mpackages/demobank-ui/src/pages/admin/AccountForm.tsx | 10++++------
Mpackages/demobank-ui/src/pages/admin/AdminHome.tsx | 5++---
Dpackages/demobank-ui/src/pages/admin/CashoutListForAccount.tsx | 51---------------------------------------------------
Mpackages/demobank-ui/src/pages/admin/CreateNewAccount.tsx | 5+----
Mpackages/demobank-ui/src/pages/admin/RemoveAccount.tsx | 7+------
Mpackages/demobank-ui/src/pages/business/CreateCashout.tsx | 28+++++++++++++---------------
21 files changed, 830 insertions(+), 853 deletions(-)

diff --git a/packages/demobank-ui/src/Routing.tsx b/packages/demobank-ui/src/Routing.tsx @@ -0,0 +1,325 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { createHashHistory } from "history"; +import { Fragment, VNode, h } from "preact"; +import { Route, Router, route } from "preact-router"; +import { useEffect } from "preact/hooks"; +import { useBackendState } from "./hooks/backend.js"; +import { BankFrame } from "./pages/BankFrame.js"; +import { WithdrawalOperationPage } from "./pages/WithdrawalOperationPage.js"; +import { LoginForm } from "./pages/LoginForm.js"; +import { PublicHistoriesPage } from "./pages/PublicHistoriesPage.js"; +import { RegistrationPage } from "./pages/RegistrationPage.js"; +import { AdminHome } from "./pages/admin/AdminHome.js"; +import { CreateCashout } from "./pages/business/CreateCashout.js"; +import { bankUiSettings } from "./settings.js"; +import { ShowAccountDetails } from "./pages/account/ShowAccountDetails.js"; +import { UpdateAccountPassword } from "./pages/account/UpdateAccountPassword.js"; +import { RemoveAccount } from "./pages/admin/RemoveAccount.js"; +import { CreateNewAccount } from "./pages/admin/CreateNewAccount.js"; +import { CashoutListForAccount } from "./pages/account/CashoutListForAccount.js"; +import { ShowCashoutDetails } from "./pages/business/ShowCashoutDetails.js"; +import { WireTransfer } from "./pages/WireTransfer.js"; +import { AccountPage } from "./pages/AccountPage/index.js"; + +export function Routing(): VNode { + const history = createHashHistory(); + const backend = useBackendState(); + const { i18n } = useTranslationContext(); + + if (backend.state.status === "loggedOut") { + return <BankFrame > + <Router history={history}> + <Route + path="/login" + component={() => ( + <Fragment> + <div class="sm:mx-auto sm:w-full sm:max-w-sm"> + <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h2> + </div> + + <LoginForm + onRegister={() => { + route("/register"); + }} + /> + </Fragment> + )} + /> + <Route + path="/public-accounts" + component={() => <PublicHistoriesPage />} + /> + <Route + path="/operation/:wopid" + component={({ wopid }: { wopid: string }) => ( + <WithdrawalOperationPage + operationId={wopid} + onContinue={() => { + route("/account"); + }} + /> + )} + /> + {bankUiSettings.allowRegistrations && + <Route + path="/register" + component={() => ( + <RegistrationPage + onComplete={() => { + route("/account"); + }} + onCancel={() => { + route("/account"); + }} + /> + )} + /> + } + <Route default component={Redirect} to="/login" /> + </Router> + </BankFrame> + } + const { isUserAdministrator, username } = backend.state + + return ( + <BankFrame account={username}> + <Router history={history}> + <Route + path="/operation/:wopid" + component={({ wopid }: { wopid: string }) => ( + <WithdrawalOperationPage + operationId={wopid} + onContinue={() => { + route("/account"); + }} + /> + )} + /> + <Route + path="/public-accounts" + component={() => <PublicHistoriesPage />} + /> + + <Route + path="/new-account" + component={() => <CreateNewAccount + onCancel={() => { + route("/account") + }} + onCreateSuccess={() => { + route("/account") + }} + />} + /> + + <Route + path="/profile/:account/details" + component={({ account }: { account: string }) => ( + <ShowAccountDetails + account={account} + onUpdateSuccess={() => { + route("/account") + }} + onClear={() => { + route("/account") + }} + /> + )} + /> + + <Route + path="/profile/:account/change-password" + component={({ account }: { account: string }) => ( + <UpdateAccountPassword + focus + account={account} + onUpdateSuccess={() => { + route("/account") + }} + onCancel={() => { + route("/account") + }} + /> + )} + /> + <Route + path="/profile/:account/delete" + component={({ account }: { account: string }) => ( + <RemoveAccount + account={account} + onUpdateSuccess={() => { + route("/account") + }} + onCancel={() => { + route("/account") + }} + /> + )} + /> + + <Route + path="/profile/:account/cashouts" + component={({ account }: { account: string }) => ( + <CashoutListForAccount + account={account} + onSelected={(cid) => { + route(`/cashout/${cid}`) + }} + onClose={() => { + route("/account") + }} + /> + )} + /> + + <Route + path="/my-profile" + component={() => ( + <ShowAccountDetails + account={username} + onUpdateSuccess={() => { + route("/account") + }} + onClear={() => { + route("/account") + }} + /> + )} + /> + <Route + path="/my-password" + component={() => ( + <UpdateAccountPassword + focus + account={username} + onUpdateSuccess={() => { + route("/account") + }} + onCancel={() => { + route("/account") + }} + /> + )} + /> + + <Route + path="/my-cashouts" + component={() => ( + <CashoutListForAccount + account={username} + onSelected={(cid) => { + route(`/cashout/${cid}`) + }} + onClose={() => { + route("/account"); + }} + /> + )} + /> + + <Route + path="/new-cashout" + component={() => ( + <CreateCashout + account={username} + onComplete={(cid) => { + route(`/cashout/${cid}`); + }} + onCancel={() => { + route("/account"); + }} + /> + )} + /> + + <Route + path="/cashout/:cid" + component={({ cid }: { cid: string }) => ( + <ShowCashoutDetails + id={cid} + onCancel={() => { + route("/account"); + }} + /> + )} + /> + + + <Route + path="/wire-transfer/:dest" + component={({ dest }: { dest: string }) => ( + <WireTransfer + toAccount={dest} + onCancel={() => { + route("/account") + }} + onSuccess={() => { + route("/account") + }} + /> + )} + /> + + <Route + path="/account" + component={() => { + if (isUserAdministrator) { + return <AdminHome + onRegister={() => { + route("/register"); + }} + onCreateAccount={() => { + route("/new-account") + }} + onShowAccountDetails={(aid) => { + route(`/profile/${aid}/details`) + }} + onRemoveAccount={(aid) => { + route(`/profile/${aid}/delete`) + }} + onShowCashoutForAccount={(aid) => { + route(`/profile/${aid}/cashouts`) + }} + onUpdateAccountPassword={(aid) => { + route(`/profile/${aid}/change-password`) + + }} + />; + } else { + return <AccountPage + account={username} + goToConfirmOperation={(wopid) => { + route(`/operation/${wopid}`); + }} + /> + } + }} + /> + <Route default component={Redirect} to="/account" /> + </Router> + </BankFrame> + ); +} + +function Redirect({ to }: { to: string }): VNode { + useEffect(() => { + route(to, true); + }, []); + return <div>being redirected to {to}</div>; +} diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx @@ -1,325 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { createHashHistory } from "history"; -import { Fragment, VNode, h } from "preact"; -import { Route, Router, route } from "preact-router"; -import { useEffect } from "preact/hooks"; -import { useBackendState } from "../hooks/backend.js"; -import { BankFrame } from "../pages/BankFrame.js"; -import { WithdrawalOperationPage } from "../pages/WithdrawalOperationPage.js"; -import { LoginForm } from "../pages/LoginForm.js"; -import { PublicHistoriesPage } from "../pages/PublicHistoriesPage.js"; -import { RegistrationPage } from "../pages/RegistrationPage.js"; -import { AdminHome } from "../pages/admin/AdminHome.js"; -import { CreateCashout } from "../pages/business/CreateCashout.js"; -import { bankUiSettings } from "../settings.js"; -import { ShowAccountDetails } from "../pages/ShowAccountDetails.js"; -import { UpdateAccountPassword } from "../pages/UpdateAccountPassword.js"; -import { RemoveAccount } from "../pages/admin/RemoveAccount.js"; -import { CreateNewAccount } from "../pages/admin/CreateNewAccount.js"; -import { CashoutListForAccount } from "../pages/admin/CashoutListForAccount.js"; -import { ShowCashoutDetails } from "../pages/business/ShowCashoutDetails.js"; -import { WireTransfer } from "../pages/admin/Account.js"; -import { AccountPage } from "../pages/AccountPage/index.js"; - -export function Routing(): VNode { - const history = createHashHistory(); - const backend = useBackendState(); - const { i18n } = useTranslationContext(); - - if (backend.state.status === "loggedOut") { - return <BankFrame > - <Router history={history}> - <Route - path="/login" - component={() => ( - <Fragment> - <div class="sm:mx-auto sm:w-full sm:max-w-sm"> - <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h2> - </div> - - <LoginForm - onRegister={() => { - route("/register"); - }} - /> - </Fragment> - )} - /> - <Route - path="/public-accounts" - component={() => <PublicHistoriesPage />} - /> - <Route - path="/operation/:wopid" - component={({ wopid }: { wopid: string }) => ( - <WithdrawalOperationPage - operationId={wopid} - onContinue={() => { - route("/account"); - }} - /> - )} - /> - {bankUiSettings.allowRegistrations && - <Route - path="/register" - component={() => ( - <RegistrationPage - onComplete={() => { - route("/account"); - }} - onCancel={() => { - route("/account"); - }} - /> - )} - /> - } - <Route default component={Redirect} to="/login" /> - </Router> - </BankFrame> - } - const { isUserAdministrator, username } = backend.state - - return ( - <BankFrame account={username}> - <Router history={history}> - <Route - path="/operation/:wopid" - component={({ wopid }: { wopid: string }) => ( - <WithdrawalOperationPage - operationId={wopid} - onContinue={() => { - route("/account"); - }} - /> - )} - /> - <Route - path="/public-accounts" - component={() => <PublicHistoriesPage />} - /> - - <Route - path="/new-account" - component={() => <CreateNewAccount - onCancel={() => { - route("/account") - }} - onCreateSuccess={() => { - route("/account") - }} - />} - /> - - <Route - path="/profile/:account/details" - component={({ account }: { account: string }) => ( - <ShowAccountDetails - account={account} - onUpdateSuccess={() => { - route("/account") - }} - onClear={() => { - route("/account") - }} - /> - )} - /> - - <Route - path="/profile/:account/change-password" - component={({ account }: { account: string }) => ( - <UpdateAccountPassword - focus - account={account} - onUpdateSuccess={() => { - route("/account") - }} - onCancel={() => { - route("/account") - }} - /> - )} - /> - <Route - path="/profile/:account/delete" - component={({ account }: { account: string }) => ( - <RemoveAccount - account={account} - onUpdateSuccess={() => { - route("/account") - }} - onCancel={() => { - route("/account") - }} - /> - )} - /> - - <Route - path="/profile/:account/cashouts" - component={({ account }: { account: string }) => ( - <CashoutListForAccount - account={account} - onSelected={(cid) => { - route(`/cashout/${cid}`) - }} - onClose={() => { - route("/account") - }} - /> - )} - /> - - <Route - path="/my-profile" - component={() => ( - <ShowAccountDetails - account={username} - onUpdateSuccess={() => { - route("/account") - }} - onClear={() => { - route("/account") - }} - /> - )} - /> - <Route - path="/my-password" - component={() => ( - <UpdateAccountPassword - focus - account={username} - onUpdateSuccess={() => { - route("/account") - }} - onCancel={() => { - route("/account") - }} - /> - )} - /> - - <Route - path="/my-cashouts" - component={() => ( - <CashoutListForAccount - account={username} - onSelected={(cid) => { - route(`/cashout/${cid}`) - }} - onClose={() => { - route("/account"); - }} - /> - )} - /> - - <Route - path="/new-cashout" - component={() => ( - <CreateCashout - account={username} - onComplete={(cid) => { - route(`/cashout/${cid}`); - }} - onCancel={() => { - route("/account"); - }} - /> - )} - /> - - <Route - path="/cashout/:cid" - component={({ cid }: { cid: string }) => ( - <ShowCashoutDetails - id={cid} - onCancel={() => { - route("/account"); - }} - /> - )} - /> - - - <Route - path="/wire-transfer/:dest" - component={({ dest }: { dest: string }) => ( - <WireTransfer - toAccount={dest} - onCancel={() => { - route("/account") - }} - onSuccess={() => { - route("/account") - }} - /> - )} - /> - - <Route - path="/account" - component={() => { - if (isUserAdministrator) { - return <AdminHome - onRegister={() => { - route("/register"); - }} - onCreateAccount={() => { - route("/new-account") - }} - onShowAccountDetails={(aid) => { - route(`/profile/${aid}/details`) - }} - onRemoveAccount={(aid) => { - route(`/profile/${aid}/delete`) - }} - onShowCashoutForAccount={(aid) => { - route(`/profile/${aid}/cashouts`) - }} - onUpdateAccountPassword={(aid) => { - route(`/profile/${aid}/change-password`) - - }} - />; - } else { - return <AccountPage - account={username} - goToConfirmOperation={(wopid) => { - route(`/operation/${wopid}`); - }} - /> - } - }} - /> - <Route default component={Redirect} to="/account" /> - </Router> - </BankFrame> - ); -} - -function Redirect({ to }: { to: string }): VNode { - useEffect(() => { - route(to, true); - }, []); - return <div>being redirected to {to}</div>; -} diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -98,8 +98,8 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode </dl> </td> <td data-negative={item.negative ? "true" : "false"} - class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> - {item.amount ? (<RenderAmount value={item.amount} negative={item.negative} /> + class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 "> + {item.amount ? (<RenderAmount value={item.amount} negative={item.negative} withColor /> ) : ( <span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span> )} diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx @@ -26,7 +26,7 @@ import { BackendStateProvider } from "../context/backend.js"; import { BankCoreApiProvider } from "../context/config.js"; import { strings } from "../i18n/strings.js"; import { bankUiSettings } from "../settings.js"; -import { Routing } from "./Routing.js"; +import { Routing } from "../Routing.js"; import { BankFrame } from "../pages/BankFrame.js"; const WITH_LOCAL_STORAGE_CACHE = false; diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -41,7 +41,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb const { api } = useBankCoreApiContext(); const [notification, notify, handleError] = useLocalNotification() - /** + /** * Register form may be shown in the initialization step. * If no register handler then this is invoke * to show a session expired or unauthorized diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -470,20 +470,21 @@ export function InputAmount( ); } -export function RenderAmount({ value, negative }: { value: AmountJson, negative?: boolean }): VNode { +export function RenderAmount({ value, negative, withColor }: { value: AmountJson, negative?: boolean, withColor?: boolean }): VNode { const { config } = useBankCoreApiContext() + const neg = !!negative //convert to true or false const str = Amounts.stringifyValue(value) const sep_pos = str.indexOf(FRAC_SEPARATOR) if (sep_pos !== -1 && str.length - sep_pos - 1 > config.currency_specification.num_fractional_normal_digits) { const limit = sep_pos + config.currency_specification.num_fractional_normal_digits + 1 const normal = str.substring(0, limit) const small = str.substring(limit) - return <span data-negative={negative} class="whitespace-nowrap data-[negative=true]:bg-red-400"> + return <span data-negative={withColor ? neg : undefined} class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> {negative ? "-" : undefined} {value.currency} {normal} <sup class="-ml-1">{small}</sup> </span> } - return <span class="whitespace-nowrap"> + return <span data-negative={withColor ? neg : undefined} class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> {negative ? "-" : undefined} {value.currency} {str} </span> diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx @@ -26,7 +26,7 @@ const logger = new Logger("PublicHistoriesPage"); interface Props { } -/** +/** * Show histories of public accounts. */ export function PublicHistoriesPage({ }: Props): VNode { diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx @@ -50,7 +50,7 @@ export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/; export const PHONE_REGEX = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/; export const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/; -/** +/** * Collect and submit registration data. */ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, onCancel: () => void }): VNode { diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx @@ -1,155 +0,0 @@ -import { TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util"; -import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { ErrorLoading } from "@gnu-taler/web-util/browser"; -import { Loading } from "@gnu-taler/web-util/browser"; -import { useBankCoreApiContext } from "../context/config.js"; -import { useAccountDetails } from "../hooks/access.js"; -import { useBackendState } from "../hooks/backend.js"; -import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; -import { LoginForm } from "./LoginForm.js"; -import { ProfileNavigation } from "./ProfileNavigation.js"; -import { assertUnreachable } from "./WithdrawalOperationPage.js"; -import { AccountForm } from "./admin/AccountForm.js"; -import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; - -export function ShowAccountDetails({ - account, - onClear, - onUpdateSuccess, -}: { - onClear?: () => void; - onUpdateSuccess: () => void; - account: string; -}): VNode { - const { i18n } = useTranslationContext(); - const { state: credentials } = useBackendState(); - const creds = credentials.status !== "loggedIn" ? undefined : credentials - const { api } = useBankCoreApiContext() - const accountIsTheCurrentUser = credentials.status === "loggedIn" ? - credentials.username === account : false - - const [update, setUpdate] = useState(false); - const [submitAccount, setSubmitAccount] = useState<TalerCorebankApi.AccountData | undefined>(); - const [notification, notify, handleError] = useLocalNotification() - - const result = useAccountDetails(account); - if (!result) { - return <Loading /> - } - if (result instanceof TalerError) { - return <ErrorLoading error={result} /> - } - if (result.type === "fail") { - switch (result.case) { - case "not-found": return <LoginForm reason="not-found" /> - case "unauthorized": return <LoginForm reason="forbidden" /> - default: assertUnreachable(result) - } - } - - async function doUpdate() { - if (!update || !submitAccount || !creds) return; - await handleError(async () => { - const resp = await api.updateAccount(creds, { - cashout_address: submitAccount.cashout_payto_uri, - challenge_contact_data: undefinedIfEmpty({ - email: submitAccount.contact_data?.email, - phone: submitAccount.contact_data?.phone, - }), - is_taler_exchange: false, - name: submitAccount.name, - }); - - if (resp.type === "ok") { - notifyInfo(i18n.str`Account updated`); - onUpdateSuccess(); - } else { - switch (resp.case) { - case "unauthorized": return notify({ - type: "error", - title: i18n.str`The rights to change the account are not sufficient`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "not-found": return notify({ - type: "error", - title: i18n.str`The username was not found`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "cant-change-legal-name-or-admin": return notify({ - type: "error", - title: i18n.str`Can't change legal name`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - default: assertUnreachable(resp) - } - } - }) - - } - - return ( - <Fragment> - <LocalNotificationBanner notification={notification} /> - {accountIsTheCurrentUser ? - <ProfileNavigation current="details" /> - : - <h1 class="text-base font-semibold leading-6 text-gray-900"> - <i18n.Translate>Account "{account}"</i18n.Translate> - </h1> - - } - - <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> - <div class="px-4 sm:px-0"> - <h2 class="text-base font-semibold leading-7 text-gray-900"> - <div class="flex items-center justify-between"> - <span class="flex flex-grow flex-col"> - <span class="text-sm text-black font-semibold leading-6 " id="availability-label"> - <i18n.Translate>Change details</i18n.Translate> - </span> - </span> - <button type="button" data-enabled={!update} class="bg-indigo-600 data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full ring-2 border-gray-600 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={() => { - setUpdate(!update) - }}> - <span aria-hidden="true" data-enabled={!update} class="translate-x-5 data-[enabled=true]: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> - </h2> - </div> - - <AccountForm - focus={update} - username={account} - template={result.body} - purpose={update ? "update" : "show"} - onChange={(a) => setSubmitAccount(a)} - > - <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> - {onClear ? - <button type="button" class="text-sm font-semibold leading-6 text-gray-900" - onClick={onClear} - > - <i18n.Translate>Cancel</i18n.Translate> - </button> - : <div /> - } - <button type="submit" - class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - disabled={!update || !submitAccount} - onClick={doUpdate} - > - <i18n.Translate>Update</i18n.Translate> - </button> - </div> - </AccountForm> - </div> - </Fragment> - ); -} - diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx @@ -1,225 +0,0 @@ -import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; -import { useBankCoreApiContext } from "../context/config.js"; -import { useBackendState } from "../hooks/backend.js"; -import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; -import { doAutoFocus } from "./PaytoWireTransferForm.js"; -import { ProfileNavigation } from "./ProfileNavigation.js"; -import { assertUnreachable } from "./WithdrawalOperationPage.js"; -import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; - -export function UpdateAccountPassword({ - account: accountName, - onCancel, - onUpdateSuccess, - focus, -}: { - onCancel: () => void; - focus?: boolean, - onUpdateSuccess: () => void; - account: string; -}): VNode { - const { i18n } = useTranslationContext(); - const { state: credentials } = useBackendState(); - const token = credentials.status !== "loggedIn" ? undefined : credentials.token - const { api } = useBankCoreApiContext(); - - const [current, setCurrent] = useState<string | undefined>(); - const [password, setPassword] = useState<string | undefined>(); - const [repeat, setRepeat] = useState<string | undefined>(); - - const accountIsTheCurrentUser = credentials.status === "loggedIn" ? - credentials.username === accountName : false - - const errors = undefinedIfEmpty({ - current: !accountIsTheCurrentUser ? undefined : !current ? i18n.str`required` : undefined, - password: !password ? i18n.str`required` : undefined, - repeat: !repeat - ? i18n.str`required` - : password !== repeat - ? i18n.str`password doesn't match` - : undefined, - }); - const [notification, notify, handleError] = useLocalNotification() - - - async function doChangePassword() { - if (!!errors || !password || !token) return; - await handleError(async () => { - const resp = await api.updatePassword({ username: accountName, token }, { - old_password: current, - new_password: password, - }); - if (resp.type === "ok") { - notifyInfo(i18n.str`Password changed`); - onUpdateSuccess(); - } else { - switch (resp.case) { - case "unauthorized": return notify({ - type: "error", - title: i18n.str`Not authorized to change the password, maybe the session is invalid.` - }) - case "old-password-invalid-or-not-allowed": return notify({ - type: "error", - title: current ? - i18n.str`This user have no right on to change the password.` : - i18n.str`This user have no right on to change the password or the old password doesn't match.` - }) - case "not-found": return notify({ - type: "error", - title: i18n.str`Account not found` - }) - default: assertUnreachable(resp) - } - } - }) - } - - return ( - <Fragment> - <LocalNotificationBanner notification={notification} /> - {accountIsTheCurrentUser ? - <ProfileNavigation current="credentials" /> : - <h1 class="text-base font-semibold leading-6 text-gray-900"> - <i18n.Translate>Account "{accountName}"</i18n.Translate> - </h1> - - } - - <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> - <div class="px-4 sm:px-0"> - <h2 class="text-base font-semibold leading-7 text-gray-900"> - <i18n.Translate>Update password</i18n.Translate> - </h2> - </div> - <form - class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" - autoCapitalize="none" - autoCorrect="off" - onSubmit={e => { - e.preventDefault() - }} - > - <div class="px-4 py-6 sm:p-8"> - <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <div class="sm:col-span-5"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="password" - > - {i18n.str`New password`} - </label> - <div class="mt-2"> - <input - ref={focus ? doAutoFocus : undefined} - type="password" - class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - name="password" - id="password" - data-error={!!errors?.password && password !== undefined} - value={password ?? ""} - onChange={(e) => { - setPassword(e.currentTarget.value) - }} - autocomplete="off" - /> - <ShowInputErrorLabel - message={errors?.password} - isDirty={password !== undefined} - /> - </div> - </div> - - <div class="sm:col-span-5"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="repeat" - > - {i18n.str`Type it again`} - </label> - <div class="mt-2"> - <input - type="password" - class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - name="repeat" - id="repeat" - data-error={!!errors?.repeat && repeat !== undefined} - value={repeat ?? ""} - onChange={(e) => { - setRepeat(e.currentTarget.value) - }} - // placeholder="" - autocomplete="off" - /> - <ShowInputErrorLabel - message={errors?.repeat} - isDirty={repeat !== undefined} - /> - </div> - <p class="mt-2 text-sm text-gray-500" > - <i18n.Translate>repeat the same password</i18n.Translate> - </p> - </div> - - {accountIsTheCurrentUser ? - <div class="sm:col-span-5"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="password" - > - {i18n.str`Current password`} - </label> - <div class="mt-2"> - <input - type="password" - class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - name="current" - id="current-password" - data-error={!!errors?.current && current !== undefined} - value={current ?? ""} - onChange={(e) => { - setCurrent(e.currentTarget.value) - }} - autocomplete="off" - /> - <ShowInputErrorLabel - message={errors?.current} - isDirty={current !== undefined} - /> - </div> - <p class="mt-2 text-sm text-gray-500" > - <i18n.Translate>your current password, for security</i18n.Translate> - </p> - </div> - : undefined} - - </div> - </div> - <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> - {onCancel ? - <button type="button" class="text-sm font-semibold leading-6 text-gray-900" - onClick={onCancel} - > - <i18n.Translate>Cancel</i18n.Translate> - </button> - : <div /> - } - <button type="submit" - class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - disabled={!!errors} - onClick={(e) => { - e.preventDefault() - doChangePassword() - }} - > - <i18n.Translate>Change</i18n.Translate> - </button> - </div> - </form> - </div> - </Fragment> - - ); -} -\ No newline at end of file diff --git a/packages/demobank-ui/src/pages/WireTransfer.tsx b/packages/demobank-ui/src/pages/WireTransfer.tsx @@ -0,0 +1,53 @@ +import { Amounts, TalerError } from "@gnu-taler/taler-util"; +import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Loading } from "@gnu-taler/web-util/browser"; +import { useAccountDetails } from "../hooks/access.js"; +import { assertUnreachable } from "./WithdrawalOperationPage.js"; +import { LoginForm } from "./LoginForm.js"; +import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; +import { useBackendState } from "../hooks/backend.js"; + +export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { onSuccess?: () => void; toAccount?: string, onCancel?: () => void, onRegister?: () => void }): VNode { + const { i18n } = useTranslationContext(); + const r = useBackendState(); + const account = r.state.status !== "loggedOut" ? r.state.username : "admin"; + const result = useAccountDetails(account); + + if (!result) { + return <Loading /> + } + if (result instanceof TalerError) { + return <ErrorLoading error={result} /> + } + if (result.type === "fail") { + switch (result.case) { + case "unauthorized": return <LoginForm reason="forbidden" /> + case "not-found": return <LoginForm reason="not-found" /> + default: assertUnreachable(result) + } + } + const { body: data } = result; + + const balance = Amounts.parseOrThrow(data.balance.amount); + const balanceIsDebit = data.balance.credit_debit_indicator == "debit"; + + const debitThreshold = Amounts.parseOrThrow(data.debit_threshold); + const limit = balanceIsDebit + ? Amounts.sub(debitThreshold, balance).amount + : Amounts.add(balance, debitThreshold).amount; + if (!balance) return <Fragment />; + return ( + <PaytoWireTransferForm + title={i18n.str`Make a wire transfer`} + toAccount={toAccount} + limit={limit} + onSuccess={() => { + notifyInfo(i18n.str`Wire transfer created!`); + if (onSuccess) onSuccess() + }} + onCancel={onCancel} + /> + ); +} diff --git a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx @@ -0,0 +1,40 @@ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { Cashouts } from "../../components/Cashouts/index.js"; +import { useBackendState } from "../../hooks/backend.js"; +import { ProfileNavigation } from "../ProfileNavigation.js"; +import { CreateNewAccount } from "../admin/CreateNewAccount.js"; +import { CreateCashout } from "../business/CreateCashout.js"; + +interface Props { + account: string, + onClose: () => void, + onSelected: (cid: string) => void +} + +export function CashoutListForAccount({ account, onSelected, onClose }: Props): VNode { + const { i18n } = useTranslationContext(); + + const { state: credentials } = useBackendState(); + + const accountIsTheCurrentUser = credentials.status === "loggedIn" ? + credentials.username === account : false + + return <Fragment> + {accountIsTheCurrentUser ? + <ProfileNavigation current="cashouts" /> + : + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Cashout for account {account}</i18n.Translate> + </h1> + } + + <CreateCashout onCancel={() => { }} onComplete={() => { }} account={account} /> + + <Cashouts + account={account} + onSelected={onSelected} + /> + </Fragment> +} + diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx @@ -0,0 +1,155 @@ +import { TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util"; +import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Loading } from "@gnu-taler/web-util/browser"; +import { useBankCoreApiContext } from "../../context/config.js"; +import { useAccountDetails } from "../../hooks/access.js"; +import { useBackendState } from "../../hooks/backend.js"; +import { undefinedIfEmpty, withRuntimeErrorHandling } from "../../utils.js"; +import { LoginForm } from "../LoginForm.js"; +import { ProfileNavigation } from "../ProfileNavigation.js"; +import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { AccountForm } from "../admin/AccountForm.js"; +import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; + +export function ShowAccountDetails({ + account, + onClear, + onUpdateSuccess, +}: { + onClear?: () => void; + onUpdateSuccess: () => void; + account: string; +}): VNode { + const { i18n } = useTranslationContext(); + const { state: credentials } = useBackendState(); + const creds = credentials.status !== "loggedIn" ? undefined : credentials + const { api } = useBankCoreApiContext() + const accountIsTheCurrentUser = credentials.status === "loggedIn" ? + credentials.username === account : false + + const [update, setUpdate] = useState(false); + const [submitAccount, setSubmitAccount] = useState<TalerCorebankApi.AccountData | undefined>(); + const [notification, notify, handleError] = useLocalNotification() + + const result = useAccountDetails(account); + if (!result) { + return <Loading /> + } + if (result instanceof TalerError) { + return <ErrorLoading error={result} /> + } + if (result.type === "fail") { + switch (result.case) { + case "not-found": return <LoginForm reason="not-found" /> + case "unauthorized": return <LoginForm reason="forbidden" /> + default: assertUnreachable(result) + } + } + + async function doUpdate() { + if (!update || !submitAccount || !creds) return; + await handleError(async () => { + const resp = await api.updateAccount(creds, { + cashout_address: submitAccount.cashout_payto_uri, + challenge_contact_data: undefinedIfEmpty({ + email: submitAccount.contact_data?.email, + phone: submitAccount.contact_data?.phone, + }), + is_taler_exchange: false, + name: submitAccount.name, + }); + + if (resp.type === "ok") { + notifyInfo(i18n.str`Account updated`); + onUpdateSuccess(); + } else { + switch (resp.case) { + case "unauthorized": return notify({ + type: "error", + title: i18n.str`The rights to change the account are not sufficient`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "not-found": return notify({ + type: "error", + title: i18n.str`The username was not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "cant-change-legal-name-or-admin": return notify({ + type: "error", + title: i18n.str`Can't change legal name`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: assertUnreachable(resp) + } + } + }) + + } + + return ( + <Fragment> + <LocalNotificationBanner notification={notification} /> + {accountIsTheCurrentUser ? + <ProfileNavigation current="details" /> + : + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Account "{account}"</i18n.Translate> + </h1> + + } + + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> + <div class="px-4 sm:px-0"> + <h2 class="text-base font-semibold leading-7 text-gray-900"> + <div class="flex items-center justify-between"> + <span class="flex flex-grow flex-col"> + <span class="text-sm text-black font-semibold leading-6 " id="availability-label"> + <i18n.Translate>Change details</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={!update} class="bg-indigo-600 data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full ring-2 border-gray-600 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={() => { + setUpdate(!update) + }}> + <span aria-hidden="true" data-enabled={!update} class="translate-x-5 data-[enabled=true]: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> + </h2> + </div> + + <AccountForm + focus={update} + username={account} + template={result.body} + purpose={update ? "update" : "show"} + onChange={(a) => setSubmitAccount(a)} + > + <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> + {onClear ? + <button type="button" class="text-sm font-semibold leading-6 text-gray-900" + onClick={onClear} + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + : <div /> + } + <button type="submit" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + disabled={!update || !submitAccount} + onClick={doUpdate} + > + <i18n.Translate>Update</i18n.Translate> + </button> + </div> + </AccountForm> + </div> + </Fragment> + ); +} + diff --git a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx @@ -0,0 +1,225 @@ +import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; +import { useBankCoreApiContext } from "../../context/config.js"; +import { useBackendState } from "../../hooks/backend.js"; +import { undefinedIfEmpty, withRuntimeErrorHandling } from "../../utils.js"; +import { doAutoFocus } from "../PaytoWireTransferForm.js"; +import { ProfileNavigation } from "../ProfileNavigation.js"; +import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; + +export function UpdateAccountPassword({ + account: accountName, + onCancel, + onUpdateSuccess, + focus, +}: { + onCancel: () => void; + focus?: boolean, + onUpdateSuccess: () => void; + account: string; +}): VNode { + const { i18n } = useTranslationContext(); + const { state: credentials } = useBackendState(); + const token = credentials.status !== "loggedIn" ? undefined : credentials.token + const { api } = useBankCoreApiContext(); + + const [current, setCurrent] = useState<string | undefined>(); + const [password, setPassword] = useState<string | undefined>(); + const [repeat, setRepeat] = useState<string | undefined>(); + + const accountIsTheCurrentUser = credentials.status === "loggedIn" ? + credentials.username === accountName : false + + const errors = undefinedIfEmpty({ + current: !accountIsTheCurrentUser ? undefined : !current ? i18n.str`required` : undefined, + password: !password ? i18n.str`required` : undefined, + repeat: !repeat + ? i18n.str`required` + : password !== repeat + ? i18n.str`password doesn't match` + : undefined, + }); + const [notification, notify, handleError] = useLocalNotification() + + + async function doChangePassword() { + if (!!errors || !password || !token) return; + await handleError(async () => { + const resp = await api.updatePassword({ username: accountName, token }, { + old_password: current, + new_password: password, + }); + if (resp.type === "ok") { + notifyInfo(i18n.str`Password changed`); + onUpdateSuccess(); + } else { + switch (resp.case) { + case "unauthorized": return notify({ + type: "error", + title: i18n.str`Not authorized to change the password, maybe the session is invalid.` + }) + case "old-password-invalid-or-not-allowed": return notify({ + type: "error", + title: current ? + i18n.str`This user have no right on to change the password.` : + i18n.str`This user have no right on to change the password or the old password doesn't match.` + }) + case "not-found": return notify({ + type: "error", + title: i18n.str`Account not found` + }) + default: assertUnreachable(resp) + } + } + }) + } + + return ( + <Fragment> + <LocalNotificationBanner notification={notification} /> + {accountIsTheCurrentUser ? + <ProfileNavigation current="credentials" /> : + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Account "{accountName}"</i18n.Translate> + </h1> + + } + + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> + <div class="px-4 sm:px-0"> + <h2 class="text-base font-semibold leading-7 text-gray-900"> + <i18n.Translate>Update password</i18n.Translate> + </h2> + </div> + <form + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" + autoCapitalize="none" + autoCorrect="off" + onSubmit={e => { + e.preventDefault() + }} + > + <div class="px-4 py-6 sm:p-8"> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="password" + > + {i18n.str`New password`} + </label> + <div class="mt-2"> + <input + ref={focus ? doAutoFocus : undefined} + type="password" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + name="password" + id="password" + data-error={!!errors?.password && password !== undefined} + value={password ?? ""} + onChange={(e) => { + setPassword(e.currentTarget.value) + }} + autocomplete="off" + /> + <ShowInputErrorLabel + message={errors?.password} + isDirty={password !== undefined} + /> + </div> + </div> + + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="repeat" + > + {i18n.str`Type it again`} + </label> + <div class="mt-2"> + <input + type="password" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + name="repeat" + id="repeat" + data-error={!!errors?.repeat && repeat !== undefined} + value={repeat ?? ""} + onChange={(e) => { + setRepeat(e.currentTarget.value) + }} + // placeholder="" + autocomplete="off" + /> + <ShowInputErrorLabel + message={errors?.repeat} + isDirty={repeat !== undefined} + /> + </div> + <p class="mt-2 text-sm text-gray-500" > + <i18n.Translate>repeat the same password</i18n.Translate> + </p> + </div> + + {accountIsTheCurrentUser ? + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="password" + > + {i18n.str`Current password`} + </label> + <div class="mt-2"> + <input + type="password" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + name="current" + id="current-password" + data-error={!!errors?.current && current !== undefined} + value={current ?? ""} + onChange={(e) => { + setCurrent(e.currentTarget.value) + }} + autocomplete="off" + /> + <ShowInputErrorLabel + message={errors?.current} + isDirty={current !== undefined} + /> + </div> + <p class="mt-2 text-sm text-gray-500" > + <i18n.Translate>your current password, for security</i18n.Translate> + </p> + </div> + : undefined} + + </div> + </div> + <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> + {onCancel ? + <button type="button" class="text-sm font-semibold leading-6 text-gray-900" + onClick={onCancel} + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + : <div /> + } + <button type="submit" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + disabled={!!errors} + onClick={(e) => { + e.preventDefault() + doChangePassword() + }} + > + <i18n.Translate>Change</i18n.Translate> + </button> + </div> + </form> + </div> + </Fragment> + + ); +} +\ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx b/packages/demobank-ui/src/pages/admin/Account.tsx @@ -1,53 +0,0 @@ -import { Amounts, TalerError } from "@gnu-taler/taler-util"; -import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { ErrorLoading } from "@gnu-taler/web-util/browser"; -import { Loading } from "@gnu-taler/web-util/browser"; -import { useAccountDetails } from "../../hooks/access.js"; -import { assertUnreachable } from "../WithdrawalOperationPage.js"; -import { LoginForm } from "../LoginForm.js"; -import { PaytoWireTransferForm } from "../PaytoWireTransferForm.js"; -import { useBackendState } from "../../hooks/backend.js"; - -export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { onSuccess?: () => void; toAccount?: string, onCancel?: () => void, onRegister?: () => void }): VNode { - const { i18n } = useTranslationContext(); - const r = useBackendState(); - const account = r.state.status !== "loggedOut" ? r.state.username : "admin"; - const result = useAccountDetails(account); - - if (!result) { - return <Loading /> - } - if (result instanceof TalerError) { - return <ErrorLoading error={result} /> - } - if (result.type === "fail") { - switch (result.case) { - case "unauthorized": return <LoginForm reason="forbidden" /> - case "not-found": return <LoginForm reason="not-found" /> - default: assertUnreachable(result) - } - } - const { body: data } = result; - - const balance = Amounts.parseOrThrow(data.balance.amount); - const balanceIsDebit = data.balance.credit_debit_indicator == "debit"; - - const debitThreshold = Amounts.parseOrThrow(data.debit_threshold); - const limit = balanceIsDebit - ? Amounts.sub(debitThreshold, balance).amount - : Amounts.add(balance, debitThreshold).amount; - if (!balance) return <Fragment />; - return ( - <PaytoWireTransferForm - title={i18n.str`Make a wire transfer`} - toAccount={toAccount} - limit={limit} - onSuccess={() => { - notifyInfo(i18n.str`Wire transfer created!`); - if (onSuccess) onSuccess() - }} - onCancel={onCancel} - /> - ); -} diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -1,11 +1,9 @@ +import { PaytoString, TalerCorebankApi, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { CopyButton, ShowInputErrorLabel, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, VNode, h } from "preact"; -import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; +import { useState } from "preact/hooks"; import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; -import { useEffect, useRef, useState } from "preact/hooks"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { PaytoString, TalerCorebankApi, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; -import { CopyButton } from "@gnu-taler/web-util/browser"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; @@ -85,7 +83,7 @@ export function AccountForm({ } else { const cashout = buildPayto("iban", newForm.cashout_payto_uri!, undefined) const account: AccountFormData = { - ...newForm as any, + ...newForm as any, cashout_payto_uri: stringifyPaytoUri(cashout) } onChange(account); diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx @@ -1,12 +1,11 @@ import { AmountString, Amounts, TalerCorebankApi, TalerError } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ErrorLoading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { ErrorLoading } from "@gnu-taler/web-util/browser"; import { Transactions } from "../../components/Transactions/index.js"; import { useLastMonitorInfo } from "../../hooks/circuit.js"; import { RenderAmount } from "../PaytoWireTransferForm.js"; -import { WireTransfer } from "./Account.js"; +import { WireTransfer } from "../WireTransfer.js"; import { AccountList } from "./AccountList.js"; /** diff --git a/packages/demobank-ui/src/pages/admin/CashoutListForAccount.tsx b/packages/demobank-ui/src/pages/admin/CashoutListForAccount.tsx @@ -1,51 +0,0 @@ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { Cashouts } from "../../components/Cashouts/index.js"; -import { useBackendState } from "../../hooks/backend.js"; -import { ProfileNavigation } from "../ProfileNavigation.js"; -import { CreateNewAccount } from "./CreateNewAccount.js"; -import { CreateCashout } from "../business/CreateCashout.js"; - -interface Props { - account: string, - onClose: () => void, - onSelected: (cid: string) => void -} - -export function CashoutListForAccount({ account, onSelected, onClose }: Props): VNode { - const { i18n } = useTranslationContext(); - - const { state: credentials } = useBackendState(); - const token = credentials.status !== "loggedIn" ? undefined : credentials.token - - const accountIsTheCurrentUser = credentials.status === "loggedIn" ? - credentials.username === account : false - - return <Fragment> - {accountIsTheCurrentUser ? - <ProfileNavigation current="cashouts" /> - : - <h1 class="text-base font-semibold leading-6 text-gray-900"> - <i18n.Translate>Cashout for account {account}</i18n.Translate> - </h1> - } - - <CreateCashout onCancel={() => {}} onComplete={() => {}} account={account} /> - <Cashouts - account={account} - onSelected={onSelected} - /> - <p> - <button - class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8" - onClick={async (e) => { - e.preventDefault(); - onClose(); - }} - > - {i18n.str`Close`} - </button> - </p> - </Fragment> -} - diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -1,16 +1,13 @@ import { TalerCorebankApi, TranslatedString } from "@gnu-taler/taler-util"; -import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Attention, LocalNotificationBanner, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { mutate } from "swr"; -import { Attention } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../../context/config.js"; import { useBackendState } from "../../hooks/backend.js"; -import { withRuntimeErrorHandling } from "../../utils.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { getRandomPassword } from "../rnd.js"; import { AccountForm, AccountFormData } from "./AccountForm.js"; -import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; export function CreateNewAccount({ onCancel, diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -1,11 +1,7 @@ import { Amounts, TalerError, TranslatedString } from "@gnu-taler/taler-util"; -import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Attention, ErrorLoading, Loading, LocalNotificationBanner, ShowInputErrorLabel, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { Attention } from "@gnu-taler/web-util/browser"; -import { ErrorLoading } from "@gnu-taler/web-util/browser"; -import { Loading } from "@gnu-taler/web-util/browser"; -import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../../context/config.js"; import { useAccountDetails } from "../../hooks/access.js"; import { useBackendState } from "../../hooks/backend.js"; @@ -13,7 +9,6 @@ import { undefinedIfEmpty } from "../../utils.js"; import { LoginForm } from "../LoginForm.js"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; -import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; export function RemoveAccount({ account, diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -77,10 +77,8 @@ export function CreateCashout({ estimateByCredit: calculateFromCredit, estimateByDebit: calculateFromDebit, } = useEstimator(); - const { state } = useBackendState() - const creds = state.status !== "loggedIn" ? undefined : state const { api, config } = useBankCoreApiContext() - const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, amount:"2" }); + const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, amount: "2" }); const [notification, notify, handleError] = useLocalNotification() const info = useConversionInfo(); @@ -116,7 +114,7 @@ export function CreateCashout({ debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold) } - const {fiat_currency, regional_currency, cashout_ratio, cashout_fee} = info.body + const { fiat_currency, regional_currency, cashout_ratio, cashout_fee } = info.body const regionalZero = Amounts.zeroOfCurrency(regional_currency); const fiatZero = Amounts.zeroOfCurrency(fiat_currency); const limit = account.balanceIsDebit @@ -189,18 +187,18 @@ export function CreateCashout({ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> <section class="mt-4 rounded-sm px-4 py-6 p-8 "> - <h2 id="summary-heading" class="font-medium text-lg">Cashout</h2> + <h2 id="summary-heading" class="font-medium text-lg"><i18n.Translate>Cashout</i18n.Translate></h2> <dl class="mt-4 space-y-4"> <div class="justify-between items-center flex"> - <dt class="text-sm text-gray-600">Convertion rate</dt> + <dt class="text-sm text-gray-600"><i18n.Translate>Convertion rate</i18n.Translate></dt> <dd class="text-sm text-gray-900">{sellRate}</dd> </div> <div class="flex items-center justify-between border-t-2 afu pt-4"> <dt class="flex items-center text-sm text-gray-600"> - <span>Current balance</span> + <span><i18n.Translate>Current balance</i18n.Translate></span> </dt> <dd class="text-sm text-gray-900"> <RenderAmount value={account.balance} /> @@ -208,7 +206,7 @@ export function CreateCashout({ </div> <div class="flex items-center justify-between border-t-2 afu pt-4"> <dt class="flex items-center text-sm text-gray-600"> - <span>Cashout fee</span> + <span><i18n.Translate>Cashout fee</i18n.Translate></span> </dt> <dd class="text-sm text-gray-900"> <RenderAmount value={sellFee} /> @@ -292,17 +290,17 @@ export function CreateCashout({ <div class="sm:col-span-5"> <dl class="mt-4 space-y-4"> - <div class="justify-between items-center flex"> - <dt class="text-sm text-gray-600">Total cost</dt> + <div class="justify-between items-center flex "> + <dt class="text-sm text-gray-600"><i18n.Translate>Total cost</i18n.Translate></dt> <dd class="text-sm text-gray-900"> - <RenderAmount value={calc.debit} negative /> + <RenderAmount value={calc.debit} negative withColor /> </dd> </div> <div class="flex items-center justify-between border-t-2 afu pt-4"> <dt class="flex items-center text-sm text-gray-600"> - <span>Balance after</span> + <span><i18n.Translate>Balance left</i18n.Translate></span> {/* <a href="#" class="ml-2 shrink-0 text-gray-400 bkx"> <span class="sr-only">Learn more about how shipping is calculated</span> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" @@ -316,7 +314,7 @@ export function CreateCashout({ {Amounts.isZero(sellFee) || Amounts.isZero(calc.beforeFee) ? undefined : ( <div class="flex items-center justify-between border-t-2 afu pt-4"> <dt class="flex items-center text-sm text-gray-600"> - <span>Amount after conversion</span> + <span><i18n.Translate>Before fee</i18n.Translate></span> {/* <a href="#" class="ml-2 shrink-0 text-gray-400 bkx"> <span class="sr-only">Learn more about how shipping is calculated</span> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" @@ -329,9 +327,9 @@ export function CreateCashout({ </div> )} <div class="flex justify-between items-center border-t-2 afu pt-4"> - <dt class="text-lg text-gray-900 font-medium">Total cashout transfer</dt> + <dt class="text-lg text-gray-900 font-medium"><i18n.Translate>Total cashout transfer</i18n.Translate></dt> <dd class="text-lg text-gray-900 font-medium"> - <RenderAmount value={calc.credit} /> + <RenderAmount value={calc.credit} withColor /> </dd> </div> </dl>