aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages/business
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-10-21 20:25:38 -0300
committerSebastian <sebasjm@gmail.com>2023-10-21 20:25:50 -0300
commit2ac73949e7cb8de44e56f2fecae617efab15671e (patch)
tree144a97d71bc9fa964675ef0cc764087ceb14e8eb /packages/demobank-ui/src/pages/business
parent4b98b693d696d90f30f0a6546b0e1f4bc181a5f2 (diff)
downloadwallet-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.tsx237
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>
- &nbsp;
- <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>
+ &nbsp;
+ <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>
+ );
+}