diff options
Diffstat (limited to 'packages/demobank-ui')
| -rw-r--r-- | packages/demobank-ui/src/components/Routing.tsx | 13 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/AdminPage.tsx | 1042 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/HomePage.tsx | 19 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/ShowAccountDetails.tsx | 143 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/UpdateAccountPassword.tsx | 131 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx | 3 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/WithdrawalQRCode.tsx | 4 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/admin/Account.tsx | 56 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/admin/AccountForm.tsx | 219 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/admin/AccountList.tsx | 120 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx | 107 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/admin/Home.tsx | 162 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/admin/RemoveAccount.tsx | 112 | ||||
| -rw-r--r-- | packages/demobank-ui/src/pages/business/Home.tsx (renamed from packages/demobank-ui/src/pages/BusinessAccount.tsx) | 35 | 
14 files changed, 1083 insertions, 1083 deletions
| diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx index 890058a9b..ef11af76e 100644 --- a/packages/demobank-ui/src/components/Routing.tsx +++ b/packages/demobank-ui/src/components/Routing.tsx @@ -19,14 +19,14 @@ import { VNode, h } from "preact";  import { Route, Router, route } from "preact-router";  import { useEffect } from "preact/hooks";  import { BankFrame } from "../pages/BankFrame.js"; -import { BusinessAccount } from "../pages/BusinessAccount.js"; +import { BusinessAccount } from "../pages/business/Home.js";  import { HomePage, WithdrawalOperationPage } from "../pages/HomePage.js";  import { PublicHistoriesPage } from "../pages/PublicHistoriesPage.js";  import { RegistrationPage } from "../pages/RegistrationPage.js";  import { Test } from "../pages/Test.js";  import { useBackendContext } from "../context/backend.js";  import { LoginForm } from "../pages/LoginForm.js"; -import { AdminPage } from "../pages/AdminPage.js"; +import { AdminHome } from "../pages/admin/Home.js";  export function Routing(): VNode {    const history = createHashHistory(); @@ -34,6 +34,7 @@ export function Routing(): VNode {    if (backend.state.status === "loggedOut") {      return <BankFrame +      account={undefined}        goToBusinessAccount={() => {          route("/business");        }} @@ -63,7 +64,7 @@ export function Routing(): VNode {        </Router>      </BankFrame>    } -  const isAdmin = backend.state.isUserAdministrator +  const { isUserAdministrator, username } = backend.state    return (      <BankFrame @@ -108,14 +109,15 @@ export function Routing(): VNode {          <Route            path="/account"            component={() => { -            if (isAdmin) { -              return <AdminPage +            if (isUserAdministrator) { +              return <AdminHome                  onRegister={() => {                    route("/register");                  }}                />;              } else {                return <HomePage +                account={username}                  onPendingOperationFound={(wopid) => {                    route(`/operation/${wopid}`);                  }} @@ -130,6 +132,7 @@ export function Routing(): VNode {            path="/business"            component={() => (              <BusinessAccount +              account={username}                onClose={() => {                  route("/account");                }} diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx deleted file mode 100644 index 18462bdc3..000000000 --- a/packages/demobank-ui/src/pages/AdminPage.tsx +++ /dev/null @@ -1,1042 +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 { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util"; -import { -  ErrorType, -  HttpResponsePaginated, -  RequestError, -  notify, -  notifyError, -  notifyInfo, -  useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { Cashouts } from "../components/Cashouts/index.js"; -import { useBackendContext } from "../context/backend.js"; -import { useAccountDetails } from "../hooks/access.js"; -import { -  useAdminAccountAPI, -  useBusinessAccountDetails, -  useBusinessAccounts, -} from "../hooks/circuit.js"; -import { -  buildRequestErrorMessage, -  PartialButDefined, -  RecursivePartial, -  undefinedIfEmpty, -  validateIBAN, -  WithIntermediate, -} from "../utils.js"; -import { ShowCashoutDetails } from "./BusinessAccount.js"; -import { handleNotOkResult } from "./HomePage.js"; -import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; - -const charset = -  "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; -const upperIdx = charset.indexOf("A"); - -function randomPassword(): string { -  const random = Array.from({ length: 16 }).map(() => { -    return charset.charCodeAt(Math.random() * charset.length); -  }); -  // first char can't be upper -  const charIdx = charset.indexOf(String.fromCharCode(random[0])); -  random[0] = -    charIdx > upperIdx ? charset.charCodeAt(charIdx - upperIdx) : random[0]; -  return String.fromCharCode(...random); -} - -interface Props { -  onRegister: () => void; -} -/** - * Query account information and show QR code if there is pending withdrawal - */ -export function AdminPage({ onRegister }: Props): VNode { -  const [account, setAccount] = useState<string | undefined>(); -  const [showDetails, setShowDetails] = useState<string | undefined>(); -  const [showCashouts, setShowCashouts] = useState<string | undefined>(); -  const [updatePassword, setUpdatePassword] = useState<string | undefined>(); -  const [removeAccount, setRemoveAccount] = useState<string | undefined>(); -  const [showCashoutDetails, setShowCashoutDetails] = useState< -    string | undefined -  >(); - -  const [createAccount, setCreateAccount] = useState(false); - -  const result = useBusinessAccounts({ account }); -  const { i18n } = useTranslationContext(); - -  if (result.loading) return <div />; -  if (!result.ok) { -    return handleNotOkResult(i18n, onRegister)(result); -  } - -  const { customers } = result.data; - -  if (showCashoutDetails) { -    return ( -      <ShowCashoutDetails -        id={showCashoutDetails} -        onLoadNotOk={handleNotOkResult(i18n, onRegister)} -        onCancel={() => { -          setShowCashoutDetails(undefined); -        }} -      /> -    ); -  } - -  if (showCashouts) { -    return ( -      <div> -        <div> -          <h1 class="nav welcome-text"> -            <i18n.Translate>Cashout for account {showCashouts}</i18n.Translate> -          </h1> -        </div> -        <Cashouts -          account={showCashouts} -          onSelected={(id) => { -            setShowCashouts(id); -            setShowCashouts(undefined); -          }} -        /> -        <p> -          <input -            class="pure-button" -            type="submit" -            value={i18n.str`Close`} -            onClick={async (e) => { -              e.preventDefault(); -              setShowCashouts(undefined); -            }} -          /> -        </p> -      </div> -    ); -  } - -  if (showDetails) { -    return ( -      <ShowAccountDetails -        account={showDetails} -        onLoadNotOk={handleNotOkResult(i18n, onRegister)} -        onChangePassword={() => { -          setUpdatePassword(showDetails); -          setShowDetails(undefined); -        }} -        onUpdateSuccess={() => { -          notifyInfo(i18n.str`Account updated`); -          setShowDetails(undefined); -        }} -        onClear={() => { -          setShowDetails(undefined); -        }} -      /> -    ); -  } -  if (removeAccount) { -    return ( -      <RemoveAccount -        account={removeAccount} -        onLoadNotOk={handleNotOkResult(i18n, onRegister)} -        onUpdateSuccess={() => { -          notifyInfo(i18n.str`Account removed`); -          setRemoveAccount(undefined); -        }} -        onClear={() => { -          setRemoveAccount(undefined); -        }} -      /> -    ); -  } -  if (updatePassword) { -    return ( -      <UpdateAccountPassword -        account={updatePassword} -        onLoadNotOk={handleNotOkResult(i18n, onRegister)} -        onUpdateSuccess={() => { -          notifyInfo(i18n.str`Password changed`); -          setUpdatePassword(undefined); -        }} -        onClear={() => { -          setUpdatePassword(undefined); -        }} -      /> -    ); -  } -  if (createAccount) { -    return ( -      <CreateNewAccount -        onClose={() => setCreateAccount(false)} -        onCreateSuccess={(password) => { -          notifyInfo( -            i18n.str`Account created with password "${password}". The user must change the password on the next login.`, -          ); -          setCreateAccount(false); -        }} -      /> -    ); -  } - -  return ( -    <Fragment> -      <div> -        <h1 class="nav welcome-text"> -          <i18n.Translate>Admin panel</i18n.Translate> -        </h1> -      </div> - -      <p> -        <div style={{ display: "flex", justifyContent: "space-between" }}> -          <div></div> -          <div> -            <input -              class="pure-button pure-button-primary content" -              type="submit" -              value={i18n.str`Create account`} -              onClick={async (e) => { -                e.preventDefault(); - -                setCreateAccount(true); -              }} -            /> -          </div> -        </div> -      </p> - -      <AdminAccount onRegister={onRegister} /> -      <section -        id="main" -        style={{ width: 600, marginLeft: "auto", marginRight: "auto" }} -      > -        {!customers.length ? ( -          <div></div> -        ) : ( -          <article> -            <h2>{i18n.str`Accounts:`}</h2> -            <div class="results"> -              <table class="pure-table pure-table-striped"> -                <thead> -                  <tr> -                    <th>{i18n.str`Username`}</th> -                    <th>{i18n.str`Name`}</th> -                    <th>{i18n.str`Balance`}</th> -                    <th>{i18n.str`Actions`}</th> -                  </tr> -                </thead> -                <tbody> -                  {customers.map((item, idx) => { -                    const balance = !item.balance -                      ? undefined -                      : Amounts.parse(item.balance.amount); -                    const balanceIsDebit = -                      item.balance && -                      item.balance.credit_debit_indicator == "debit"; -                    return ( -                      <tr key={idx}> -                        <td> -                          <a -                            href="#" -                            onClick={(e) => { -                              e.preventDefault(); -                              setShowDetails(item.username); -                            }} -                          > -                            {item.username} -                          </a> -                        </td> -                        <td>{item.name}</td> -                        <td> -                          {!balance ? ( -                            i18n.str`unknown` -                          ) : ( -                            <span class="amount"> -                              {balanceIsDebit ? <b>-</b> : null} -                              <span class="value">{`${Amounts.stringifyValue( -                                balance, -                              )}`}</span> -                                -                              <span class="currency">{`${balance.currency}`}</span> -                            </span> -                          )} -                        </td> -                        <td> -                          <a -                            href="#" -                            onClick={(e) => { -                              e.preventDefault(); -                              setUpdatePassword(item.username); -                            }} -                          > -                            change password -                          </a> -                            -                          <a -                            href="#" -                            onClick={(e) => { -                              e.preventDefault(); -                              setShowCashouts(item.username); -                            }} -                          > -                            cashouts -                          </a> -                            -                          <a -                            href="#" -                            onClick={(e) => { -                              e.preventDefault(); -                              setRemoveAccount(item.username); -                            }} -                          > -                            remove -                          </a> -                        </td> -                      </tr> -                    ); -                  })} -                </tbody> -              </table> -            </div> -          </article> -        )} -      </section> -    </Fragment> -  ); -} - -function AdminAccount({ onRegister }: { onRegister: () => void }): VNode { -  const { i18n } = useTranslationContext(); -  const r = useBackendContext(); -  const account = r.state.status === "loggedIn" ? r.state.username : "admin"; -  const result = useAccountDetails(account); - -  if (!result.ok) { -    return handleNotOkResult(i18n, onRegister)(result); -  } -  const { data } = result; -  const balance = Amounts.parseOrThrow(data.balance.amount); -  const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold); -  const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit"; -  const limit = balanceIsDebit -    ? Amounts.sub(debitThreshold, balance).amount -    : Amounts.add(balance, debitThreshold).amount; -  if (!balance) return <Fragment />; -  return ( -    <Fragment> -      <section id="assets"> -        <div class="asset-summary"> -          <h2>{i18n.str`Bank account balance`}</h2> -          {!balance ? ( -            <div class="large-amount" style={{ color: "gray" }}> -              Waiting server response... -            </div> -          ) : ( -            <div class="large-amount amount"> -              {balanceIsDebit ? <b>-</b> : null} -              <span class="value">{`${Amounts.stringifyValue(balance)}`}</span> -                -              <span class="currency">{`${balance.currency}`}</span> -            </div> -          )} -        </div> -      </section> -      <PaytoWireTransferForm -        focus -        limit={limit} -        onSuccess={() => { -          notifyInfo(i18n.str`Wire transfer created!`); -        }} -        onCancel={undefined} -      /> -    </Fragment> -  ); -} - -const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; -const EMAIL_REGEX = -  /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/; - -function initializeFromTemplate( -  account: SandboxBackend.Circuit.CircuitAccountData | undefined, -): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> { -  const emptyAccount = { -    cashout_address: undefined, -    iban: undefined, -    name: undefined, -    username: undefined, -    contact_data: undefined, -  }; -  const emptyContact = { -    email: undefined, -    phone: undefined, -  }; - -  const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> = -    structuredClone(account) ?? emptyAccount; -  if (typeof initial.contact_data === "undefined") { -    initial.contact_data = emptyContact; -  } -  initial.contact_data.email; -  return initial as any; -} - -export function UpdateAccountPassword({ -  account, -  onClear, -  onUpdateSuccess, -  onLoadNotOk, -}: { -  onLoadNotOk: <T>( -    error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, -  ) => VNode; -  onClear: () => void; -  onUpdateSuccess: () => void; -  account: string; -}): VNode { -  const { i18n } = useTranslationContext(); -  const result = useBusinessAccountDetails(account); -  const { changePassword } = useAdminAccountAPI(); -  const [password, setPassword] = useState<string | undefined>(); -  const [repeat, setRepeat] = useState<string | undefined>(); - -  if (!result.ok) { -    if (result.loading || result.type === ErrorType.TIMEOUT) { -      return onLoadNotOk(result); -    } -    if (result.status === HttpStatusCode.NotFound) { -      return <div>account not found</div>; -    } -    return onLoadNotOk(result); -  } - -  const errors = undefinedIfEmpty({ -    password: !password ? i18n.str`required` : undefined, -    repeat: !repeat -      ? i18n.str`required` -      : password !== repeat -        ? i18n.str`password doesn't match` -        : undefined, -  }); - -  return ( -    <div> -      <div> -        <h1 class="nav welcome-text"> -          <i18n.Translate>Update password for {account}</i18n.Translate> -        </h1> -      </div> - -      <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}> -        <form class="pure-form"> -          <fieldset> -            <label>{i18n.str`Password`}</label> -            <input -              type="password" -              value={password ?? ""} -              onChange={(e) => { -                setPassword(e.currentTarget.value); -              }} -            /> -            <ShowInputErrorLabel -              message={errors?.password} -              isDirty={password !== undefined} -            /> -          </fieldset> -          <fieldset> -            <label>{i18n.str`Repeat password`}</label> -            <input -              type="password" -              value={repeat ?? ""} -              onChange={(e) => { -                setRepeat(e.currentTarget.value); -              }} -            /> -            <ShowInputErrorLabel -              message={errors?.repeat} -              isDirty={repeat !== undefined} -            /> -          </fieldset> -        </form> -        <p> -          <div style={{ display: "flex", justifyContent: "space-between" }}> -            <div> -              <input -                class="pure-button" -                type="submit" -                value={i18n.str`Close`} -                onClick={async (e) => { -                  e.preventDefault(); -                  onClear(); -                }} -              /> -            </div> -            <div> -              <input -                id="select-exchange" -                class="pure-button pure-button-primary content" -                disabled={!!errors} -                type="submit" -                value={i18n.str`Confirm`} -                onClick={async (e) => { -                  e.preventDefault(); -                  if (!!errors || !password) return; -                  try { -                    const r = await changePassword(account, { -                      new_password: password, -                    }); -                    onUpdateSuccess(); -                  } catch (error) { -                    if (error instanceof RequestError) { -                      notify(buildRequestErrorMessage(i18n, error.cause)); -                    } else { -                      notifyError(i18n.str`Operation failed, please report`, (error instanceof Error -                        ? error.message -                        : JSON.stringify(error)) as TranslatedString) -                    } -                  } -                }} -              /> -            </div> -          </div> -        </p> -      </div> -    </div> -  ); -} - -function CreateNewAccount({ -  onClose, -  onCreateSuccess, -}: { -  onClose: () => void; -  onCreateSuccess: (password: string) => void; -}): VNode { -  const { i18n } = useTranslationContext(); -  const { createAccount } = useAdminAccountAPI(); -  const [submitAccount, setSubmitAccount] = useState< -    SandboxBackend.Circuit.CircuitAccountData | undefined -  >(); -  return ( -    <div> -      <div> -        <h1 class="nav welcome-text"> -          <i18n.Translate>New account</i18n.Translate> -        </h1> -      </div> - -      <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}> -        <AccountForm -          template={undefined} -          purpose="create" -          onChange={(a) => { -            setSubmitAccount(a); -          }} -        /> - -        <p> -          <div style={{ display: "flex", justifyContent: "space-between" }}> -            <div> -              <input -                class="pure-button" -                type="submit" -                value={i18n.str`Close`} -                onClick={async (e) => { -                  e.preventDefault(); -                  onClose(); -                }} -              /> -            </div> -            <div> -              <input -                id="select-exchange" -                class="pure-button pure-button-primary content" -                disabled={!submitAccount} -                type="submit" -                value={i18n.str`Confirm`} -                onClick={async (e) => { -                  e.preventDefault(); - -                  if (!submitAccount) return; -                  try { -                    const account: SandboxBackend.Circuit.CircuitAccountRequest = -                    { -                      cashout_address: submitAccount.cashout_address, -                      contact_data: submitAccount.contact_data, -                      internal_iban: submitAccount.iban, -                      name: submitAccount.name, -                      username: submitAccount.username, -                      password: randomPassword(), -                    }; - -                    await createAccount(account); -                    onCreateSuccess(account.password); -                  } catch (error) { -                    if (error instanceof RequestError) { -                      notify( -                        buildRequestErrorMessage(i18n, error.cause, { -                          onClientError: (status) => -                            status === HttpStatusCode.Forbidden -                              ? i18n.str`The rights to perform the operation are not sufficient` -                              : status === HttpStatusCode.BadRequest -                                ? i18n.str`Input data was invalid` -                                : status === HttpStatusCode.Conflict -                                  ? i18n.str`At least one registration detail was not available` -                                  : undefined, -                        }), -                      ); -                    } else { -                      notifyError( -                        i18n.str`Operation failed, please report`, -                        (error instanceof Error -                          ? error.message -                          : JSON.stringify(error)) as TranslatedString -                      ) -                    } -                  } -                }} -              /> -            </div> -          </div> -        </p> -      </div> -    </div> -  ); -} - -export function ShowAccountDetails({ -  account, -  onClear, -  onUpdateSuccess, -  onLoadNotOk, -  onChangePassword, -}: { -  onLoadNotOk: <T>( -    error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, -  ) => VNode; -  onClear?: () => void; -  onChangePassword: () => void; -  onUpdateSuccess: () => void; -  account: string; -}): VNode { -  const { i18n } = useTranslationContext(); -  const result = useBusinessAccountDetails(account); -  const { updateAccount } = useAdminAccountAPI(); -  const [update, setUpdate] = useState(false); -  const [submitAccount, setSubmitAccount] = useState< -    SandboxBackend.Circuit.CircuitAccountData | undefined -  >(); - -  if (!result.ok) { -    if (result.loading || result.type === ErrorType.TIMEOUT) { -      return onLoadNotOk(result); -    } -    if (result.status === HttpStatusCode.NotFound) { -      return <div>account not found</div>; -    } -    return onLoadNotOk(result); -  } - -  return ( -    <div> -      <div> -        <h1 class="nav welcome-text"> -          <i18n.Translate>Business account details</i18n.Translate> -        </h1> -      </div> -      <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}> -        <AccountForm -          template={result.data} -          purpose={update ? "update" : "show"} -          onChange={(a) => setSubmitAccount(a)} -        /> - -        <p class="buttons-account"> -          <div -            style={{ -              display: "flex", -              justifyContent: "space-between", -              flexFlow: "wrap-reverse", -            }} -          > -            <div> -              {onClear ? ( -                <input -                  class="pure-button" -                  type="submit" -                  value={i18n.str`Close`} -                  onClick={async (e) => { -                    e.preventDefault(); -                    onClear(); -                  }} -                /> -              ) : undefined} -            </div> -            <div style={{ display: "flex" }}> -              <div> -                <input -                  id="select-exchange" -                  class="pure-button pure-button-primary content" -                  disabled={update && !submitAccount} -                  type="submit" -                  value={i18n.str`Change password`} -                  onClick={async (e) => { -                    e.preventDefault(); -                    onChangePassword(); -                  }} -                /> -              </div> -              <div> -                <input -                  id="select-exchange" -                  class="pure-button pure-button-primary content" -                  disabled={update && !submitAccount} -                  type="submit" -                  value={update ? i18n.str`Confirm` : i18n.str`Update`} -                  onClick={async (e) => { -                    e.preventDefault(); - -                    if (!update) { -                      setUpdate(true); -                    } else { -                      if (!submitAccount) return; -                      try { -                        await updateAccount(account, { -                          cashout_address: submitAccount.cashout_address, -                          contact_data: submitAccount.contact_data, -                        }); -                        onUpdateSuccess(); -                      } catch (error) { -                        if (error instanceof RequestError) { -                          notify( -                            buildRequestErrorMessage(i18n, error.cause, { -                              onClientError: (status) => -                                status === HttpStatusCode.Forbidden -                                  ? i18n.str`The rights to change the account are not sufficient` -                                  : status === HttpStatusCode.NotFound -                                    ? i18n.str`The username was not found` -                                    : undefined, -                            }), -                          ); -                        } else { -                          notifyError( -                            i18n.str`Operation failed, please report`, -                            (error instanceof Error -                              ? error.message -                              : JSON.stringify(error)) as TranslatedString -                          ) -                        } -                      } -                    } -                  }} -                /> -              </div> -            </div> -          </div> -        </p> -      </div> -    </div> -  ); -} - -function RemoveAccount({ -  account, -  onClear, -  onUpdateSuccess, -  onLoadNotOk, -}: { -  onLoadNotOk: <T>( -    error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, -  ) => VNode; -  onClear: () => void; -  onUpdateSuccess: () => void; -  account: string; -}): VNode { -  const { i18n } = useTranslationContext(); -  const result = useAccountDetails(account); -  const { deleteAccount } = useAdminAccountAPI(); - -  if (!result.ok) { -    if (result.loading || result.type === ErrorType.TIMEOUT) { -      return onLoadNotOk(result); -    } -    if (result.status === HttpStatusCode.NotFound) { -      return <div>account not found</div>; -    } -    return onLoadNotOk(result); -  } - -  const balance = Amounts.parse(result.data.balance.amount); -  if (!balance) { -    return <div>there was an error reading the balance</div>; -  } -  const isBalanceEmpty = Amounts.isZero(balance); -  return ( -    <div> -      <div> -        <h1 class="nav welcome-text"> -          <i18n.Translate>Remove account: {account}</i18n.Translate> -        </h1> -      </div> -      {/* {FXME: SHOW WARNING} */} -      {/* {!isBalanceEmpty && ( -        <ErrorBannerFloat -          error={{ -            title: i18n.str`Can't delete the account`, -            description: i18n.str`Balance is not empty`, -          }} -          onClear={() => saveError(undefined)} -        /> -      )} */} - -      <p> -        <div style={{ display: "flex", justifyContent: "space-between" }}> -          <div> -            <input -              class="pure-button" -              type="submit" -              value={i18n.str`Cancel`} -              onClick={async (e) => { -                e.preventDefault(); -                onClear(); -              }} -            /> -          </div> -          <div> -            <input -              id="select-exchange" -              class="pure-button pure-button-primary content" -              disabled={!isBalanceEmpty} -              type="submit" -              value={i18n.str`Confirm`} -              onClick={async (e) => { -                e.preventDefault(); -                try { -                  const r = await deleteAccount(account); -                  onUpdateSuccess(); -                } catch (error) { -                  if (error instanceof RequestError) { -                    notify( -                      buildRequestErrorMessage(i18n, error.cause, { -                        onClientError: (status) => -                          status === HttpStatusCode.Forbidden -                            ? i18n.str`The administrator specified a institutional username` -                            : status === HttpStatusCode.NotFound -                              ? i18n.str`The username was not found` -                              : status === HttpStatusCode.PreconditionFailed -                                ? i18n.str`Balance was not zero` -                                : undefined, -                      }), -                    ); -                  } else { -                    notifyError(i18n.str`Operation failed, please report`, -                      (error instanceof Error -                          ? error.message -                          : JSON.stringify(error)) as TranslatedString); -                  } -                } -              }} -            /> -          </div> -        </div> -      </p> -    </div> -  ); -} -/** - * Create valid account object to update or create - * Take template as initial values for the form - * Purpose indicate if all field al read only (show), part of them (update) - * or none (create) - * @param param0 - * @returns - */ -function AccountForm({ -  template, -  purpose, -  onChange, -}: { -  template: SandboxBackend.Circuit.CircuitAccountData | undefined; -  onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void; -  purpose: "create" | "update" | "show"; -}): VNode { -  const initial = initializeFromTemplate(template); -  const [form, setForm] = useState(initial); -  const [errors, setErrors] = useState< -    RecursivePartial<typeof initial> | undefined -  >(undefined); -  const { i18n } = useTranslationContext(); - -  function updateForm(newForm: typeof initial): void { -    const parsed = !newForm.cashout_address -      ? undefined -      : parsePaytoUri(newForm.cashout_address); - -    const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({ -      cashout_address: !newForm.cashout_address -        ? i18n.str`required` -        : !parsed -          ? i18n.str`does not follow the pattern` -          : !parsed.isKnown || parsed.targetType !== "iban" -            ? i18n.str`only "IBAN" target are supported` -            : !IBAN_REGEX.test(parsed.iban) -              ? i18n.str`IBAN should have just uppercased letters and numbers` -              : validateIBAN(parsed.iban, i18n), -      contact_data: undefinedIfEmpty({ -        email: !newForm.contact_data?.email -          ? i18n.str`required` -          : !EMAIL_REGEX.test(newForm.contact_data.email) -            ? i18n.str`it should be an email` -            : undefined, -        phone: !newForm.contact_data?.phone -          ? i18n.str`required` -          : !newForm.contact_data.phone.startsWith("+") -            ? i18n.str`should start with +` -            : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone) -              ? i18n.str`phone number can't have other than numbers` -              : undefined, -      }), -      iban: !newForm.iban -        ? undefined //optional field -        : !IBAN_REGEX.test(newForm.iban) -          ? i18n.str`IBAN should have just uppercased letters and numbers` -          : validateIBAN(newForm.iban, i18n), -      name: !newForm.name ? i18n.str`required` : undefined, -      username: !newForm.username ? i18n.str`required` : undefined, -    }); -    setErrors(errors); -    setForm(newForm); -    onChange(errors === undefined ? (newForm as any) : undefined); -  } - -  return ( -    <form class="pure-form"> -      <fieldset> -        <label for="username"> -          {i18n.str`Username`} -          {purpose === "create" && <b style={{ color: "red" }}>*</b>} -        </label> -        <input -          name="username" -          type="text" -          disabled={purpose !== "create"} -          value={form.username} -          onChange={(e) => { -            form.username = e.currentTarget.value; -            updateForm(structuredClone(form)); -          }} -        />{" "} -        <ShowInputErrorLabel -          message={errors?.username} -          isDirty={form.username !== undefined} -        /> -      </fieldset> -      <fieldset> -        <label> -          {i18n.str`Name`} -          {purpose === "create" && <b style={{ color: "red" }}>*</b>} -        </label> -        <input -          disabled={purpose !== "create"} -          value={form.name ?? ""} -          onChange={(e) => { -            form.name = e.currentTarget.value; -            updateForm(structuredClone(form)); -          }} -        /> -        <ShowInputErrorLabel -          message={errors?.name} -          isDirty={form.name !== undefined} -        /> -      </fieldset> -      {purpose !== "create" && ( -        <fieldset> -          <label>{i18n.str`Internal IBAN`}</label> -          <input -            disabled={true} -            value={form.iban ?? ""} -            onChange={(e) => { -              form.iban = e.currentTarget.value; -              updateForm(structuredClone(form)); -            }} -          /> -          <ShowInputErrorLabel -            message={errors?.iban} -            isDirty={form.iban !== undefined} -          /> -        </fieldset> -      )} -      <fieldset> -        <label> -          {i18n.str`Email`} -          {purpose !== "show" && <b style={{ color: "red" }}>*</b>} -        </label> -        <input -          disabled={purpose === "show"} -          value={form.contact_data.email ?? ""} -          onChange={(e) => { -            form.contact_data.email = e.currentTarget.value; -            updateForm(structuredClone(form)); -          }} -        /> -        <ShowInputErrorLabel -          message={errors?.contact_data?.email} -          isDirty={form.contact_data.email !== undefined} -        /> -      </fieldset> -      <fieldset> -        <label> -          {i18n.str`Phone`} -          {purpose !== "show" && <b style={{ color: "red" }}>*</b>} -        </label> -        <input -          disabled={purpose === "show"} -          value={form.contact_data.phone ?? ""} -          onChange={(e) => { -            form.contact_data.phone = e.currentTarget.value; -            updateForm(structuredClone(form)); -          }} -        /> -        <ShowInputErrorLabel -          message={errors?.contact_data?.phone} -          isDirty={form.contact_data?.phone !== undefined} -        /> -      </fieldset> -      <fieldset> -        <label> -          {i18n.str`Cashout address`} -          {purpose !== "show" && <b style={{ color: "red" }}>*</b>} -        </label> -        <input -          disabled={purpose === "show"} -          value={(form.cashout_address ?? "").substring("payto://iban/".length)} -          onChange={(e) => { -            form.cashout_address = "payto://iban/" + e.currentTarget.value; -            updateForm(structuredClone(form)); -          }} -        /> -        <ShowInputErrorLabel -          message={errors?.cashout_address} -          isDirty={form.cashout_address !== undefined} -        /> -      </fieldset> -    </form> -  ); -} diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index e82e46eb2..a911f347c 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -35,7 +35,7 @@ import { useBackendContext } from "../context/backend.js";  import { getInitialBackendBaseURL } from "../hooks/backend.js";  import { useSettings } from "../hooks/settings.js";  import { AccountPage } from "./AccountPage/index.js"; -import { AdminPage } from "./AdminPage.js"; +import { AdminHome } from "./admin/Home.js";  import { LoginForm } from "./LoginForm.js";  import { WithdrawalQRCode } from "./WithdrawalQRCode.js";  import { error } from "console"; @@ -54,31 +54,24 @@ const logger = new Logger("AccountPage");   */  export function HomePage({    onRegister, +  account,    onPendingOperationFound,  }: { +  account: string,    onPendingOperationFound: (id: string) => void;    onRegister: () => void;  }): VNode { -  const backend = useBackendContext();    const [settings] = useSettings();    const { i18n } = useTranslationContext(); -  if (backend.state.status === "loggedOut") { -    return <LoginForm onRegister={onRegister} />; -  } -    if (settings.currentWithdrawalOperationId) {      onPendingOperationFound(settings.currentWithdrawalOperationId);      return <Loading />;    } -  if (backend.state.isUserAdministrator) { -    return <AdminPage onRegister={onRegister} />; -  } -    return (      <AccountPage -      account={backend.state.username} +      account={account}        onLoadNotOk={handleNotOkResult(i18n, onRegister)}      />    ); @@ -105,8 +98,8 @@ export function WithdrawalOperationPage({    if (!parsedUri) {      notifyError( -      i18n.str`The Withdrawal URI is not valid: "${uri}"`, -      undefined +      i18n.str`The Withdrawal URI is not valid`, +      uri as TranslatedString      );      return <Loading />;    } diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx new file mode 100644 index 000000000..91b50b84c --- /dev/null +++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx @@ -0,0 +1,143 @@ +import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode,h } from "preact"; +import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js"; +import { useState } from "preact/hooks"; +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { buildRequestErrorMessage } from "../utils.js"; +import { AccountForm } from "./admin/AccountForm.js"; + +export function ShowAccountDetails({ +    account, +    onClear, +    onUpdateSuccess, +    onLoadNotOk, +    onChangePassword, +  }: { +    onLoadNotOk: <T>( +      error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, +    ) => VNode; +    onClear?: () => void; +    onChangePassword: () => void; +    onUpdateSuccess: () => void; +    account: string; +  }): VNode { +    const { i18n } = useTranslationContext(); +    const result = useBusinessAccountDetails(account); +    const { updateAccount } = useAdminAccountAPI(); +    const [update, setUpdate] = useState(false); +    const [submitAccount, setSubmitAccount] = useState< +      SandboxBackend.Circuit.CircuitAccountData | undefined +    >(); +   +    if (!result.ok) { +      if (result.loading || result.type === ErrorType.TIMEOUT) { +        return onLoadNotOk(result); +      } +      if (result.status === HttpStatusCode.NotFound) { +        return <div>account not found</div>; +      } +      return onLoadNotOk(result); +    } +   +    return ( +      <div> +        <div> +          <h1 class="nav welcome-text"> +            <i18n.Translate>Business account details</i18n.Translate> +          </h1> +        </div> +        <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}> +          <AccountForm +            template={result.data} +            purpose={update ? "update" : "show"} +            onChange={(a) => setSubmitAccount(a)} +          /> +   +          <p class="buttons-account"> +            <div +              style={{ +                display: "flex", +                justifyContent: "space-between", +                flexFlow: "wrap-reverse", +              }} +            > +              <div> +                {onClear ? ( +                  <input +                    class="pure-button" +                    type="submit" +                    value={i18n.str`Close`} +                    onClick={async (e) => { +                      e.preventDefault(); +                      onClear(); +                    }} +                  /> +                ) : undefined} +              </div> +              <div style={{ display: "flex" }}> +                <div> +                  <input +                    id="select-exchange" +                    class="pure-button pure-button-primary content" +                    disabled={update && !submitAccount} +                    type="submit" +                    value={i18n.str`Change password`} +                    onClick={async (e) => { +                      e.preventDefault(); +                      onChangePassword(); +                    }} +                  /> +                </div> +                <div> +                  <input +                    id="select-exchange" +                    class="pure-button pure-button-primary content" +                    disabled={update && !submitAccount} +                    type="submit" +                    value={update ? i18n.str`Confirm` : i18n.str`Update`} +                    onClick={async (e) => { +                      e.preventDefault(); +   +                      if (!update) { +                        setUpdate(true); +                      } else { +                        if (!submitAccount) return; +                        try { +                          await updateAccount(account, { +                            cashout_address: submitAccount.cashout_address, +                            contact_data: submitAccount.contact_data, +                          }); +                          onUpdateSuccess(); +                        } catch (error) { +                          if (error instanceof RequestError) { +                            notify( +                              buildRequestErrorMessage(i18n, error.cause, { +                                onClientError: (status) => +                                  status === HttpStatusCode.Forbidden +                                    ? i18n.str`The rights to change the account are not sufficient` +                                    : status === HttpStatusCode.NotFound +                                      ? i18n.str`The username was not found` +                                      : undefined, +                              }), +                            ); +                          } else { +                            notifyError( +                              i18n.str`Operation failed, please report`, +                              (error instanceof Error +                                ? error.message +                                : JSON.stringify(error)) as TranslatedString +                            ) +                          } +                        } +                      } +                    }} +                  /> +                </div> +              </div> +            </div> +          </p> +        </div> +      </div> +    ); +  } +  
\ No newline at end of file diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx new file mode 100644 index 000000000..084a5b643 --- /dev/null +++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx @@ -0,0 +1,131 @@ +import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js"; +import { useState } from "preact/hooks"; +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { VNode,h ,Fragment} from "preact"; +import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; +import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; + +export function UpdateAccountPassword({ +    account, +    onClear, +    onUpdateSuccess, +    onLoadNotOk, +  }: { +    onLoadNotOk: <T>( +      error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, +    ) => VNode; +    onClear: () => void; +    onUpdateSuccess: () => void; +    account: string; +  }): VNode { +    const { i18n } = useTranslationContext(); +    const result = useBusinessAccountDetails(account); +    const { changePassword } = useAdminAccountAPI(); +    const [password, setPassword] = useState<string | undefined>(); +    const [repeat, setRepeat] = useState<string | undefined>(); +   +    if (!result.ok) { +      if (result.loading || result.type === ErrorType.TIMEOUT) { +        return onLoadNotOk(result); +      } +      if (result.status === HttpStatusCode.NotFound) { +        return <div>account not found</div>; +      } +      return onLoadNotOk(result); +    } +   +    const errors = undefinedIfEmpty({ +      password: !password ? i18n.str`required` : undefined, +      repeat: !repeat +        ? i18n.str`required` +        : password !== repeat +          ? i18n.str`password doesn't match` +          : undefined, +    }); +   +    return ( +      <div> +        <div> +          <h1 class="nav welcome-text"> +            <i18n.Translate>Update password for {account}</i18n.Translate> +          </h1> +        </div> +   +        <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}> +          <form class="pure-form"> +            <fieldset> +              <label>{i18n.str`Password`}</label> +              <input +                type="password" +                value={password ?? ""} +                onChange={(e) => { +                  setPassword(e.currentTarget.value); +                }} +              /> +              <ShowInputErrorLabel +                message={errors?.password} +                isDirty={password !== undefined} +              /> +            </fieldset> +            <fieldset> +              <label>{i18n.str`Repeat password`}</label> +              <input +                type="password" +                value={repeat ?? ""} +                onChange={(e) => { +                  setRepeat(e.currentTarget.value); +                }} +              /> +              <ShowInputErrorLabel +                message={errors?.repeat} +                isDirty={repeat !== undefined} +              /> +            </fieldset> +          </form> +          <p> +            <div style={{ display: "flex", justifyContent: "space-between" }}> +              <div> +                <input +                  class="pure-button" +                  type="submit" +                  value={i18n.str`Close`} +                  onClick={async (e) => { +                    e.preventDefault(); +                    onClear(); +                  }} +                /> +              </div> +              <div> +                <input +                  id="select-exchange" +                  class="pure-button pure-button-primary content" +                  disabled={!!errors} +                  type="submit" +                  value={i18n.str`Confirm`} +                  onClick={async (e) => { +                    e.preventDefault(); +                    if (!!errors || !password) return; +                    try { +                      const r = await changePassword(account, { +                        new_password: password, +                      }); +                      onUpdateSuccess(); +                    } catch (error) { +                      if (error instanceof RequestError) { +                        notify(buildRequestErrorMessage(i18n, error.cause)); +                      } else { +                        notifyError(i18n.str`Operation failed, please report`, (error instanceof Error +                          ? error.message +                          : JSON.stringify(error)) as TranslatedString) +                      } +                    } +                  }} +                /> +              </div> +            </div> +          </p> +        </div> +      </div> +    ); +  }
\ No newline at end of file diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index ced152feb..30fcbdff7 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -317,7 +317,8 @@ export function WithdrawalConfirmationQuestion({                    </div>                    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">                      <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt> -                    <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{Amounts.stringifyValue(details.amount)}</dd> +                    <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">To be added</dd> +                    {/* Amounts.stringifyValue(details.amount) */}                    </div>                  </dl>                </div> diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index b48e3b1dc..2a3a1ec2c 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -100,10 +100,6 @@ export function WithdrawalQRCode({    }    if (data.confirmation_done) { -    if (!settings.showWithdrawalSuccess) { -      clearCurrentWithdrawal() -      onContinue() -    }      return <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">        <div>          <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100"> diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx b/packages/demobank-ui/src/pages/admin/Account.tsx new file mode 100644 index 000000000..8ab3e1323 --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/Account.tsx @@ -0,0 +1,56 @@ +import { Amounts } from "@gnu-taler/taler-util"; +import { PaytoWireTransferForm } from "../PaytoWireTransferForm.js"; +import { handleNotOkResult } from "../HomePage.js"; +import { useAccountDetails } from "../../hooks/access.js"; +import { useBackendContext } from "../../context/backend.js"; +import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; + +export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode { +    const { i18n } = useTranslationContext(); +    const r = useBackendContext(); +    const account = r.state.status === "loggedIn" ? r.state.username : "admin"; +    const result = useAccountDetails(account); +   +    if (!result.ok) { +      return handleNotOkResult(i18n, onRegister)(result); +    } +    const { data } = result; +    const balance = Amounts.parseOrThrow(data.balance.amount); +    const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold); +    const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit"; +    const limit = balanceIsDebit +      ? Amounts.sub(debitThreshold, balance).amount +      : Amounts.add(balance, debitThreshold).amount; +    if (!balance) return <Fragment />; +    return ( +      <Fragment> +        <section id="assets"> +          <div class="asset-summary"> +            <h2>{i18n.str`Bank account balance`}</h2> +            {!balance ? ( +              <div class="large-amount" style={{ color: "gray" }}> +                Waiting server response... +              </div> +            ) : ( +              <div class="large-amount amount"> +                {balanceIsDebit ? <b>-</b> : null} +                <span class="value">{`${Amounts.stringifyValue(balance)}`}</span> +                  +                <span class="currency">{`${balance.currency}`}</span> +              </div> +            )} +          </div> +        </section> +        <PaytoWireTransferForm +          focus +          limit={limit} +          onSuccess={() => { +            notifyInfo(i18n.str`Wire transfer created!`); +          }} +          onCancel={undefined} +        /> +      </Fragment> +    ); +  } +  
\ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx new file mode 100644 index 000000000..9ca0323a1 --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -0,0 +1,219 @@ +import { VNode,h  } from "preact"; +import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; +import { useState } from "preact/hooks"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { parsePaytoUri } from "@gnu-taler/taler-util"; + +const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; +const EMAIL_REGEX = +  /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; +const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/; + +/** + * Create valid account object to update or create + * Take template as initial values for the form + * Purpose indicate if all field al read only (show), part of them (update) + * or none (create) + * @param param0 + * @returns + */ +export function AccountForm({ +    template, +    purpose, +    onChange, +  }: { +    template: SandboxBackend.Circuit.CircuitAccountData | undefined; +    onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void; +    purpose: "create" | "update" | "show"; +  }): VNode { +    const initial = initializeFromTemplate(template); +    const [form, setForm] = useState(initial); +    const [errors, setErrors] = useState< +      RecursivePartial<typeof initial> | undefined +    >(undefined); +    const { i18n } = useTranslationContext(); +   +    function updateForm(newForm: typeof initial): void { +      const parsed = !newForm.cashout_address +        ? undefined +        : parsePaytoUri(newForm.cashout_address); +   +      const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({ +        cashout_address: !newForm.cashout_address +          ? i18n.str`required` +          : !parsed +            ? i18n.str`does not follow the pattern` +            : !parsed.isKnown || parsed.targetType !== "iban" +              ? i18n.str`only "IBAN" target are supported` +              : !IBAN_REGEX.test(parsed.iban) +                ? i18n.str`IBAN should have just uppercased letters and numbers` +                : validateIBAN(parsed.iban, i18n), +        contact_data: undefinedIfEmpty({ +          email: !newForm.contact_data?.email +            ? i18n.str`required` +            : !EMAIL_REGEX.test(newForm.contact_data.email) +              ? i18n.str`it should be an email` +              : undefined, +          phone: !newForm.contact_data?.phone +            ? i18n.str`required` +            : !newForm.contact_data.phone.startsWith("+") +              ? i18n.str`should start with +` +              : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone) +                ? i18n.str`phone number can't have other than numbers` +                : undefined, +        }), +        iban: !newForm.iban +          ? undefined //optional field +          : !IBAN_REGEX.test(newForm.iban) +            ? i18n.str`IBAN should have just uppercased letters and numbers` +            : validateIBAN(newForm.iban, i18n), +        name: !newForm.name ? i18n.str`required` : undefined, +        username: !newForm.username ? i18n.str`required` : undefined, +      }); +      setErrors(errors); +      setForm(newForm); +      onChange(errors === undefined ? (newForm as any) : undefined); +    } +   +    return ( +      <form class="pure-form"> +        <fieldset> +          <label for="username"> +            {i18n.str`Username`} +            {purpose === "create" && <b style={{ color: "red" }}>*</b>} +          </label> +          <input +            name="username" +            type="text" +            disabled={purpose !== "create"} +            value={form.username} +            onChange={(e) => { +              form.username = e.currentTarget.value; +              updateForm(structuredClone(form)); +            }} +          />{" "} +          <ShowInputErrorLabel +            message={errors?.username} +            isDirty={form.username !== undefined} +          /> +        </fieldset> +        <fieldset> +          <label> +            {i18n.str`Name`} +            {purpose === "create" && <b style={{ color: "red" }}>*</b>} +          </label> +          <input +            disabled={purpose !== "create"} +            value={form.name ?? ""} +            onChange={(e) => { +              form.name = e.currentTarget.value; +              updateForm(structuredClone(form)); +            }} +          /> +          <ShowInputErrorLabel +            message={errors?.name} +            isDirty={form.name !== undefined} +          /> +        </fieldset> +        {purpose !== "create" && ( +          <fieldset> +            <label>{i18n.str`Internal IBAN`}</label> +            <input +              disabled={true} +              value={form.iban ?? ""} +              onChange={(e) => { +                form.iban = e.currentTarget.value; +                updateForm(structuredClone(form)); +              }} +            /> +            <ShowInputErrorLabel +              message={errors?.iban} +              isDirty={form.iban !== undefined} +            /> +          </fieldset> +        )} +        <fieldset> +          <label> +            {i18n.str`Email`} +            {purpose !== "show" && <b style={{ color: "red" }}>*</b>} +          </label> +          <input +            disabled={purpose === "show"} +            value={form.contact_data.email ?? ""} +            onChange={(e) => { +              form.contact_data.email = e.currentTarget.value; +              updateForm(structuredClone(form)); +            }} +          /> +          <ShowInputErrorLabel +            message={errors?.contact_data?.email} +            isDirty={form.contact_data.email !== undefined} +          /> +        </fieldset> +        <fieldset> +          <label> +            {i18n.str`Phone`} +            {purpose !== "show" && <b style={{ color: "red" }}>*</b>} +          </label> +          <input +            disabled={purpose === "show"} +            value={form.contact_data.phone ?? ""} +            onChange={(e) => { +              form.contact_data.phone = e.currentTarget.value; +              updateForm(structuredClone(form)); +            }} +          /> +          <ShowInputErrorLabel +            message={errors?.contact_data?.phone} +            isDirty={form.contact_data?.phone !== undefined} +          /> +        </fieldset> +        <fieldset> +          <label> +            {i18n.str`Cashout address`} +            {purpose !== "show" && <b style={{ color: "red" }}>*</b>} +          </label> +          <input +            disabled={purpose === "show"} +            value={(form.cashout_address ?? "").substring("payto://iban/".length)} +            onChange={(e) => { +              form.cashout_address = "payto://iban/" + e.currentTarget.value; +              updateForm(structuredClone(form)); +            }} +          /> +          <ShowInputErrorLabel +            message={errors?.cashout_address} +            isDirty={form.cashout_address !== undefined} +          /> +        </fieldset> +      </form> +    ); +  } +   +  function initializeFromTemplate( +    account: SandboxBackend.Circuit.CircuitAccountData | undefined, +  ): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> { +    const emptyAccount = { +      cashout_address: undefined, +      iban: undefined, +      name: undefined, +      username: undefined, +      contact_data: undefined, +    }; +    const emptyContact = { +      email: undefined, +      phone: undefined, +    }; +   +    const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> = +      structuredClone(account) ?? emptyAccount; +    if (typeof initial.contact_data === "undefined") { +      initial.contact_data = emptyContact; +    } +    initial.contact_data.email; +    return initial as any; +  } +   +   +  
\ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx new file mode 100644 index 000000000..56b15818b --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx @@ -0,0 +1,120 @@ +import { h, VNode } from "preact"; +import { useBusinessAccounts } from "../../hooks/circuit.js"; +import { handleNotOkResult } from "../HomePage.js"; +import { AccountAction } from "./Home.js"; +import { Amounts } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; + +interface Props { +    onAction: (type: AccountAction, account: string) => void; +    account: string | undefined; +    onRegister: () => void; + +} + +export function AccountList({ account, onAction, onRegister }: Props): VNode { +    const result = useBusinessAccounts({ account }); +    const { i18n } = useTranslationContext(); + +    if (result.loading) return <div />; +    if (!result.ok) { +        return handleNotOkResult(i18n, onRegister)(result); +    } + +    const { customers } = result.data; +    return <section +        id="main" +        style={{ width: 600, marginLeft: "auto", marginRight: "auto" }} +    > +        {!customers.length ? ( +            <div></div> +        ) : ( +            <article> +                <h2>{i18n.str`Accounts:`}</h2> +                <div class="results"> +                    <table class="pure-table pure-table-striped"> +                        <thead> +                            <tr> +                                <th>{i18n.str`Username`}</th> +                                <th>{i18n.str`Name`}</th> +                                <th>{i18n.str`Balance`}</th> +                                <th>{i18n.str`Actions`}</th> +                            </tr> +                        </thead> +                        <tbody> +                            {customers.map((item, idx) => { +                                const balance = !item.balance +                                    ? undefined +                                    : Amounts.parse(item.balance.amount); +                                const balanceIsDebit = +                                    item.balance && +                                    item.balance.credit_debit_indicator == "debit"; +                                return ( +                                    <tr key={idx}> +                                        <td> +                                            <a +                                                href="#" +                                                onClick={(e) => { +                                                    e.preventDefault(); +                                                    onAction("show-details", item.username) +                                                }} +                                            > +                                                {item.username} +                                            </a> +                                        </td> +                                        <td>{item.name}</td> +                                        <td> +                                            {!balance ? ( +                                                i18n.str`unknown` +                                            ) : ( +                                                <span class="amount"> +                                                    {balanceIsDebit ? <b>-</b> : null} +                                                    <span class="value">{`${Amounts.stringifyValue( +                                                        balance, +                                                    )}`}</span> +                                                      +                                                    <span class="currency">{`${balance.currency}`}</span> +                                                </span> +                                            )} +                                        </td> +                                        <td> +                                            <a +                                                href="#" +                                                onClick={(e) => { +                                                    e.preventDefault(); +                                                    onAction("update-password", item.username) +                                                }} +                                            > +                                                change password +                                            </a> +                                              +                                            <a +                                                href="#" +                                                onClick={(e) => { +                                                    e.preventDefault(); +                                                    onAction("show-cashout", item.username) +                                                }} +                                            > +                                                cashouts +                                            </a> +                                              +                                            <a +                                                href="#" +                                                onClick={(e) => { +                                                    e.preventDefault(); +                                                    onAction("remove-account", item.username) +                                                }} +                                            > +                                                remove +                                            </a> +                                        </td> +                                    </tr> +                                ); +                            })} +                        </tbody> +                    </table> +                </div> +            </article> +        )} +    </section> +}
\ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx new file mode 100644 index 000000000..90835d52b --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -0,0 +1,107 @@ +import { RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h, Fragment } from "preact"; +import { useAdminAccountAPI } from "../../hooks/circuit.js"; +import { useState } from "preact/hooks"; +import { buildRequestErrorMessage } from "../../utils.js"; +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { getRandomPassword } from "../rnd.js"; +import { AccountForm } from "./AccountForm.js"; + +export function CreateNewAccount({ +    onClose, +    onCreateSuccess, +}: { +    onClose: () => void; +    onCreateSuccess: (password: string) => void; +}): VNode { +    const { i18n } = useTranslationContext(); +    const { createAccount } = useAdminAccountAPI(); +    const [submitAccount, setSubmitAccount] = useState< +        SandboxBackend.Circuit.CircuitAccountData | undefined +    >(); +    return ( +        <div> +            <div> +                <h1 class="nav welcome-text"> +                    <i18n.Translate>New account</i18n.Translate> +                </h1> +            </div> + +            <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}> +                <AccountForm +                    template={undefined} +                    purpose="create" +                    onChange={(a) => { +                        setSubmitAccount(a); +                    }} +                /> + +                <p> +                    <div style={{ display: "flex", justifyContent: "space-between" }}> +                        <div> +                            <input +                                class="pure-button" +                                type="submit" +                                value={i18n.str`Close`} +                                onClick={async (e) => { +                                    e.preventDefault(); +                                    onClose(); +                                }} +                            /> +                        </div> +                        <div> +                            <input +                                id="select-exchange" +                                class="pure-button pure-button-primary content" +                                disabled={!submitAccount} +                                type="submit" +                                value={i18n.str`Confirm`} +                                onClick={async (e) => { +                                    e.preventDefault(); + +                                    if (!submitAccount) return; +                                    try { +                                        const account: SandboxBackend.Circuit.CircuitAccountRequest = +                                        { +                                            cashout_address: submitAccount.cashout_address, +                                            contact_data: submitAccount.contact_data, +                                            internal_iban: submitAccount.iban, +                                            name: submitAccount.name, +                                            username: submitAccount.username, +                                            password: getRandomPassword(), +                                        }; + +                                        await createAccount(account); +                                        onCreateSuccess(account.password); +                                    } catch (error) { +                                        if (error instanceof RequestError) { +                                            notify( +                                                buildRequestErrorMessage(i18n, error.cause, { +                                                    onClientError: (status) => +                                                        status === HttpStatusCode.Forbidden +                                                            ? i18n.str`The rights to perform the operation are not sufficient` +                                                            : status === HttpStatusCode.BadRequest +                                                                ? i18n.str`Input data was invalid` +                                                                : status === HttpStatusCode.Conflict +                                                                    ? i18n.str`At least one registration detail was not available` +                                                                    : undefined, +                                                }), +                                            ); +                                        } else { +                                            notifyError( +                                                i18n.str`Operation failed, please report`, +                                                (error instanceof Error +                                                    ? error.message +                                                    : JSON.stringify(error)) as TranslatedString +                                            ) +                                        } +                                    } +                                }} +                            /> +                        </div> +                    </div> +                </p> +            </div> +        </div> +    ); +} diff --git a/packages/demobank-ui/src/pages/admin/Home.tsx b/packages/demobank-ui/src/pages/admin/Home.tsx new file mode 100644 index 000000000..e1ec6cfe0 --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/Home.tsx @@ -0,0 +1,162 @@ +import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { Cashouts } from "../../components/Cashouts/index.js"; +import { ShowCashoutDetails } from "../business/Home.js"; +import { handleNotOkResult } from "../HomePage.js"; +import { ShowAccountDetails } from "../ShowAccountDetails.js"; +import { UpdateAccountPassword } from "../UpdateAccountPassword.js"; +import { AdminAccount } from "./Account.js"; +import { AccountList } from "./AccountList.js"; +import { CreateNewAccount } from "./CreateNewAccount.js"; +import { RemoveAccount } from "./RemoveAccount.js"; + +/** + * Query account information and show QR code if there is pending withdrawal + */ +interface Props { +  onRegister: () => void; +} +export type AccountAction = "show-details" |  +  "show-cashout" |  +  "update-password" |  +  "remove-account" |  +  "show-cashouts-details"; + +export function AdminHome({ onRegister }: Props): VNode { +  const [action, setAction] = useState<{ +    type: AccountAction, +    account: string +  }>() + +  const [createAccount, setCreateAccount] = useState(false); + +  const { i18n } = useTranslationContext(); + +  if (action) { +    switch (action.type) { +      case "show-details": return <ShowCashoutDetails +        id={action.account} +        onLoadNotOk={handleNotOkResult(i18n, onRegister)} +        onCancel={() => { +          setAction(undefined); +        }} +      /> +      case "show-cashout": return ( +        <div> +          <div> +            <h1 class="nav welcome-text"> +              <i18n.Translate>Cashout for account {action.account}</i18n.Translate> +            </h1> +          </div> +          <Cashouts +            account={action.account} +            onSelected={(id) => { +              setAction({ +                type: "show-cashouts-details", +                account: action.account +              }); +            }} +          /> +          <p> +            <input +              class="pure-button" +              type="submit" +              value={i18n.str`Close`} +              onClick={async (e) => { +                e.preventDefault(); +                setAction(undefined); +              }} +            /> +          </p> +        </div> +      ) +      case "update-password": return <UpdateAccountPassword +        account={action.account} +        onLoadNotOk={handleNotOkResult(i18n, onRegister)} +        onUpdateSuccess={() => { +          notifyInfo(i18n.str`Password changed`); +          setAction(undefined); +        }} +        onClear={() => { +          setAction(undefined); +        }} +      /> +      case "remove-account": return <RemoveAccount +        account={action.account} +        onLoadNotOk={handleNotOkResult(i18n, onRegister)} +        onUpdateSuccess={() => { +          notifyInfo(i18n.str`Account removed`); +          setAction(undefined); +        }} +        onClear={() => { +          setAction(undefined); +        }} +      /> +      case "show-cashouts-details": return <ShowAccountDetails +        account={action.account} +        onLoadNotOk={handleNotOkResult(i18n, onRegister)} +        onChangePassword={() => { +          setAction({ +            type: "update-password", +            account: action.account, +          }) +        }} +        onUpdateSuccess={() => { +          notifyInfo(i18n.str`Account updated`); +          setAction(undefined); +        }} +        onClear={() => { +          setAction(undefined); +        }} +      /> +    } +  } + +  if (createAccount) { +    return ( +      <CreateNewAccount +        onClose={() => setCreateAccount(false)} +        onCreateSuccess={(password) => { +          notifyInfo( +            i18n.str`Account created with password "${password}". The user must change the password on the next login.`, +          ); +          setCreateAccount(false); +        }} +      /> +    ); +  } + +  return ( +    <Fragment> +      <div> +        <h1 class="nav welcome-text"> +          <i18n.Translate>Admin panel</i18n.Translate> +        </h1> +      </div> + +      <p> +        <div style={{ display: "flex", justifyContent: "space-between" }}> +          <div></div> +          <div> +            <input +              class="pure-button pure-button-primary content" +              type="submit" +              value={i18n.str`Create account`} +              onClick={async (e) => { +                e.preventDefault(); + +                setCreateAccount(true); +              }} +            /> +          </div> +        </div> +      </p> + +      <AdminAccount onRegister={onRegister} /> + +      <AccountList account={undefined} onAction={(type,account) => setAction({account, type})} onRegister={onRegister}/> + +    </Fragment> +  ); +}
\ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx new file mode 100644 index 000000000..2900db9d2 --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -0,0 +1,112 @@ +import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode,h,Fragment } from "preact"; +import { useAccountDetails } from "../../hooks/access.js"; +import { useAdminAccountAPI } from "../../hooks/circuit.js"; +import { Amounts, HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { buildRequestErrorMessage } from "../../utils.js"; + +export function RemoveAccount({ +    account, +    onClear, +    onUpdateSuccess, +    onLoadNotOk, +  }: { +    onLoadNotOk: <T>( +      error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, +    ) => VNode; +    onClear: () => void; +    onUpdateSuccess: () => void; +    account: string; +  }): VNode { +    const { i18n } = useTranslationContext(); +    const result = useAccountDetails(account); +    const { deleteAccount } = useAdminAccountAPI(); +   +    if (!result.ok) { +      if (result.loading || result.type === ErrorType.TIMEOUT) { +        return onLoadNotOk(result); +      } +      if (result.status === HttpStatusCode.NotFound) { +        return <div>account not found</div>; +      } +      return onLoadNotOk(result); +    } +   +    const balance = Amounts.parse(result.data.balance.amount); +    if (!balance) { +      return <div>there was an error reading the balance</div>; +    } +    const isBalanceEmpty = Amounts.isZero(balance); +    return ( +      <div> +        <div> +          <h1 class="nav welcome-text"> +            <i18n.Translate>Remove account: {account}</i18n.Translate> +          </h1> +        </div> +        {/* {FXME: SHOW WARNING} */} +        {/* {!isBalanceEmpty && ( +          <ErrorBannerFloat +            error={{ +              title: i18n.str`Can't delete the account`, +              description: i18n.str`Balance is not empty`, +            }} +            onClear={() => saveError(undefined)} +          /> +        )} */} +   +        <p> +          <div style={{ display: "flex", justifyContent: "space-between" }}> +            <div> +              <input +                class="pure-button" +                type="submit" +                value={i18n.str`Cancel`} +                onClick={async (e) => { +                  e.preventDefault(); +                  onClear(); +                }} +              /> +            </div> +            <div> +              <input +                id="select-exchange" +                class="pure-button pure-button-primary content" +                disabled={!isBalanceEmpty} +                type="submit" +                value={i18n.str`Confirm`} +                onClick={async (e) => { +                  e.preventDefault(); +                  try { +                    const r = await deleteAccount(account); +                    onUpdateSuccess(); +                  } catch (error) { +                    if (error instanceof RequestError) { +                      notify( +                        buildRequestErrorMessage(i18n, error.cause, { +                          onClientError: (status) => +                            status === HttpStatusCode.Forbidden +                              ? i18n.str`The administrator specified a institutional username` +                              : status === HttpStatusCode.NotFound +                                ? i18n.str`The username was not found` +                                : status === HttpStatusCode.PreconditionFailed +                                  ? i18n.str`Balance was not zero` +                                  : undefined, +                        }), +                      ); +                    } else { +                      notifyError(i18n.str`Operation failed, please report`, +                        (error instanceof Error +                            ? error.message +                            : JSON.stringify(error)) as TranslatedString); +                    } +                  } +                }} +              /> +            </div> +          </div> +        </p> +      </div> +    ); +  } +  
\ No newline at end of file diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/business/Home.tsx index ec71ceca6..8beea640a 100644 --- a/packages/demobank-ui/src/pages/BusinessAccount.tsx +++ b/packages/demobank-ui/src/pages/business/Home.tsx @@ -30,52 +30,51 @@ import {  } from "@gnu-taler/web-util/browser";  import { Fragment, VNode, h } from "preact";  import { useEffect, useState } from "preact/hooks"; -import { Cashouts } from "../components/Cashouts/index.js"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; -import { useBackendContext } from "../context/backend.js"; -import { useAccountDetails } from "../hooks/access.js"; +import { Cashouts } from "../../components/Cashouts/index.js"; +import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { useBackendContext } from "../../context/backend.js"; +import { useAccountDetails } from "../../hooks/access.js";  import {    useCashoutDetails,    useCircuitAccountAPI,    useEstimator,    useRatiosAndFeeConfig, -} from "../hooks/circuit.js"; +} from "../../hooks/circuit.js";  import {    TanChannel,    buildRequestErrorMessage,    undefinedIfEmpty, -} from "../utils.js"; -import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js"; -import { handleNotOkResult } from "./HomePage.js"; -import { LoginForm } from "./LoginForm.js"; -import { Amount } from "./PaytoWireTransferForm.js"; +} from "../../utils.js"; +import { handleNotOkResult } from "../HomePage.js"; +import { LoginForm } from "../LoginForm.js"; +import { Amount } from "../PaytoWireTransferForm.js"; +import { ShowAccountDetails } from "../ShowAccountDetails.js"; +import { UpdateAccountPassword } from "../UpdateAccountPassword.js";  interface Props { +  account: string,    onClose: () => void;    onRegister: () => void;    onLoadNotOk: () => void;  }  export function BusinessAccount({    onClose, +  account,    onLoadNotOk,    onRegister,  }: Props): VNode {    const { i18n } = useTranslationContext(); -  const backend = useBackendContext();    const [updatePassword, setUpdatePassword] = useState(false);    const [newCashout, setNewcashout] = useState(false);    const [showCashoutDetails, setShowCashoutDetails] = useState<      string | undefined    >(); -  if (backend.state.status === "loggedOut") { -    return <LoginForm onRegister={onRegister} />; -  }    if (newCashout) {      return (        <CreateCashout -        account={backend.state.username} +        account={account}          onLoadNotOk={handleNotOkResult(i18n, onRegister)}          onCancel={() => {            setNewcashout(false); @@ -104,7 +103,7 @@ export function BusinessAccount({    if (updatePassword) {      return (        <UpdateAccountPassword -        account={backend.state.username} +        account={account}          onLoadNotOk={handleNotOkResult(i18n, onRegister)}          onUpdateSuccess={() => {            notifyInfo(i18n.str`Password changed`); @@ -119,7 +118,7 @@ export function BusinessAccount({    return (      <div>        <ShowAccountDetails -        account={backend.state.username} +        account={account}          onLoadNotOk={handleNotOkResult(i18n, onRegister)}          onUpdateSuccess={() => {            notifyInfo(i18n.str`Account updated`); @@ -133,7 +132,7 @@ export function BusinessAccount({          <div class="active">            <h3>{i18n.str`Latest cashouts`}</h3>            <Cashouts -            account={backend.state.username} +            account={account}              onSelected={(id) => {                setShowCashoutDetails(id);              }} | 
