diff options
author | Sebastian <sebasjm@gmail.com> | 2023-10-21 20:25:38 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-10-21 20:25:50 -0300 |
commit | 2ac73949e7cb8de44e56f2fecae617efab15671e (patch) | |
tree | 144a97d71bc9fa964675ef0cc764087ceb14e8eb /packages/demobank-ui/src/pages/business | |
parent | 4b98b693d696d90f30f0a6546b0e1f4bc181a5f2 (diff) | |
download | wallet-core-2ac73949e7cb8de44e56f2fecae617efab15671e.tar.xz |
more ui
Diffstat (limited to 'packages/demobank-ui/src/pages/business')
-rw-r--r-- | packages/demobank-ui/src/pages/business/CreateCashout.tsx (renamed from packages/demobank-ui/src/pages/business/Home.tsx) | 346 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx | 237 |
2 files changed, 253 insertions, 330 deletions
diff --git a/packages/demobank-ui/src/pages/business/Home.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx index d7beda01d..4696c899e 100644 --- a/packages/demobank-ui/src/pages/business/Home.tsx +++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -21,15 +21,12 @@ import { } from "@gnu-taler/taler-util"; import { notify, - notifyError, - notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { mutate } from "swr"; -import { Cashouts } from "../../components/Cashouts/index.js"; import { ErrorLoading } from "../../components/ErrorLoading.js"; import { Loading } from "../../components/Loading.js"; import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; @@ -43,114 +40,15 @@ import { } from "../../hooks/circuit.js"; import { TanChannel, - buildRequestErrorMessage, undefinedIfEmpty, - withRuntimeErrorHandling, + withRuntimeErrorHandling } from "../../utils.js"; import { LoginForm } from "../LoginForm.js"; import { InputAmount } from "../PaytoWireTransferForm.js"; -import { ShowAccountDetails } from "../ShowAccountDetails.js"; -import { UpdateAccountPassword } from "../UpdateAccountPassword.js"; +import { assertUnreachable } from "../HomePage.js"; +import { Attention } from "../../components/Attention.js"; interface Props { - account: string, - onClose: () => void; - onRegister: () => void; -} -export function BusinessAccount({ - onClose, - account, - 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 ( - <CreateCashout - account={account} - onCancel={() => { - 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 ( - <ShowCashoutDetails - id={showCashoutDetails} - onCancel={() => { - setShowCashoutDetails(undefined); - }} - /> - ); - } - if (updatePassword) { - return ( - <UpdateAccountPassword - account={account} - onUpdateSuccess={() => { - notifyInfo(i18n.str`Password changed`); - setUpdatePassword(false); - }} - onCancel={() => { - setUpdatePassword(false); - }} - /> - ); - } - return ( - <div> - <ShowAccountDetails - account={account} - onUpdateSuccess={() => { - notifyInfo(i18n.str`Account updated`); - }} - onChangePassword={() => { - setUpdatePassword(true); - }} - onClear={onClose} - /> - <section style={{ marginTop: "2em" }}> - <div class="active"> - <h3>{i18n.str`Latest cashouts`}</h3> - <Cashouts - account={account} - onSelected={(id) => { - setShowCashoutDetails(id); - }} - /> - </div> - <br /> - <div style={{ display: "flex", justifyContent: "space-between" }}> - <div /> - <input - class="pure-button pure-button-primary content" - type="submit" - value={i18n.str`New cashout`} - onClick={async (e) => { - e.preventDefault(); - setNewcashout(true); - }} - /> - </div> - </section> - </div> - ); -} - -interface PropsCashout { account: string; onComplete: (id: string) => void; onCancel: () => void; @@ -167,11 +65,11 @@ type ErrorFrom<T> = { }; -function CreateCashout({ +export function CreateCashout({ account: accountName, onComplete, onCancel, -}: PropsCashout): VNode { +}: Props): VNode { const { i18n } = useTranslationContext(); const resultRatios = useRatiosAndFeeConfig(); const resultAccount = useAccountDetails(accountName); @@ -184,6 +82,17 @@ function CreateCashout({ const { api, config } = useBankCoreApiContext() const [form, setForm] = useState<Partial<FormType>>({ isDebit: true }); + if (!config.have_cashout) { + return <Attention type="warning" title={i18n.str`Unable to create a cashout`} onClose={onCancel}> + <i18n.Translate>The bank configuration does not support cashout operations.</i18n.Translate> + </Attention> + } + if (!config.fiat_currency) { + return <Attention type="warning" title={i18n.str`Unable to create a cashout`} onClose={onCancel}> + <i18n.Translate>The bank configuration support cashout operations but there is no fiat currency.</i18n.Translate> + </Attention> + } + if (!resultAccount || !resultRatios) { return <Loading /> } @@ -207,9 +116,6 @@ function CreateCashout({ default: assertUnreachable(resultRatios.case) } } - if (!config.fiat_currency) { - return <div>cashout operations are not supported</div> - } const ratio = resultRatios.body @@ -514,223 +420,3 @@ function CreateCashout({ </div> ); } - -interface ShowCashoutProps { - id: string; - onCancel: () => void; -} -export function ShowCashoutDetails({ - id, - onCancel, -}: ShowCashoutProps): VNode { - const { i18n } = useTranslationContext(); - const { state } = useBackendState(); - const creds = state.status !== "loggedIn" ? undefined : state - const { api } = useBankCoreApiContext() - const result = useCashoutDetails(id); - const [code, setCode] = useState<string | undefined>(undefined); - - if (!result) { - return <Loading /> - } - if (result instanceof TalerError) { - return <ErrorLoading error={result} /> - } - if (result.type === "fail") { - switch (result.case) { - case "already-aborted": return <div>this cashout is already aborted</div> - default: assertUnreachable(result.case) - } - } - const errors = undefinedIfEmpty({ - code: !code ? i18n.str`required` : undefined, - }); - const isPending = String(result.body.status).toUpperCase() === "PENDING"; - return ( - <div> - <h1>Cashout details {id}</h1> - <form class="pure-form"> - <fieldset> - <label> - <i18n.Translate>Subject</i18n.Translate> - </label> - <input readOnly value={result.body.subject} /> - </fieldset> - <fieldset> - <label> - <i18n.Translate>Created</i18n.Translate> - </label> - <input readOnly value={result.body.creation_time.t_s === "never" ? i18n.str`never` : format(result.body.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")} /> - </fieldset> - <fieldset> - <label> - <i18n.Translate>Confirmed</i18n.Translate> - </label> - <input readOnly value={result.body.confirmation_time === undefined ? "-" : - (result.body.confirmation_time.t_s === "never" ? - i18n.str`never` : - format(result.body.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss")) - } /> - </fieldset> - <fieldset> - <label> - <i18n.Translate>Debited</i18n.Translate> - </label> - <input readOnly value={result.body.amount_debit} /> - </fieldset> - <fieldset> - <label> - <i18n.Translate>Credit</i18n.Translate> - </label> - <input readOnly value={result.body.amount_credit} /> - </fieldset> - <fieldset> - <label> - <i18n.Translate>Status</i18n.Translate> - </label> - <input readOnly value={result.body.status} /> - </fieldset> - <fieldset> - <label> - <i18n.Translate>Destination</i18n.Translate> - </label> - <input readOnly value={result.body.credit_payto_uri} /> - </fieldset> - {isPending ? ( - <fieldset> - <label> - <i18n.Translate>Code</i18n.Translate> - </label> - <input - value={code ?? ""} - onChange={(e) => { - setCode(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errors?.code} - isDirty={code !== undefined} - /> - </fieldset> - ) : undefined} - </form> - <br /> - <div style={{ display: "flex", justifyContent: "space-between" }}> - <button - class="pure-button pure-button-secondary btn-cancel" - onClick={(e) => { - e.preventDefault(); - onCancel(); - }} - > - {i18n.str`Back`} - </button> - {isPending ? ( - <div> - <button - type="submit" - class="pure-button pure-button-primary button-error" - onClick={async (e) => { - e.preventDefault(); - if (!creds) return; - await withRuntimeErrorHandling(i18n, async () => { - const resp = await api.abortCashoutById(creds, id); - if (resp.type === "ok") { - onCancel(); - } else { - switch (resp.case) { - case "not-found": return notify({ - type: "error", - title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "already-confirmed": return notify({ - type: "error", - title: i18n.str`Cashout was already confimed.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - default: { - assertUnreachable(resp) - } - } - } - }) - }} - > - {i18n.str`Abort`} - </button> - - <button - type="submit" - disabled={!code} - class="pure-button pure-button-primary " - onClick={async (e) => { - e.preventDefault(); - if (!creds || !code) return; - await withRuntimeErrorHandling(i18n, async () => { - const resp = await api.confirmCashoutById(creds, id, { - tan: code, - }); - if (resp.type === "ok") { - mutate(() => true)//clean cashout state - } else { - switch (resp.case) { - case "not-found": return notify({ - type: "error", - title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "wrong-tan-or-credential": return notify({ - type: "error", - title: i18n.str`Invalid code or credentials.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "cashout-address-changed": return notify({ - type: "error", - title: i18n.str`The cash-out address between the creation and the confirmation changed.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - default: assertUnreachable(resp) - } - } - }) - }} - > - {i18n.str`Confirm`} - </button> - </div> - ) : ( - <div /> - )} - </div> - </div> - ); -} - -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"); -} diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx new file mode 100644 index 000000000..a8e34e4b9 --- /dev/null +++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx @@ -0,0 +1,237 @@ +/* + 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 { + TalerError, + TranslatedString +} from "@gnu-taler/taler-util"; +import { + notify, + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { mutate } from "swr"; +import { ErrorLoading } from "../../components/ErrorLoading.js"; +import { Loading } from "../../components/Loading.js"; +import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { useBankCoreApiContext } from "../../context/config.js"; +import { useBackendState } from "../../hooks/backend.js"; +import { + useCashoutDetails +} from "../../hooks/circuit.js"; +import { + undefinedIfEmpty, + withRuntimeErrorHandling +} from "../../utils.js"; +import { assertUnreachable } from "../HomePage.js"; + +interface Props { + id: string; + onCancel: () => void; +} +export function ShowCashoutDetails({ + id, + onCancel, +}: Props): VNode { + const { i18n } = useTranslationContext(); + const { state } = useBackendState(); + const creds = state.status !== "loggedIn" ? undefined : state + const { api } = useBankCoreApiContext() + const result = useCashoutDetails(id); + const [code, setCode] = useState<string | undefined>(undefined); + + if (!result) { + return <Loading /> + } + if (result instanceof TalerError) { + return <ErrorLoading error={result} /> + } + if (result.type === "fail") { + switch (result.case) { + case "already-aborted": return <div>this cashout is already aborted</div> + default: assertUnreachable(result.case) + } + } + const errors = undefinedIfEmpty({ + code: !code ? i18n.str`required` : undefined, + }); + const isPending = String(result.body.status).toUpperCase() === "PENDING"; + return ( + <div> + <h1>Cashout details {id}</h1> + <form class="pure-form"> + <fieldset> + <label> + <i18n.Translate>Subject</i18n.Translate> + </label> + <input readOnly value={result.body.subject} /> + </fieldset> + <fieldset> + <label> + <i18n.Translate>Created</i18n.Translate> + </label> + <input readOnly value={result.body.creation_time.t_s === "never" ? i18n.str`never` : format(result.body.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")} /> + </fieldset> + <fieldset> + <label> + <i18n.Translate>Confirmed</i18n.Translate> + </label> + <input readOnly value={result.body.confirmation_time === undefined ? "-" : + (result.body.confirmation_time.t_s === "never" ? + i18n.str`never` : + format(result.body.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss")) + } /> + </fieldset> + <fieldset> + <label> + <i18n.Translate>Debited</i18n.Translate> + </label> + <input readOnly value={result.body.amount_debit} /> + </fieldset> + <fieldset> + <label> + <i18n.Translate>Credit</i18n.Translate> + </label> + <input readOnly value={result.body.amount_credit} /> + </fieldset> + <fieldset> + <label> + <i18n.Translate>Status</i18n.Translate> + </label> + <input readOnly value={result.body.status} /> + </fieldset> + <fieldset> + <label> + <i18n.Translate>Destination</i18n.Translate> + </label> + <input readOnly value={result.body.credit_payto_uri} /> + </fieldset> + {isPending ? ( + <fieldset> + <label> + <i18n.Translate>Code</i18n.Translate> + </label> + <input + value={code ?? ""} + onChange={(e) => { + setCode(e.currentTarget.value); + }} + /> + <ShowInputErrorLabel + message={errors?.code} + isDirty={code !== undefined} + /> + </fieldset> + ) : undefined} + </form> + <br /> + <div style={{ display: "flex", justifyContent: "space-between" }}> + <button + class="pure-button pure-button-secondary btn-cancel" + onClick={(e) => { + e.preventDefault(); + onCancel(); + }} + > + {i18n.str`Back`} + </button> + {isPending ? ( + <div> + <button + type="submit" + class="pure-button pure-button-primary button-error" + onClick={async (e) => { + e.preventDefault(); + if (!creds) return; + await withRuntimeErrorHandling(i18n, async () => { + const resp = await api.abortCashoutById(creds, id); + if (resp.type === "ok") { + onCancel(); + } else { + switch (resp.case) { + case "not-found": return notify({ + type: "error", + title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "already-confirmed": return notify({ + type: "error", + title: i18n.str`Cashout was already confimed.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: { + assertUnreachable(resp) + } + } + } + }) + }} + > + {i18n.str`Abort`} + </button> + + <button + type="submit" + disabled={!code} + class="pure-button pure-button-primary " + onClick={async (e) => { + e.preventDefault(); + if (!creds || !code) return; + await withRuntimeErrorHandling(i18n, async () => { + const resp = await api.confirmCashoutById(creds, id, { + tan: code, + }); + if (resp.type === "ok") { + mutate(() => true)//clean cashout state + } else { + switch (resp.case) { + case "not-found": return notify({ + type: "error", + title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "wrong-tan-or-credential": return notify({ + type: "error", + title: i18n.str`Invalid code or credentials.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "cashout-address-changed": return notify({ + type: "error", + title: i18n.str`The cash-out address between the creation and the confirmation changed.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: assertUnreachable(resp) + } + } + }) + }} + > + {i18n.str`Confirm`} + </button> + </div> + ) : ( + <div /> + )} + </div> + </div> + ); +} |