From 062939d9cc016a186a282f7a48492c3e01cd740c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 21 Sep 2023 10:31:10 -0300 Subject: [PATCH] admin refactor --- .../demobank-ui/src/components/Routing.tsx | 13 +- packages/demobank-ui/src/pages/AdminPage.tsx | 1042 ----------------- packages/demobank-ui/src/pages/HomePage.tsx | 19 +- .../src/pages/ShowAccountDetails.tsx | 143 +++ .../src/pages/UpdateAccountPassword.tsx | 131 +++ .../pages/WithdrawalConfirmationQuestion.tsx | 3 +- .../src/pages/WithdrawalQRCode.tsx | 4 - .../demobank-ui/src/pages/admin/Account.tsx | 56 + .../src/pages/admin/AccountForm.tsx | 219 ++++ .../src/pages/admin/AccountList.tsx | 120 ++ .../src/pages/admin/CreateNewAccount.tsx | 107 ++ packages/demobank-ui/src/pages/admin/Home.tsx | 162 +++ .../src/pages/admin/RemoveAccount.tsx | 112 ++ .../Home.tsx} | 35 +- 14 files changed, 1083 insertions(+), 1083 deletions(-) delete mode 100644 packages/demobank-ui/src/pages/AdminPage.tsx create mode 100644 packages/demobank-ui/src/pages/ShowAccountDetails.tsx create mode 100644 packages/demobank-ui/src/pages/UpdateAccountPassword.tsx create mode 100644 packages/demobank-ui/src/pages/admin/Account.tsx create mode 100644 packages/demobank-ui/src/pages/admin/AccountForm.tsx create mode 100644 packages/demobank-ui/src/pages/admin/AccountList.tsx create mode 100644 packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx create mode 100644 packages/demobank-ui/src/pages/admin/Home.tsx create mode 100644 packages/demobank-ui/src/pages/admin/RemoveAccount.tsx rename packages/demobank-ui/src/pages/{BusinessAccount.tsx => business/Home.tsx} (96%) 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 { route("/business"); }} @@ -63,7 +64,7 @@ export function Routing(): VNode { } - const isAdmin = backend.state.isUserAdministrator + const { isUserAdministrator, username } = backend.state return ( { - if (isAdmin) { - return { route("/register"); }} />; } else { return { route(`/operation/${wopid}`); }} @@ -130,6 +132,7 @@ export function Routing(): VNode { path="/business" component={() => ( { 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 - */ - -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(); - const [showDetails, setShowDetails] = useState(); - const [showCashouts, setShowCashouts] = useState(); - const [updatePassword, setUpdatePassword] = useState(); - const [removeAccount, setRemoveAccount] = useState(); - const [showCashoutDetails, setShowCashoutDetails] = useState< - string | undefined - >(); - - const [createAccount, setCreateAccount] = useState(false); - - const result = useBusinessAccounts({ account }); - const { i18n } = useTranslationContext(); - - if (result.loading) return
; - if (!result.ok) { - return handleNotOkResult(i18n, onRegister)(result); - } - - const { customers } = result.data; - - if (showCashoutDetails) { - return ( - { - setShowCashoutDetails(undefined); - }} - /> - ); - } - - if (showCashouts) { - return ( -
-
-

- Cashout for account {showCashouts} -

-
- { - setShowCashouts(id); - setShowCashouts(undefined); - }} - /> -

- { - e.preventDefault(); - setShowCashouts(undefined); - }} - /> -

-
- ); - } - - if (showDetails) { - return ( - { - setUpdatePassword(showDetails); - setShowDetails(undefined); - }} - onUpdateSuccess={() => { - notifyInfo(i18n.str`Account updated`); - setShowDetails(undefined); - }} - onClear={() => { - setShowDetails(undefined); - }} - /> - ); - } - if (removeAccount) { - return ( - { - notifyInfo(i18n.str`Account removed`); - setRemoveAccount(undefined); - }} - onClear={() => { - setRemoveAccount(undefined); - }} - /> - ); - } - if (updatePassword) { - return ( - { - notifyInfo(i18n.str`Password changed`); - setUpdatePassword(undefined); - }} - onClear={() => { - setUpdatePassword(undefined); - }} - /> - ); - } - if (createAccount) { - return ( - 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 ( - -
-

- Admin panel -

-
- -

-

-
-
- { - e.preventDefault(); - - setCreateAccount(true); - }} - /> -
-
-

- - -
- {!customers.length ? ( -
- ) : ( -
-

{i18n.str`Accounts:`}

-
- - - - - - - - - - - {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 ( - - - - - - - ); - })} - -
{i18n.str`Username`}{i18n.str`Name`}{i18n.str`Balance`}{i18n.str`Actions`}
- { - e.preventDefault(); - setShowDetails(item.username); - }} - > - {item.username} - - {item.name} - {!balance ? ( - i18n.str`unknown` - ) : ( - - {balanceIsDebit ? - : null} - {`${Amounts.stringifyValue( - balance, - )}`} -   - {`${balance.currency}`} - - )} - - { - e.preventDefault(); - setUpdatePassword(item.username); - }} - > - change password - -   - { - e.preventDefault(); - setShowCashouts(item.username); - }} - > - cashouts - -   - { - e.preventDefault(); - setRemoveAccount(item.username); - }} - > - remove - -
-
-
- )} -
-
- ); -} - -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 ; - return ( - -
-
-

{i18n.str`Bank account balance`}

- {!balance ? ( -
- Waiting server response... -
- ) : ( -
- {balanceIsDebit ? - : null} - {`${Amounts.stringifyValue(balance)}`} -   - {`${balance.currency}`} -
- )} -
-
- { - notifyInfo(i18n.str`Wire transfer created!`); - }} - onCancel={undefined} - /> -
- ); -} - -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 { - const emptyAccount = { - cashout_address: undefined, - iban: undefined, - name: undefined, - username: undefined, - contact_data: undefined, - }; - const emptyContact = { - email: undefined, - phone: undefined, - }; - - const initial: PartialButDefined = - 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: ( - error: HttpResponsePaginated, - ) => VNode; - onClear: () => void; - onUpdateSuccess: () => void; - account: string; -}): VNode { - const { i18n } = useTranslationContext(); - const result = useBusinessAccountDetails(account); - const { changePassword } = useAdminAccountAPI(); - const [password, setPassword] = useState(); - const [repeat, setRepeat] = useState(); - - if (!result.ok) { - if (result.loading || result.type === ErrorType.TIMEOUT) { - return onLoadNotOk(result); - } - if (result.status === HttpStatusCode.NotFound) { - return
account not found
; - } - 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 ( -
-
-

- Update password for {account} -

-
- -
-
-
- - { - setPassword(e.currentTarget.value); - }} - /> - -
-
- - { - setRepeat(e.currentTarget.value); - }} - /> - -
-
-

-

-
- { - e.preventDefault(); - onClear(); - }} - /> -
-
- { - 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) - } - } - }} - /> -
-
-

-
-
- ); -} - -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 ( -
-
-

- New account -

-
- -
- { - setSubmitAccount(a); - }} - /> - -

-

-
- { - e.preventDefault(); - onClose(); - }} - /> -
-
- { - 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 - ) - } - } - }} - /> -
-
-

-
-
- ); -} - -export function ShowAccountDetails({ - account, - onClear, - onUpdateSuccess, - onLoadNotOk, - onChangePassword, -}: { - onLoadNotOk: ( - error: HttpResponsePaginated, - ) => 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
account not found
; - } - return onLoadNotOk(result); - } - - return ( -
-
-

- Business account details -

-
-
- setSubmitAccount(a)} - /> - -
-
- {onClear ? ( - { - e.preventDefault(); - onClear(); - }} - /> - ) : undefined} -
-
-
- { - e.preventDefault(); - onChangePassword(); - }} - /> -
-
- { - 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 - ) - } - } - } - }} - /> -
-
-
-

-
-
- ); -} - -function RemoveAccount({ - account, - onClear, - onUpdateSuccess, - onLoadNotOk, -}: { - onLoadNotOk: ( - error: HttpResponsePaginated, - ) => 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
account not found
; - } - return onLoadNotOk(result); - } - - const balance = Amounts.parse(result.data.balance.amount); - if (!balance) { - return
there was an error reading the balance
; - } - const isBalanceEmpty = Amounts.isZero(balance); - return ( -
-
-

- Remove account: {account} -

-
- {/* {FXME: SHOW WARNING} */} - {/* {!isBalanceEmpty && ( - saveError(undefined)} - /> - )} */} - -

-

-
- { - e.preventDefault(); - onClear(); - }} - /> -
-
- { - 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); - } - } - }} - /> -
-
-

-
- ); -} -/** - * 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 | undefined - >(undefined); - const { i18n } = useTranslationContext(); - - function updateForm(newForm: typeof initial): void { - const parsed = !newForm.cashout_address - ? undefined - : parsePaytoUri(newForm.cashout_address); - - const errors = undefinedIfEmpty>({ - 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.username = e.currentTarget.value; - updateForm(structuredClone(form)); - }} - />{" "} - -
-
- - { - form.name = e.currentTarget.value; - updateForm(structuredClone(form)); - }} - /> - -
- {purpose !== "create" && ( -
- - { - form.iban = e.currentTarget.value; - updateForm(structuredClone(form)); - }} - /> - -
- )} -
- - { - form.contact_data.email = e.currentTarget.value; - updateForm(structuredClone(form)); - }} - /> - -
-
- - { - form.contact_data.phone = e.currentTarget.value; - updateForm(structuredClone(form)); - }} - /> - -
-
- - { - form.cashout_address = "payto://iban/" + e.currentTarget.value; - updateForm(structuredClone(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 ; - } - if (settings.currentWithdrawalOperationId) { onPendingOperationFound(settings.currentWithdrawalOperationId); return ; } - if (backend.state.isUserAdministrator) { - return ; - } - return ( ); @@ -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 ; } 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: ( + error: HttpResponsePaginated, + ) => 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
account not found
; + } + return onLoadNotOk(result); + } + + return ( +
+
+

+ Business account details +

+
+
+ setSubmitAccount(a)} + /> + +
+
+ {onClear ? ( + { + e.preventDefault(); + onClear(); + }} + /> + ) : undefined} +
+
+
+ { + e.preventDefault(); + onChangePassword(); + }} + /> +
+
+ { + 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 + ) + } + } + } + }} + /> +
+
+
+

+
+
+ ); + } + \ 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: ( + error: HttpResponsePaginated, + ) => VNode; + onClear: () => void; + onUpdateSuccess: () => void; + account: string; + }): VNode { + const { i18n } = useTranslationContext(); + const result = useBusinessAccountDetails(account); + const { changePassword } = useAdminAccountAPI(); + const [password, setPassword] = useState(); + const [repeat, setRepeat] = useState(); + + if (!result.ok) { + if (result.loading || result.type === ErrorType.TIMEOUT) { + return onLoadNotOk(result); + } + if (result.status === HttpStatusCode.NotFound) { + return
account not found
; + } + 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 ( +
+
+

+ Update password for {account} +

+
+ +
+
+
+ + { + setPassword(e.currentTarget.value); + }} + /> + +
+
+ + { + setRepeat(e.currentTarget.value); + }} + /> + +
+
+

+

+
+ { + e.preventDefault(); + onClear(); + }} + /> +
+
+ { + 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) + } + } + }} + /> +
+
+

+
+
+ ); + } \ 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({
Amount
-
{Amounts.stringifyValue(details.amount)}
+
To be added
+ {/* Amounts.stringifyValue(details.amount) */}
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
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 ; + return ( + +
+
+

{i18n.str`Bank account balance`}

+ {!balance ? ( +
+ Waiting server response... +
+ ) : ( +
+ {balanceIsDebit ? - : null} + {`${Amounts.stringifyValue(balance)}`} +   + {`${balance.currency}`} +
+ )} +
+
+ { + notifyInfo(i18n.str`Wire transfer created!`); + }} + onCancel={undefined} + /> +
+ ); + } + \ 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 | undefined + >(undefined); + const { i18n } = useTranslationContext(); + + function updateForm(newForm: typeof initial): void { + const parsed = !newForm.cashout_address + ? undefined + : parsePaytoUri(newForm.cashout_address); + + const errors = undefinedIfEmpty>({ + 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.username = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + />{" "} + +
+
+ + { + form.name = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
+ {purpose !== "create" && ( +
+ + { + form.iban = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
+ )} +
+ + { + form.contact_data.email = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
+
+ + { + form.contact_data.phone = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
+
+ + { + form.cashout_address = "payto://iban/" + e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
+
+ ); + } + + function initializeFromTemplate( + account: SandboxBackend.Circuit.CircuitAccountData | undefined, + ): WithIntermediate { + const emptyAccount = { + cashout_address: undefined, + iban: undefined, + name: undefined, + username: undefined, + contact_data: undefined, + }; + const emptyContact = { + email: undefined, + phone: undefined, + }; + + const initial: PartialButDefined = + 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
; + if (!result.ok) { + return handleNotOkResult(i18n, onRegister)(result); + } + + const { customers } = result.data; + return
+ {!customers.length ? ( +
+ ) : ( +
+

{i18n.str`Accounts:`}

+
+ + + + + + + + + + + {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 ( + + + + + + + ); + })} + +
{i18n.str`Username`}{i18n.str`Name`}{i18n.str`Balance`}{i18n.str`Actions`}
+ { + e.preventDefault(); + onAction("show-details", item.username) + }} + > + {item.username} + + {item.name} + {!balance ? ( + i18n.str`unknown` + ) : ( + + {balanceIsDebit ? - : null} + {`${Amounts.stringifyValue( + balance, + )}`} +   + {`${balance.currency}`} + + )} + + { + e.preventDefault(); + onAction("update-password", item.username) + }} + > + change password + +   + { + e.preventDefault(); + onAction("show-cashout", item.username) + }} + > + cashouts + +   + { + e.preventDefault(); + onAction("remove-account", item.username) + }} + > + remove + +
+
+
+ )} +
+} \ 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 ( +
+
+

+ New account +

+
+ +
+ { + setSubmitAccount(a); + }} + /> + +

+

+
+ { + e.preventDefault(); + onClose(); + }} + /> +
+
+ { + 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 + ) + } + } + }} + /> +
+
+

+
+
+ ); +} 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 { + setAction(undefined); + }} + /> + case "show-cashout": return ( +
+
+

+ Cashout for account {action.account} +

+
+ { + setAction({ + type: "show-cashouts-details", + account: action.account + }); + }} + /> +

+ { + e.preventDefault(); + setAction(undefined); + }} + /> +

+
+ ) + case "update-password": return { + notifyInfo(i18n.str`Password changed`); + setAction(undefined); + }} + onClear={() => { + setAction(undefined); + }} + /> + case "remove-account": return { + notifyInfo(i18n.str`Account removed`); + setAction(undefined); + }} + onClear={() => { + setAction(undefined); + }} + /> + case "show-cashouts-details": return { + setAction({ + type: "update-password", + account: action.account, + }) + }} + onUpdateSuccess={() => { + notifyInfo(i18n.str`Account updated`); + setAction(undefined); + }} + onClear={() => { + setAction(undefined); + }} + /> + } + } + + if (createAccount) { + return ( + 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 ( + +
+

+ Admin panel +

+
+ +

+

+
+
+ { + e.preventDefault(); + + setCreateAccount(true); + }} + /> +
+
+

+ + + + setAction({account, type})} onRegister={onRegister}/> + +
+ ); +} \ 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: ( + error: HttpResponsePaginated, + ) => 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
account not found
; + } + return onLoadNotOk(result); + } + + const balance = Amounts.parse(result.data.balance.amount); + if (!balance) { + return
there was an error reading the balance
; + } + const isBalanceEmpty = Amounts.isZero(balance); + return ( +
+
+

+ Remove account: {account} +

+
+ {/* {FXME: SHOW WARNING} */} + {/* {!isBalanceEmpty && ( + saveError(undefined)} + /> + )} */} + +

+

+
+ { + e.preventDefault(); + onClear(); + }} + /> +
+
+ { + 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); + } + } + }} + /> +
+
+

+
+ ); + } + \ 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 similarity index 96% rename from packages/demobank-ui/src/pages/BusinessAccount.tsx rename to 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 ; - } if (newCashout) { return ( { setNewcashout(false); @@ -104,7 +103,7 @@ export function BusinessAccount({ if (updatePassword) { return ( { notifyInfo(i18n.str`Password changed`); @@ -119,7 +118,7 @@ export function BusinessAccount({ return (
{ notifyInfo(i18n.str`Account updated`); @@ -133,7 +132,7 @@ export function BusinessAccount({

{i18n.str`Latest cashouts`}

{ setShowCashoutDetails(id); }}