From 062939d9cc016a186a282f7a48492c3e01cd740c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 21 Sep 2023 10:31:10 -0300 Subject: admin refactor --- packages/demobank-ui/src/pages/business/Home.tsx | 757 +++++++++++++++++++++++ 1 file changed, 757 insertions(+) create mode 100644 packages/demobank-ui/src/pages/business/Home.tsx (limited to 'packages/demobank-ui/src/pages/business/Home.tsx') diff --git a/packages/demobank-ui/src/pages/business/Home.tsx b/packages/demobank-ui/src/pages/business/Home.tsx new file mode 100644 index 000000000..8beea640a --- /dev/null +++ b/packages/demobank-ui/src/pages/business/Home.tsx @@ -0,0 +1,757 @@ +/* + 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 { + AmountJson, + Amounts, + HttpStatusCode, + TranslatedString +} from "@gnu-taler/taler-util"; +import { + HttpResponse, + HttpResponsePaginated, + RequestError, + notify, + notifyError, + notifyInfo, + useTranslationContext, +} 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 { + useCashoutDetails, + useCircuitAccountAPI, + useEstimator, + useRatiosAndFeeConfig, +} from "../../hooks/circuit.js"; +import { + TanChannel, + buildRequestErrorMessage, + undefinedIfEmpty, +} 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 [updatePassword, setUpdatePassword] = useState(false); + const [newCashout, setNewcashout] = useState(false); + const [showCashoutDetails, setShowCashoutDetails] = useState< + string | undefined + >(); + + + if (newCashout) { + return ( + { + setNewcashout(false); + }} + onComplete={(id) => { + notifyInfo( + i18n.str`Cashout created. You need to confirm the operation to complete the transaction.`, + ); + setNewcashout(false); + setShowCashoutDetails(id); + }} + /> + ); + } + if (showCashoutDetails) { + return ( + { + setShowCashoutDetails(undefined); + }} + /> + ); + } + if (updatePassword) { + return ( + { + notifyInfo(i18n.str`Password changed`); + setUpdatePassword(false); + }} + onClear={() => { + setUpdatePassword(false); + }} + /> + ); + } + return ( +
+ { + notifyInfo(i18n.str`Account updated`); + }} + onChangePassword={() => { + setUpdatePassword(true); + }} + onClear={onClose} + /> +
+
+

{i18n.str`Latest cashouts`}

+ { + setShowCashoutDetails(id); + }} + /> +
+
+
+
+ { + e.preventDefault(); + setNewcashout(true); + }} + /> +
+
+
+ ); +} + +interface PropsCashout { + account: string; + onComplete: (id: string) => void; + onCancel: () => void; + onLoadNotOk: ( + error: + | HttpResponsePaginated + | HttpResponse, + ) => VNode; +} + +type FormType = { + isDebit: boolean; + amount: string; + subject: string; + channel: TanChannel; +}; +type ErrorFrom = { + [P in keyof T]+?: string; +}; + +// check #7719 +function useRatiosAndFeeConfigWithChangeDetection(): HttpResponse< + SandboxBackend.Circuit.Config & { hasChanged?: boolean }, + SandboxBackend.SandboxError +> { + const result = useRatiosAndFeeConfig(); + const [oldResult, setOldResult] = useState< + SandboxBackend.Circuit.Config | undefined + >(undefined); + const dataFromBackend = result.ok ? result.data : undefined; + useEffect(() => { + // save only the first result of /config to the backend + if (!dataFromBackend || oldResult !== undefined) return; + setOldResult(dataFromBackend); + }, [dataFromBackend]); + + if (!result.ok) return result; + + const data = !oldResult ? result.data : oldResult; + const hasChanged = + oldResult && + (result.data.name !== oldResult.name || + result.data.version !== oldResult.version || + result.data.ratios_and_fees.buy_at_ratio !== + oldResult.ratios_and_fees.buy_at_ratio || + result.data.ratios_and_fees.buy_in_fee !== + oldResult.ratios_and_fees.buy_in_fee || + result.data.ratios_and_fees.sell_at_ratio !== + oldResult.ratios_and_fees.sell_at_ratio || + result.data.ratios_and_fees.sell_out_fee !== + oldResult.ratios_and_fees.sell_out_fee || + result.data.fiat_currency !== oldResult.fiat_currency); + + return { + ...result, + data: { ...data, hasChanged }, + }; +} + +function CreateCashout({ + account, + onComplete, + onCancel, + onLoadNotOk, +}: PropsCashout): VNode { + const { i18n } = useTranslationContext(); + const ratiosResult = useRatiosAndFeeConfig(); + const result = useAccountDetails(account); + const { + estimateByCredit: calculateFromCredit, + estimateByDebit: calculateFromDebit, + } = useEstimator(); + const [form, setForm] = useState>({ isDebit: true }); + + const { createCashout } = useCircuitAccountAPI(); + if (!result.ok) return onLoadNotOk(result); + if (!ratiosResult.ok) return onLoadNotOk(ratiosResult); + const config = ratiosResult.data; + + const balance = Amounts.parseOrThrow(result.data.balance.amount); + const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold); + const zero = Amounts.zeroOfCurrency(balance.currency); + const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit"; + const limit = balanceIsDebit + ? Amounts.sub(debitThreshold, balance).amount + : Amounts.add(balance, debitThreshold).amount; + + const zeroCalc = { debit: zero, credit: zero, beforeFee: zero }; + const [calc, setCalc] = useState(zeroCalc); + const sellRate = config.ratios_and_fees.sell_at_ratio; + const sellFee = !config.ratios_and_fees.sell_out_fee + ? zero + : Amounts.parseOrThrow( + `${balance.currency}:${config.ratios_and_fees.sell_out_fee}`, + ); + const fiatCurrency = config.fiat_currency; + + if (!sellRate || sellRate < 0) return
error rate
; + + const amount = Amounts.parseOrThrow( + `${!form.isDebit ? fiatCurrency : balance.currency}:${ + !form.amount ? "0" : form.amount + }`, + ); + + useEffect(() => { + if (form.isDebit) { + calculateFromDebit(amount, sellFee, sellRate) + .then((r) => { + setCalc(r); + }) + .catch((error) => { + notify( + error instanceof RequestError + ? buildRequestErrorMessage(i18n, error.cause) + : { + type: "error", + title: i18n.str`Could not estimate the cashout`, + description: error.message as TranslatedString + }, + ); + }); + } else { + calculateFromCredit(amount, sellFee, sellRate) + .then((r) => { + setCalc(r); + }) + .catch((error) => { + notify( + error instanceof RequestError + ? buildRequestErrorMessage(i18n, error.cause) + : { + type: "error", + title: i18n.str`Could not estimate the cashout`, + description: error.message, + }, + ); + }); + } + }, [form.amount, form.isDebit]); + + const balanceAfter = Amounts.sub(balance, calc.debit).amount; + + function updateForm(newForm: typeof form): void { + setForm(newForm); + } + const errors = undefinedIfEmpty>({ + amount: !form.amount + ? i18n.str`required` + : !amount + ? i18n.str`could not be parsed` + : Amounts.cmp(limit, calc.debit) === -1 + ? i18n.str`balance is not enough` + : Amounts.cmp(calc.beforeFee, sellFee) === -1 + ? i18n.str`the total amount to transfer does not cover the fees` + : Amounts.isZero(calc.credit) + ? i18n.str`the total transfer at destination will be zero` + : undefined, + channel: !form.channel ? i18n.str`required` : undefined, + }); + + return ( +
+

New cashout

+
+
+ + { + form.subject = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
+
+ +
+ { + form.amount = v; + updateForm(structuredClone(form)); + }} + error={errors?.amount} + /> + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
{" "} + {Amounts.isZero(sellFee) ? undefined : ( + +
+ + +
+ +
+ + +
+
+ )} +
+ + +
+
+ + +
+ { + e.preventDefault(); + form.channel = TanChannel.EMAIL; + updateForm(structuredClone(form)); + }} + /> + { + e.preventDefault(); + form.channel = TanChannel.SMS; + updateForm(structuredClone(form)); + }} + /> + { + e.preventDefault(); + form.channel = TanChannel.FILE; + updateForm(structuredClone(form)); + }} + /> +
+ +
+
+
+ + + +
+
+
+ ); +} + +interface ShowCashoutProps { + id: string; + onCancel: () => void; + onLoadNotOk: ( + error: HttpResponsePaginated, + ) => VNode; +} +export function ShowCashoutDetails({ + id, + onCancel, + onLoadNotOk, +}: ShowCashoutProps): VNode { + const { i18n } = useTranslationContext(); + const result = useCashoutDetails(id); + const { abortCashout, confirmCashout } = useCircuitAccountAPI(); + const [code, setCode] = useState(undefined); + if (!result.ok) return onLoadNotOk(result); + const errors = undefinedIfEmpty({ + code: !code ? i18n.str`required` : undefined, + }); + const isPending = String(result.data.status).toUpperCase() === "PENDING"; + return ( +
+

Cashout details {id}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {isPending ? ( +
+ + { + setCode(e.currentTarget.value); + }} + /> + +
+ ) : undefined} +
+
+
+ + {isPending ? ( +
+ +   + +
+ ) : ( +
+ )} +
+
+ ); +} + +const MAX_AMOUNT_DIGIT = 2; +/** + * Truncate the amount of digits to display + * in the form based on the fee calculations + * + * Backend must have the same truncation + * @param a + * @returns + */ +function truncate(a: AmountJson): AmountJson { + const str = Amounts.stringify(a); + const idx = str.indexOf("."); + if (idx === -1) { + return a; + } + const truncated = str.substring(0, idx + 1 + MAX_AMOUNT_DIGIT); + return Amounts.parseOrThrow(truncated); +} + +export function assertUnreachable(x: never): never { + throw new Error("Didn't expect to get here"); +} -- cgit v1.2.3 From 0b7bbed99d155ba030a1328e357ab6751bdbb835 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 21 Sep 2023 13:10:16 -0300 Subject: more ui: business and admin --- packages/demobank-ui/src/components/Routing.tsx | 17 +- .../src/components/ShowInputErrorLabel.tsx | 4 +- .../demobank-ui/src/pages/AccountPage/index.ts | 5 +- .../demobank-ui/src/pages/AccountPage/state.ts | 7 +- .../demobank-ui/src/pages/AccountPage/views.tsx | 29 +- packages/demobank-ui/src/pages/BankFrame.tsx | 32 +- packages/demobank-ui/src/pages/HomePage.tsx | 6 +- packages/demobank-ui/src/pages/PaymentOptions.tsx | 131 +++--- .../src/pages/PaytoWireTransferForm.tsx | 6 +- .../demobank-ui/src/pages/RegistrationPage.tsx | 2 +- .../demobank-ui/src/pages/ShowAccountDetails.tsx | 278 ++++++------ .../src/pages/UpdateAccountPassword.tsx | 278 +++++++----- packages/demobank-ui/src/pages/admin/Account.tsx | 72 ++- .../demobank-ui/src/pages/admin/AccountForm.tsx | 494 +++++++++++++-------- .../demobank-ui/src/pages/admin/AccountList.tsx | 182 +++++--- .../src/pages/admin/CreateNewAccount.tsx | 174 ++++---- packages/demobank-ui/src/pages/admin/Home.tsx | 51 +-- .../demobank-ui/src/pages/admin/RemoveAccount.tsx | 302 +++++++++---- packages/demobank-ui/src/pages/business/Home.tsx | 2 +- 19 files changed, 1181 insertions(+), 891 deletions(-) (limited to 'packages/demobank-ui/src/pages/business/Home.tsx') diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx index ef11af76e..b8e39948b 100644 --- a/packages/demobank-ui/src/components/Routing.tsx +++ b/packages/demobank-ui/src/components/Routing.tsx @@ -33,12 +33,7 @@ export function Routing(): VNode { const backend = useBackendContext(); if (backend.state.status === "loggedOut") { - return { - route("/business"); - }} - > + return { - route("/business"); - }} - > + { route(`/operation/${wopid}`); }} + goToBusinessAccount={() => { + route("/business"); + }} onRegister={() => { route("/register"); }} diff --git a/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx index dacffe20a..c5840cad9 100644 --- a/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx +++ b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx @@ -24,6 +24,6 @@ export function ShowInputErrorLabel({ isDirty: boolean; }): VNode { if (message && isDirty) - return
{message}
; - return ; + return
{message}
; + return
; } diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts index ed6945f84..128a6d30f 100644 --- a/packages/demobank-ui/src/pages/AccountPage/index.ts +++ b/packages/demobank-ui/src/pages/AccountPage/index.ts @@ -28,6 +28,7 @@ export interface Props { onLoadNotOk: ( error: HttpResponsePaginated, ) => VNode; + goToBusinessAccount: () => void; } export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound; @@ -51,10 +52,8 @@ export namespace State { status: "ready"; error: undefined; account: string, - payto: PaytoUriIBAN | PaytoUriTalerBank, - balance: AmountJson, - balanceIsDebit: boolean, limit: AmountJson, + goToBusinessAccount: () => void; } export interface InvalidIban { diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts index 2249b743e..a57e19901 100644 --- a/packages/demobank-ui/src/pages/AccountPage/state.ts +++ b/packages/demobank-ui/src/pages/AccountPage/state.ts @@ -20,7 +20,7 @@ import { useBackendContext } from "../../context/backend.js"; import { useAccountDetails } from "../../hooks/access.js"; import { Props, State } from "./index.js"; -export function useComponentState({ account, onLoadNotOk }: Props): State { +export function useComponentState({ account, goToBusinessAccount }: Props): State { const result = useAccountDetails(account); const backend = useBackendContext(); const { i18n } = useTranslationContext(); @@ -60,7 +60,6 @@ export function useComponentState({ account, onLoadNotOk }: Props): State { const payto = parsePaytoUri(data.paytoUri); if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) { - console.log(payto) return { status: "invalid-iban", error: result @@ -75,11 +74,9 @@ export function useComponentState({ account, onLoadNotOk }: Props): State { return { status: "ready", + goToBusinessAccount, error: undefined, account, - balance, - balanceIsDebit, limit, - payto }; } diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx index f2cbbba6c..abd14848f 100644 --- a/packages/demobank-ui/src/pages/AccountPage/views.tsx +++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx @@ -22,6 +22,7 @@ import { PaymentOptions } from "../PaymentOptions.js"; import { State } from "./index.js"; import { CopyButton } from "../../components/CopyButton.js"; import { bankUiSettings } from "../../settings.js"; +import { useBusinessAccountDetails } from "../../hooks/circuit.js"; export function InvalidIbanView({ error }: State.InvalidIban) { return ( @@ -77,11 +78,35 @@ function ImportantMessage(): VNode { } -export function ReadyView({ account, balance, balanceIsDebit, limit, payto }: State.Ready): VNode<{}> { - const { i18n } = useTranslationContext(); +export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> { return + ; } +function MaybeBusinessButton({ + account, + onClick, +}: { + account: string; + onClick: () => void; +}): VNode { + const { i18n } = useTranslationContext(); + const result = useBusinessAccountDetails(account); + if (!result.ok) return ; + return ( +
+ +
+ ); +} diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 80a8224d4..4b23686d6 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -39,36 +39,12 @@ const versionText = VERSION const logger = new Logger("BankFrame"); -function MaybeBusinessButton({ - account, - onClick, -}: { - account: string; - onClick: () => void; -}): VNode { - const { i18n } = useTranslationContext(); - const result = useBusinessAccountDetails(account); - if (!result.ok) return ; - return ( - { - e.preventDefault(); - onClick(); - }} - >{i18n.str`Business Profile`} - ); -} - export function BankFrame({ children, - goToBusinessAccount, account, }: { - account: string | undefined, + account?: string, children: ComponentChildren; - goToBusinessAccount?: () => void; }): VNode { const { i18n } = useTranslationContext(); const backend = useBackendContext(); @@ -489,5 +465,9 @@ function AccountBalance({ account }: { account: string }): VNode { const result = useAccountDetails(account); if (!result.ok) return
- return
{result.data.balance.credit_debit_indicator === "debit" ? "-" : ""} {Amounts.currencyOf(result.data.balance.amount)} {Amounts.stringifyValue(result.data.balance.amount)}
+ return
+ {Amounts.currencyOf(result.data.balance.amount)} +  {result.data.balance.credit_debit_indicator === "debit" ? "-" : ""} + {Amounts.stringifyValue(result.data.balance.amount)} +
} diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index a911f347c..40cc147a6 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -31,14 +31,11 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { Loading } from "../components/Loading.js"; -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 { AdminHome } from "./admin/Home.js"; import { LoginForm } from "./LoginForm.js"; import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; -import { error } from "console"; const logger = new Logger("AccountPage"); @@ -56,10 +53,12 @@ export function HomePage({ onRegister, account, onPendingOperationFound, + goToBusinessAccount, }: { account: string, onPendingOperationFound: (id: string) => void; onRegister: () => void; + goToBusinessAccount: () => void; }): VNode { const [settings] = useSettings(); const { i18n } = useTranslationContext(); @@ -72,6 +71,7 @@ export function HomePage({ return ( ); diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index c82c1b28d..5cb09a5d4 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -33,76 +33,79 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); // const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined); - return (
- - Send money to - + return ( +
+ + Send money to + -
- {/* */} - +
+ {tab === "charge-wallet" && ( + { + updateSettings("currentWithdrawalOperationId", id); + }} + onCancel={() => { + setTab(undefined) + }} + /> + )} + {tab === "wire-transfer" && ( + { + notifyInfo(i18n.str`Wire transfer created!`); + }} + onCancel={() => { + setTab(undefined) + }} + /> + )} -
) +
+ ) } diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index cdaf363e3..af6f7caee 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -44,10 +44,12 @@ const logger = new Logger("PaytoWireTransferForm"); export function PaytoWireTransferForm({ focus, + title, onSuccess, onCancel, limit, }: { + title: TranslatedString, focus?: boolean; onSuccess: () => void; onCancel: (() => void) | undefined; @@ -158,7 +160,9 @@ export function PaytoWireTransferForm({ return (
-

Transfer details

+

+ {title} +