From e39d5c488e2e425bd7febf694caadc17d5126401 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 20 Sep 2023 15:18:36 -0300 Subject: more ui --- .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 23 ++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) (limited to 'packages/demobank-ui/src/pages/WithdrawalQRCode.tsx') diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 80fdac3c8..3b983c2d4 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -15,15 +15,16 @@ */ import { + Amounts, HttpStatusCode, Logger, WithdrawUriResult, + parsePaytoUri, } from "@gnu-taler/taler-util"; -import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ErrorType, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { Loading } from "../components/Loading.js"; import { useWithdrawalDetails } from "../hooks/access.js"; -import { notifyInfo } from "../hooks/notification.js"; import { useSettings } from "../hooks/settings.js"; import { handleNotOkResult } from "./HomePage.js"; import { QrCodeSection } from "./QrCodeSection.js"; @@ -127,6 +128,19 @@ export function WithdrawalQRCode({ } + if (!data.selected_reserve_pub) { + return
+ the exchange is selcted but no reserve pub +
+ } + + const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account) + + if (!account) { + return
+ the exchange is selcted but no account +
+ } if (!data.selection_done) { return ( @@ -144,6 +158,11 @@ export function WithdrawalQRCode({ return ( { notifyInfo(i18n.str`Operation canceled`); clearCurrentWithdrawal() -- cgit v1.2.3 From 7d4c5a71aaa6c4e781af124fe821d8be4ed101ed Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 20 Sep 2023 16:10:32 -0300 Subject: more ui --- packages/demobank-ui/src/hooks/backend.ts | 16 +- packages/demobank-ui/src/hooks/settings.ts | 10 ++ packages/demobank-ui/src/pages/BankFrame.tsx | 50 +++++-- packages/demobank-ui/src/pages/HomePage.tsx | 9 +- .../src/pages/WithdrawalConfirmationQuestion.tsx | 165 +++++++++++---------- .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 117 ++++++++++----- 6 files changed, 221 insertions(+), 146 deletions(-) (limited to 'packages/demobank-ui/src/pages/WithdrawalQRCode.tsx') diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index 4b60d1b6c..c05ab33e9 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -85,18 +85,26 @@ export function getInitialBackendBaseURL(): string { typeof localStorage !== "undefined" ? localStorage.getItem("bank-base-url") : undefined; + let result: string; if (!overrideUrl) { //normal path if (!bankUiSettings.backendBaseURL) { console.error( "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", ); - return canonicalizeBaseUrl(window.origin); + result = window.origin } - return canonicalizeBaseUrl(bankUiSettings.backendBaseURL); + result = bankUiSettings.backendBaseURL; + } else { + // testing/development path + result = overrideUrl + } + try { + return canonicalizeBaseUrl(result) + } catch (e) { + //fall back + return canonicalizeBaseUrl(window.origin) } - // testing/development path - return canonicalizeBaseUrl(overrideUrl); } export const defaultState: BackendState = { diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts index 46b31bf2a..43e803726 100644 --- a/packages/demobank-ui/src/hooks/settings.ts +++ b/packages/demobank-ui/src/hooks/settings.ts @@ -15,8 +15,12 @@ */ import { + AmountString, Codec, buildCodecForObject, + codecForAmountString, + codecForBoolean, + codecForNumber, codecForString, codecOptional, } from "@gnu-taler/taler-util"; @@ -24,15 +28,21 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; interface Settings { currentWithdrawalOperationId: string | undefined; + showWithdrawalSuccess: boolean; + maxWithdrawalAmount: number; } export const codecForSettings = (): Codec => buildCodecForObject() .property("currentWithdrawalOperationId", codecOptional(codecForString())) + .property("showWithdrawalSuccess", (codecForBoolean())) + .property("maxWithdrawalAmount", codecForNumber()) .build("Settings"); const defaultSettings: Settings = { currentWithdrawalOperationId: undefined, + showWithdrawalSuccess: true, + maxWithdrawalAmount: 25 }; const DEMOBANK_SETTINGS_KEY = buildStorageKey( diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index d234845a0..e682085ae 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -183,8 +183,28 @@ export function BankFrame({ {/* */} +
  • +
    + + + Show withdrawal confirmation + + + +
    +
  • + + +
  • Sites @@ -343,14 +363,14 @@ function StatusBanner(): VNode { switch (n.message.type) { case "error": return
    -
    -
    - -
    -
    -

    {n.message.title}

    +
    +
    + +
    +
    +

    {n.message.title}

    -

    +

    +
    + {n.message.description && +
    + {n.message.description} +
    + }
    - {n.message.description && -
    - {n.message.description} -
    - } -
    case "info": return
    diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index e00daf278..e82e46eb2 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -95,8 +95,9 @@ export function WithdrawalOperationPage({ }): VNode { //FIXME: libeufin sandbox should return show to create the integration api endpoint //or return withdrawal uri from response + const baseUrl = getInitialBackendBaseURL() const uri = stringifyWithdrawUri({ - bankIntegrationApiBaseUrl: `${getInitialBackendBaseURL()}/integration-api`, + bankIntegrationApiBaseUrl: `${baseUrl}/integration-api`, withdrawalOperationId: operationId, }); const parsedUri = parseWithdrawUri(uri); @@ -155,7 +156,7 @@ export function handleNotOkResult( } case ErrorType.SERVER: { notify({ - type: "error", + type: "error", title: i18n.str`Server returned with error`, description: result.payload.error.description as TranslatedString, debug: JSON.stringify(result.payload), @@ -164,7 +165,7 @@ export function handleNotOkResult( } case ErrorType.UNREADABLE: { notify({ - type:"error", + type: "error", title: i18n.str`Unexpected error.`, description: i18n.str`Response from ${result.info?.url} is unreadable, http status: ${result.status}`, debug: JSON.stringify(result), @@ -173,7 +174,7 @@ export function handleNotOkResult( } case ErrorType.UNEXPECTED: { notify({ - type:"error", + type: "error", title: i18n.str`Unexpected error.`, description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`, debug: JSON.stringify(result), diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index 28f00169d..80e7a78ac 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -20,24 +20,23 @@ import { HttpStatusCode, Logger, PaytoUri, - PaytoUriGeneric, PaytoUriIBAN, PaytoUriTalerBank, TranslatedString, - WithdrawUriResult, + WithdrawUriResult } from "@gnu-taler/taler-util"; import { RequestError, notify, notifyError, + notifyInfo, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useMemo, useState } from "preact/hooks"; +import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { useAccessAnonAPI } from "../hooks/access.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; -import { Amount } from "./PaytoWireTransferForm.js"; const logger = new Logger("WithdrawalConfirmationQuestion"); @@ -71,6 +70,7 @@ export function WithdrawalConfirmationQuestion({ const { confirmWithdrawal, abortWithdrawal } = useAccessAnonAPI(); const [captchaAnswer, setCaptchaAnswer] = useState(); const answer = parseInt(captchaAnswer ?? "", 10); + const [busy, setBusy] = useState>() const errors = undefinedIfEmpty({ answer: !captchaAnswer ? i18n.str`Answer the question before continue` @@ -79,13 +79,15 @@ export function WithdrawalConfirmationQuestion({ : answer !== captchaNumbers.a + captchaNumbers.b ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.` : undefined, - }); + }) ?? busy; async function doTransfer() { try { + setBusy({}) await confirmWithdrawal( withdrawUri.withdrawalOperationId, ); + notifyInfo(i18n.str`Wire transfer completed!`) } catch (error) { if (error instanceof RequestError) { notify( @@ -107,10 +109,12 @@ export function WithdrawalConfirmationQuestion({ ) } } + setBusy(undefined) } async function doCancel() { try { + setBusy({}) await abortWithdrawal(withdrawUri.withdrawalOperationId); onAborted(); } catch (error) { @@ -132,6 +136,7 @@ export function WithdrawalConfirmationQuestion({ ) } } + setBusy(undefined) } return ( @@ -142,68 +147,6 @@ export function WithdrawalConfirmationQuestion({ Confirm the withdrawal operation
    -
    -
    -
    -

    Wire transfer details

    -
    -
    -
    - {((): VNode => { - switch (details.account.targetType) { - case "iban": { - const p = details.account as PaytoUriIBAN - const name = p.params["receiver-name"] - return -
    -
    Exchange account
    -
    {p.iban}
    -
    - {name && -
    -
    Exchange name
    -
    {p.params["receiver-name"]}
    -
    - } -
    - } - case "x-taler-bank": { - const p = details.account as PaytoUriTalerBank - const name = p.params["receiver-name"] - return -
    -
    Exchange account
    -
    {p.account}
    -
    - {name && -
    -
    Exchange name
    -
    {p.params["receiver-name"]}
    -
    - } -
    - } - default: - return
    -
    Exchange account
    -
    {details.account.targetPath}
    -
    - - } - })()} -
    -
    Withdrawal identification
    -
    {details.reserve}
    -
    -
    -
    Amount
    -
    {Amounts.stringifyValue(details.amount)}
    -
    -
    -
    -
    - -
    - +
    @@ -323,6 +262,68 @@ export function WithdrawalConfirmationQuestion({
    +
    +
    +
    +

    Wire transfer details

    +
    +
    +
    + {((): VNode => { + switch (details.account.targetType) { + case "iban": { + const p = details.account as PaytoUriIBAN + const name = p.params["receiver-name"] + return +
    +
    Exchange account
    +
    {p.iban}
    +
    + {name && +
    +
    Exchange name
    +
    {p.params["receiver-name"]}
    +
    + } +
    + } + case "x-taler-bank": { + const p = details.account as PaytoUriTalerBank + const name = p.params["receiver-name"] + return +
    +
    Exchange account
    +
    {p.account}
    +
    + {name && +
    +
    Exchange name
    +
    {p.params["receiver-name"]}
    +
    + } +
    + } + default: + return
    +
    Exchange account
    +
    {details.account.targetPath}
    +
    + + } + })()} +
    +
    Withdrawal identification
    +
    {details.reserve}
    +
    +
    +
    Amount
    +
    {Amounts.stringifyValue(details.amount)}
    +
    +
    +
    +
    + +
    diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 3b983c2d4..b48e3b1dc 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -62,9 +62,10 @@ export function WithdrawalQRCode({ result.type === ErrorType.CLIENT && result.status === HttpStatusCode.NotFound ) { + clearCurrentWithdrawal() return
    operation not found
    ; } - onLoadNotOk(); + // onLoadNotOk(); return handleNotOkResult(i18n)(result); } const { data } = result; @@ -85,12 +86,12 @@ export function WithdrawalQRCode({

    { e.preventDefault(); clearCurrentWithdrawal() onContinue() - }}> + }}> {i18n.str`Continue`} @@ -99,49 +100,69 @@ export function WithdrawalQRCode({ } if (data.confirmation_done) { - return
    -

    {i18n.str`Operation completed`}

    - -
    -

    - - The wire transfer to the GNU Taler Exchange bank's account is completed, now the - exchange will send the requested amount into your GNU Taler wallet. - -

    -

    - - You can close this page now or continue to the account page. - -

    -
    + if (!settings.showWithdrawalSuccess) { + clearCurrentWithdrawal() + onContinue() + } + return
    +
    +
    + +
    +
    + +
    +

    + + The wire transfer to the Taler exchange bank's account is completed, now the + exchange will send the requested amount into your GNU Taler wallet. + +

    +
    +
    +

    + + You can close this page now or continue to the account page. + +

    +
    +
    +
    +
    +
    + + + Do not show this again + + +
    -
    -
    - } - if (!data.selected_reserve_pub) { - return
    - the exchange is selcted but no reserve pub +
    +
    + +
    - } - const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account) - if (!account) { - return
    - the exchange is selcted but no account -
    } - if (!data.selection_done) { return ( ); } + if (!data.selected_reserve_pub) { + return
    + the exchange is selcted but no reserve pub +
    + } + + const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account) + + if (!account) { + return
    + the exchange is selcted but no account +
    + } + return ( ); } \ No newline at end of file -- cgit v1.2.3 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/components/Routing.tsx | 13 +- packages/demobank-ui/src/pages/AdminPage.tsx | 1042 -------------------- packages/demobank-ui/src/pages/BusinessAccount.tsx | 758 -------------- packages/demobank-ui/src/pages/HomePage.tsx | 19 +- .../demobank-ui/src/pages/ShowAccountDetails.tsx | 143 +++ .../src/pages/UpdateAccountPassword.tsx | 131 +++ .../src/pages/WithdrawalConfirmationQuestion.tsx | 3 +- .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 4 - packages/demobank-ui/src/pages/admin/Account.tsx | 56 ++ .../demobank-ui/src/pages/admin/AccountForm.tsx | 219 ++++ .../demobank-ui/src/pages/admin/AccountList.tsx | 120 +++ .../src/pages/admin/CreateNewAccount.tsx | 107 ++ packages/demobank-ui/src/pages/admin/Home.tsx | 162 +++ .../demobank-ui/src/pages/admin/RemoveAccount.tsx | 112 +++ packages/demobank-ui/src/pages/business/Home.tsx | 757 ++++++++++++++ 15 files changed, 1823 insertions(+), 1823 deletions(-) delete mode 100644 packages/demobank-ui/src/pages/AdminPage.tsx delete mode 100644 packages/demobank-ui/src/pages/BusinessAccount.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 create mode 100644 packages/demobank-ui/src/pages/business/Home.tsx (limited to 'packages/demobank-ui/src/pages/WithdrawalQRCode.tsx') 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/BusinessAccount.tsx b/packages/demobank-ui/src/pages/BusinessAccount.tsx deleted file mode 100644 index ec71ceca6..000000000 --- a/packages/demobank-ui/src/pages/BusinessAccount.tsx +++ /dev/null @@ -1,758 +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 { - 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 { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js"; -import { handleNotOkResult } from "./HomePage.js"; -import { LoginForm } from "./LoginForm.js"; -import { Amount } from "./PaytoWireTransferForm.js"; - -interface Props { - onClose: () => void; - onRegister: () => void; - onLoadNotOk: () => void; -} -export function BusinessAccount({ - onClose, - 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); - }} - 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"); -} 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/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 56a6f47c7daae088c2017c0d9781ddcf7cee175b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 21 Sep 2023 15:44:17 -0300 Subject: more ui --- packages/demobank-ui/src/components/Routing.tsx | 12 +- packages/demobank-ui/src/hooks/settings.ts | 8 +- .../demobank-ui/src/pages/AccountPage/views.tsx | 50 ++++++ packages/demobank-ui/src/pages/BankFrame.tsx | 133 ++++++++------- packages/demobank-ui/src/pages/HomePage.tsx | 20 +-- packages/demobank-ui/src/pages/PaymentOptions.tsx | 137 +++++++-------- packages/demobank-ui/src/pages/QrCodeSection.tsx | 106 ++++++++++++ .../demobank-ui/src/pages/WalletWithdrawForm.tsx | 183 +++++++++++---------- .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 159 +++++++++++++++--- .../demobank-ui/src/pages/admin/RemoveAccount.tsx | 2 +- 10 files changed, 553 insertions(+), 257 deletions(-) (limited to 'packages/demobank-ui/src/pages/WithdrawalQRCode.tsx') diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx index b8e39948b..e1fd93737 100644 --- a/packages/demobank-ui/src/components/Routing.tsx +++ b/packages/demobank-ui/src/components/Routing.tsx @@ -76,9 +76,9 @@ export function Routing(): VNode { onContinue={() => { route("/account"); }} - onLoadNotOk={() => { - route("/account"); - }} + // onLoadNotOk={() => { + // route("/account"); + // }} /> )} /> @@ -108,9 +108,9 @@ export function Routing(): VNode { } else { return { - route(`/operation/${wopid}`); - }} + // onPendingOperationFound={(wopid) => { + // route(`/operation/${wopid}`); + // }} goToBusinessAccount={() => { route("/business"); }} diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts index 43e803726..c2fd93a0c 100644 --- a/packages/demobank-ui/src/hooks/settings.ts +++ b/packages/demobank-ui/src/hooks/settings.ts @@ -29,20 +29,26 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; interface Settings { currentWithdrawalOperationId: string | undefined; showWithdrawalSuccess: boolean; + showDemoDescription: boolean; maxWithdrawalAmount: number; + fastWithdrawal: boolean; } export const codecForSettings = (): Codec => buildCodecForObject() .property("currentWithdrawalOperationId", codecOptional(codecForString())) .property("showWithdrawalSuccess", (codecForBoolean())) + .property("showDemoDescription", (codecForBoolean())) + .property("fastWithdrawal", (codecForBoolean())) .property("maxWithdrawalAmount", codecForNumber()) .build("Settings"); const defaultSettings: Settings = { currentWithdrawalOperationId: undefined, showWithdrawalSuccess: true, - maxWithdrawalAmount: 25 + showDemoDescription: true, + maxWithdrawalAmount: 25, + fastWithdrawal: false, }; const DEMOBANK_SETTINGS_KEY = buildStorageKey( diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx index abd14848f..0187989af 100644 --- a/packages/demobank-ui/src/pages/AccountPage/views.tsx +++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx @@ -23,6 +23,7 @@ import { State } from "./index.js"; import { CopyButton } from "../../components/CopyButton.js"; import { bankUiSettings } from "../../settings.js"; import { useBusinessAccountDetails } from "../../hooks/circuit.js"; +import { useSettings } from "../../hooks/settings.js"; export function InvalidIbanView({ error }: State.InvalidIban) { return ( @@ -78,9 +79,58 @@ function ImportantMessage(): VNode { } +function ShowDemoInfo():VNode { + const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings(); + if (!settings.showDemoDescription) return + return
    +
    +
    + +
    +
    +

    + This is a demo bank! +

    +
    +

    + + This part of the demo shows how a bank that supports Taler + directly would work. In addition to using your own bank + account, you can also see the transaction history of some{" "} + Public Accounts. + +

    +

    + +

    + +
    +
    +
    +
    +} + export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> { + const { i18n } = useTranslationContext(); + return + + + ; diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 4b23686d6..d1c94135b 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -27,7 +27,6 @@ import { CopyButton, CopyIcon } from "../components/CopyButton.js"; import logo from "../assets/logo-2021.svg"; import { useAccountDetails } from "../hooks/access.js"; -const IS_PUBLIC_ACCOUNT_ENABLED = false; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -142,24 +141,40 @@ export function BankFrame({
    } diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index 40cc147a6..2acfc9b57 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -52,21 +52,21 @@ const logger = new Logger("AccountPage"); export function HomePage({ onRegister, account, - onPendingOperationFound, + // onPendingOperationFound, goToBusinessAccount, }: { account: string, - onPendingOperationFound: (id: string) => void; + // onPendingOperationFound: (id: string) => void; onRegister: () => void; goToBusinessAccount: () => void; }): VNode { const [settings] = useSettings(); const { i18n } = useTranslationContext(); - if (settings.currentWithdrawalOperationId) { - onPendingOperationFound(settings.currentWithdrawalOperationId); - return ; - } + // if (settings.currentWithdrawalOperationId) { + // onPendingOperationFound(settings.currentWithdrawalOperationId); + // return ; + // } return ( void; onContinue: () => void; }): VNode { //FIXME: libeufin sandbox should return show to create the integration api endpoint @@ -95,6 +93,7 @@ export function WithdrawalOperationPage({ }); const parsedUri = parseWithdrawUri(uri); const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings(); if (!parsedUri) { notifyError( @@ -107,8 +106,9 @@ export function WithdrawalOperationPage({ return ( { + updateSettings("currentWithdrawalOperationId", undefined) + }} /> ); } diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 1728074a3..573f8c769 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -34,80 +34,83 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { // const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined); 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/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx index 7c1b3bdc5..416c714e2 100644 --- a/packages/demobank-ui/src/pages/QrCodeSection.tsx +++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx @@ -135,3 +135,109 @@ export function QrCodeSection({
    ); } + + +export function QrCodeSectionSimpler({ + withdrawUri, + onAborted, +}: { + withdrawUri: WithdrawUriResult; + onAborted: () => void; +}): VNode { + const { i18n } = useTranslationContext(); + useEffect(() => { + //Taler Wallet WebExtension is listening to headers response and tab updates. + //In the SPA there is no header response with the Taler URI so + //this hack manually triggers the tab update after the QR is in the DOM. + // WebExtension will be using + // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated + document.title = `${document.title} ${withdrawUri.withdrawalOperationId}`; + }, []); + const talerWithdrawUri = stringifyWithdrawUri(withdrawUri); + + const { abortWithdrawal } = useAccessAnonAPI(); + + async function doAbort() { + try { + await abortWithdrawal(withdrawUri.withdrawalOperationId); + onAborted(); + } catch (error) { + if (error instanceof RequestError) { + notify( + buildRequestErrorMessage(i18n, error.cause, { + onClientError: (status) => + status === HttpStatusCode.Conflict + ? i18n.str`The reserve operation has been confirmed previously and can't be aborted` + : undefined, + }), + ); + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } + } + } + + return ( + +
    +
    +

    + If you have a Taler wallet installed in this device +

    + +
    +

    + You will see the details of the operation in your wallet including the fees (if applies). + If you still one you can install it from here. +

    +
    +
    +
    + +
    +
    +
    + +
    +
    +

    + Or if you have the wallet in another device +

    +
    + Scan the QR below to start the withdrawal +
    +
    + +
    +
    +
    + +
    +
    + + + ); +} + diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index 8c41f7576..08f706919 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -20,6 +20,7 @@ import { HttpStatusCode, Logger, TranslatedString, + WithdrawUriResult, parseWithdrawUri, } from "@gnu-taler/taler-util"; import { @@ -34,6 +35,9 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { useAccessAPI } from "../hooks/access.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { Amount } from "./PaytoWireTransferForm.js"; +import { useSettings } from "../hooks/settings.js"; +import { WithdrawalOperationState } from "./WithdrawalQRCode.js"; +import { Loading } from "../components/Loading.js"; const logger = new Logger("WalletWithdrawForm"); const RefAmount = forwardRef(Amount); @@ -51,8 +55,9 @@ export function WalletWithdrawForm({ }): VNode { const { i18n } = useTranslationContext(); const { createWithdrawal } = useAccessAPI(); + const [settings, updateSettings] = useSettings() - const [amountStr, setAmountStr] = useState("5.00"); + const [amountStr, setAmountStr] = useState(`${settings.maxWithdrawalAmount}`); const ref = useRef(null); useEffect(() => { if (focus) ref.current?.focus(); @@ -78,7 +83,6 @@ export function WalletWithdrawForm({ async function doStart() { if (!parsedAmount) return; try { - console.log("ASDASD") const result = await createWithdrawal({ amount: Amounts.stringify(parsedAmount), }); @@ -109,7 +113,6 @@ export function WalletWithdrawForm({ ) } } - } return (
    @@ -122,91 +125,103 @@ export function WalletWithdrawForm({ After using your wallet you will be redirected here to confirm or cancel the operation.

    -
    { - e.preventDefault() - }} - > -
    -
    -
    - - { - setAmountStr(v); - }} - error={errors?.amount} - ref={ref} - /> -
    -
    - - - - - - -
    + error={errors?.amount} + ref={ref} + /> +
    +
    + + + + + + +
    +
    +
    +
    + +
    -
    -
    - - -
    - + + : settings.currentWithdrawalOperationId === undefined ? + : + { + onCancel() + }} + /> + }
    ); } diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 2a3a1ec2c..9976babdb 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -18,24 +18,29 @@ import { Amounts, HttpStatusCode, Logger, + TranslatedString, WithdrawUriResult, parsePaytoUri, + parseWithdrawUri, + stringifyWithdrawUri, } from "@gnu-taler/taler-util"; -import { ErrorType, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ErrorType, RequestError, notify, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { Loading } from "../components/Loading.js"; -import { useWithdrawalDetails } from "../hooks/access.js"; +import { useAccessAPI, useWithdrawalDetails } from "../hooks/access.js"; import { useSettings } from "../hooks/settings.js"; import { handleNotOkResult } from "./HomePage.js"; -import { QrCodeSection } from "./QrCodeSection.js"; +import { QrCodeSection, QrCodeSectionSimpler } from "./QrCodeSection.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; +import { useEffect, useState } from "preact/hooks"; +import { buildRequestErrorMessage } from "../utils.js"; +import { getInitialBackendBaseURL } from "../hooks/backend.js"; const logger = new Logger("WithdrawalQRCode"); interface Props { withdrawUri: WithdrawUriResult; - onContinue: () => void; - onLoadNotOk: () => void; + onClose: () => void; } /** * Offer the QR code (and a clickable taler://-link) to @@ -44,14 +49,9 @@ interface Props { */ export function WithdrawalQRCode({ withdrawUri, - onContinue, - onLoadNotOk, + onClose, }: Props): VNode { const [settings, updateSettings] = useSettings(); - function clearCurrentWithdrawal(): void { - updateSettings("currentWithdrawalOperationId", undefined); - onContinue(); - } const { i18n } = useTranslationContext(); const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId); if (!result.ok) { @@ -62,7 +62,7 @@ export function WithdrawalQRCode({ result.type === ErrorType.CLIENT && result.status === HttpStatusCode.NotFound ) { - clearCurrentWithdrawal() + onClose() return
    operation not found
    ; } // onLoadNotOk(); @@ -89,8 +89,7 @@ export function WithdrawalQRCode({ style={{ float: "right" }} onClick={async (e) => { e.preventDefault(); - clearCurrentWithdrawal() - onContinue() + onClose() }}> {i18n.str`Continue`} @@ -149,8 +148,7 @@ export function WithdrawalQRCode({ class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" onClick={async (e) => { e.preventDefault(); - clearCurrentWithdrawal() - onContinue() + onClose() }}> Continue @@ -165,8 +163,7 @@ export function WithdrawalQRCode({ withdrawUri={withdrawUri} onAborted={() => { notifyInfo(i18n.str`Operation canceled`); - clearCurrentWithdrawal() - onContinue() + onClose() }} /> ); @@ -196,9 +193,131 @@ export function WithdrawalQRCode({ }} onAborted={() => { notifyInfo(i18n.str`Operation canceled`); - clearCurrentWithdrawal() - onContinue() + onClose() }} /> ); +} + + +export function WithdrawalOperationState({ + currency, + currentOperation, + onClose, +}: {currency:string, currentOperation: string, onClose: () => void}): VNode { + const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings() + const { createWithdrawal } = useAccessAPI(); + + const amount = settings.maxWithdrawalAmount + async function doSilentStart() { + //FIXME: if amount is not enough use balance + const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`) + + try { + const result = await createWithdrawal({ + amount: Amounts.stringify(parsedAmount), + }); + const uri = parseWithdrawUri(result.data.taler_withdraw_uri); + if (!uri) { + return notifyError( + i18n.str`Server responded with an invalid withdraw URI`, + i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`); + } else { + updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId) + } + } catch (error) { + if (error instanceof RequestError) { + notify( + buildRequestErrorMessage(i18n, error.cause, { + onClientError: (status) => + status === HttpStatusCode.Forbidden + ? i18n.str`The operation was rejected due to insufficient funds` + : undefined, + }), + ); + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } + } + } + + useEffect(() => { + doSilentStart() + }, [settings.fastWithdrawal, amount]) + + const result = useWithdrawalDetails(currentOperation); + if (!result.ok) { + if (result.loading) { + return ; + } + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) { + onClose() + return
    operation not found
    ; + } + // onLoadNotOk(); + return handleNotOkResult(i18n)(result); + } + const { data } = result; + + const baseUrl = getInitialBackendBaseURL() + const uri = stringifyWithdrawUri({ + bankIntegrationApiBaseUrl: `${baseUrl}/integration-api`, + withdrawalOperationId: currentOperation, + }); + const parsedUri = parseWithdrawUri(uri); + + if (data.aborted) { + return
    + the operation was aborted, you can create another one +
    + } + + if (data.confirmation_done) { + return
    + the wire transfer is made, you coin should arrive shortly +
    + } + if (!parsedUri) { + return
    + the operation is not valid, create another one +
    + } + if (!data.selection_done) { + return ( + { + notifyInfo(i18n.str`Operation canceled`); + onClose() + }} + /> + ); + } + + if (!data.selected_reserve_pub) { + return
    + the exchange is selcted but no reserve pub +
    + } + + const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account) + + if (!account) { + return
    + the exchange is selected but no account +
    + } + + return
    + the operation is wating for the question to be answered +
    ; } \ 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 index 050f1fb8a..1e5370afc 100644 --- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -61,7 +61,7 @@ export function RemoveAccount({

    - The account can be delete while still holding some balance. First make sure that the owner make a complete cashout. + The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.

    -- cgit v1.2.3 From a59df74fb2b4374fd58f68fd4abaffe623cd54d6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 22 Sep 2023 15:29:19 -0300 Subject: more ui --- packages/demobank-ui/src/components/Routing.tsx | 27 +- packages/demobank-ui/src/hooks/settings.ts | 3 + .../demobank-ui/src/pages/AccountPage/index.ts | 2 + .../demobank-ui/src/pages/AccountPage/state.ts | 3 +- .../demobank-ui/src/pages/AccountPage/views.tsx | 4 +- packages/demobank-ui/src/pages/BankFrame.tsx | 15 + packages/demobank-ui/src/pages/HomePage.tsx | 17 +- .../demobank-ui/src/pages/OperationState/index.ts | 10 +- .../demobank-ui/src/pages/OperationState/state.ts | 109 ++++++- .../demobank-ui/src/pages/OperationState/views.tsx | 363 ++++++++++++++++++++- packages/demobank-ui/src/pages/PaymentOptions.tsx | 17 +- packages/demobank-ui/src/pages/QrCodeSection.tsx | 104 ------ .../demobank-ui/src/pages/WalletWithdrawForm.tsx | 273 ++++++++++------ .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 163 +-------- packages/demobank-ui/src/pages/business/Home.tsx | 1 - 15 files changed, 693 insertions(+), 418 deletions(-) (limited to 'packages/demobank-ui/src/pages/WithdrawalQRCode.tsx') diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx index e1fd93737..90d2d4c48 100644 --- a/packages/demobank-ui/src/components/Routing.tsx +++ b/packages/demobank-ui/src/components/Routing.tsx @@ -45,6 +45,20 @@ export function Routing(): VNode { /> )} /> + ( + { + route("/account"); + }} + // onLoadNotOk={() => { + // route("/account"); + // }} + /> + )} + /> ( @@ -64,10 +78,6 @@ export function Routing(): VNode { return ( - ( @@ -76,9 +86,6 @@ export function Routing(): VNode { onContinue={() => { route("/account"); }} - // onLoadNotOk={() => { - // route("/account"); - // }} /> )} /> @@ -108,9 +115,9 @@ export function Routing(): VNode { } else { return { - // route(`/operation/${wopid}`); - // }} + goToConfirmOperation={(wopid) => { + route(`/operation/${wopid}`); + }} goToBusinessAccount={() => { route("/business"); }} diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts index c2fd93a0c..5f004c6d4 100644 --- a/packages/demobank-ui/src/hooks/settings.ts +++ b/packages/demobank-ui/src/hooks/settings.ts @@ -30,6 +30,7 @@ interface Settings { currentWithdrawalOperationId: string | undefined; showWithdrawalSuccess: boolean; showDemoDescription: boolean; + showInstallWallet: boolean; maxWithdrawalAmount: number; fastWithdrawal: boolean; } @@ -39,6 +40,7 @@ export const codecForSettings = (): Codec => .property("currentWithdrawalOperationId", codecOptional(codecForString())) .property("showWithdrawalSuccess", (codecForBoolean())) .property("showDemoDescription", (codecForBoolean())) + .property("showInstallWallet", (codecForBoolean())) .property("fastWithdrawal", (codecForBoolean())) .property("maxWithdrawalAmount", codecForNumber()) .build("Settings"); @@ -47,6 +49,7 @@ const defaultSettings: Settings = { currentWithdrawalOperationId: undefined, showWithdrawalSuccess: true, showDemoDescription: true, + showInstallWallet: true, maxWithdrawalAmount: 25, fastWithdrawal: false, }; diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts index 128a6d30f..81eeb4a03 100644 --- a/packages/demobank-ui/src/pages/AccountPage/index.ts +++ b/packages/demobank-ui/src/pages/AccountPage/index.ts @@ -29,6 +29,7 @@ export interface Props { error: HttpResponsePaginated, ) => VNode; goToBusinessAccount: () => void; + goToConfirmOperation: (id:string) => void; } export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound; @@ -54,6 +55,7 @@ export namespace State { account: string, limit: AmountJson, goToBusinessAccount: () => void; + goToConfirmOperation: (id:string) => 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 a57e19901..1a1475c0d 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, goToBusinessAccount }: Props): State { +export function useComponentState({ account, goToBusinessAccount, goToConfirmOperation }: Props): State { const result = useAccountDetails(account); const backend = useBackendContext(); const { i18n } = useTranslationContext(); @@ -75,6 +75,7 @@ export function useComponentState({ account, goToBusinessAccount }: Props): Stat return { status: "ready", goToBusinessAccount, + goToConfirmOperation, error: undefined, account, limit, diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx index 0187989af..23a815bd8 100644 --- a/packages/demobank-ui/src/pages/AccountPage/views.tsx +++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx @@ -123,7 +123,7 @@ function ShowDemoInfo():VNode { } -export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> { +export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> { const { i18n } = useTranslationContext(); return @@ -131,7 +131,7 @@ export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): - + ; } diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index d1c94135b..5bfaa63ec 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -206,6 +206,21 @@ export function BankFrame({
  • +
  • +
    + + + Show install wallet first + + + +
    +
  • diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index 2acfc9b57..8d5e1f3b9 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -36,6 +36,7 @@ import { useSettings } from "../hooks/settings.js"; import { AccountPage } from "./AccountPage/index.js"; import { LoginForm } from "./LoginForm.js"; import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; +import { route } from "preact-router"; const logger = new Logger("AccountPage"); @@ -52,25 +53,20 @@ const logger = new Logger("AccountPage"); export function HomePage({ onRegister, account, - // onPendingOperationFound, + goToConfirmOperation, goToBusinessAccount, }: { account: string, - // onPendingOperationFound: (id: string) => void; onRegister: () => void; goToBusinessAccount: () => void; + goToConfirmOperation: (id:string) => void; }): VNode { - const [settings] = useSettings(); const { i18n } = useTranslationContext(); - // if (settings.currentWithdrawalOperationId) { - // onPendingOperationFound(settings.currentWithdrawalOperationId); - // return ; - // } - return ( @@ -102,12 +98,13 @@ export function WithdrawalOperationPage({ ); return ; } - + return ( { updateSettings("currentWithdrawalOperationId", undefined) + onContinue() }} /> ); @@ -178,7 +175,7 @@ export function handleNotOkResult( assertUnreachable(result); } } - + route("/") return
    error
    ; } return
    ; diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts index 254fcba5f..32302f272 100644 --- a/packages/demobank-ui/src/pages/OperationState/index.ts +++ b/packages/demobank-ui/src/pages/OperationState/index.ts @@ -26,6 +26,7 @@ import { ErrorLoading } from "../../components/ErrorLoading.js"; export interface Props { currency: string; onClose: () => void; + goToConfirmOperation: (id: string) => void; } export type State = State.Loading | @@ -57,26 +58,33 @@ export namespace State { error: undefined; uri: WithdrawUriResult, onClose: () => void; + onAbort: () => void; } export interface InvalidPayto { status: "invalid-payto", error: undefined; payto: string | null; + onClose: () => void; } export interface InvalidWithdrawal { status: "invalid-withdrawal", error: undefined; + onClose: () => void; uri: string, } export interface InvalidReserve { status: "invalid-reserve", error: undefined; + onClose: () => void; reserve: string | null; } export interface NeedConfirmation { status: "need-confirmation", + onAbort: () => void; + onConfirm: () => void; error: undefined; + busy: boolean, } export interface Aborted { status: "aborted", @@ -111,7 +119,7 @@ const viewMapping: utils.StateViewMap = { ready: ReadyView, }; -export const AccountPage = utils.compose( +export const OperationState = utils.compose( (p: Props) => useComponentState(p), viewMapping, ); diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts index 6fb7bb28f..ae03ed529 100644 --- a/packages/demobank-ui/src/pages/OperationState/state.ts +++ b/packages/demobank-ui/src/pages/OperationState/state.ts @@ -15,21 +15,24 @@ */ import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; -import { ErrorType, RequestError, notify, notifyError, useTranslationContext, utils } from "@gnu-taler/web-util/browser"; +import { ErrorType, RequestError, notify, notifyError, notifyInfo, useTranslationContext, utils } from "@gnu-taler/web-util/browser"; import { useBackendContext } from "../../context/backend.js"; -import { useAccessAPI, useAccountDetails, useWithdrawalDetails } from "../../hooks/access.js"; +import { useAccessAPI, useAccessAnonAPI, useAccountDetails, useWithdrawalDetails } from "../../hooks/access.js"; import { Props, State } from "./index.js"; import { useSettings } from "../../hooks/settings.js"; -import { buildRequestErrorMessage } from "../../utils.js"; -import { useEffect } from "preact/hooks"; +import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js"; +import { useEffect, useMemo, useState } from "preact/hooks"; import { getInitialBackendBaseURL } from "../../hooks/backend.js"; -export function useComponentState({ currency, onClose }: Props): utils.RecursiveState { +export function useComponentState({ currency, onClose,goToConfirmOperation }: Props): utils.RecursiveState { const { i18n } = useTranslationContext(); const [settings, updateSettings] = useSettings() const { createWithdrawal } = useAccessAPI(); + const { abortWithdrawal, confirmWithdrawal } = useAccessAnonAPI(); + const [busy, setBusy] = useState>() const amount = settings.maxWithdrawalAmount + async function doSilentStart() { //FIXME: if amount is not enough use balance const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`) @@ -67,12 +70,14 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive } } + const withdrawalOperationId = settings.currentWithdrawalOperationId useEffect(() => { - doSilentStart() + if (withdrawalOperationId === undefined) { + doSilentStart() + } }, [settings.fastWithdrawal, amount]) const baseUrl = getInitialBackendBaseURL() - const withdrawalOperationId = settings.currentWithdrawalOperationId if (!withdrawalOperationId) { return { @@ -81,6 +86,63 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive } } + const wid = withdrawalOperationId + + async function doAbort() { + try { + setBusy({}) + await abortWithdrawal(wid); + onClose(); + } catch (error) { + if (error instanceof RequestError) { + notify( + buildRequestErrorMessage(i18n, error.cause, { + onClientError: (status) => + status === HttpStatusCode.Conflict + ? i18n.str`The reserve operation has been confirmed previously and can't be aborted` + : undefined, + }), + ); + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } + } + setBusy(undefined) + } + + async function doConfirm() { + try { + setBusy({}) + await confirmWithdrawal(wid); + notifyInfo(i18n.str`Wire transfer completed!`) + } catch (error) { + if (error instanceof RequestError) { + notify( + buildRequestErrorMessage(i18n, error.cause, { + onClientError: (status) => + status === HttpStatusCode.Conflict + ? i18n.str`The withdrawal has been aborted previously and can't be confirmed` + : status === HttpStatusCode.UnprocessableEntity + ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before` + : undefined, + }), + ); + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } + } + setBusy(undefined) + } const bankIntegrationApiBaseUrl = `${baseUrl}/integration-api` const uri = stringifyWithdrawUri({ bankIntegrationApiBaseUrl, @@ -92,11 +154,13 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive status: "invalid-withdrawal", error: undefined, uri, + onClose, } } return (): utils.RecursiveState => { const result = useWithdrawalDetails(withdrawalOperationId); + if (!result.ok) { if (result.loading) { return { @@ -119,10 +183,17 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive } if (data.confirmation_done) { + if (!settings.showWithdrawalSuccess) { + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + } return { status: "confirmed", error: undefined, - onClose, + onClose: async () => { + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + }, } } @@ -131,7 +202,12 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive status: "ready", error: undefined, uri: parsedUri, - onClose + onClose: async () => { + await doAbort() + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + }, + onAbort: doAbort, } } @@ -139,7 +215,8 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive return { status: "invalid-reserve", error: undefined, - reserve: data.selected_reserve_pub + reserve: data.selected_reserve_pub, + onClose, } } @@ -149,13 +226,23 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive return { status: "invalid-payto", error: undefined, - payto: data.selected_exchange_account + payto: data.selected_exchange_account, + onClose, } } + + // goToConfirmOperation(withdrawalOperationId) return { status: "need-confirmation", error: undefined, + onAbort: async () => { + await doAbort() + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + }, + busy: !!busy, + onConfirm: doConfirm } } diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx index db25eaf61..17f1d8457 100644 --- a/packages/demobank-ui/src/pages/OperationState/views.tsx +++ b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see */ -import { Amounts, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, stringifyPaytoUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { Transactions } from "../../components/Transactions/index.js"; @@ -24,42 +24,375 @@ import { CopyButton } from "../../components/CopyButton.js"; import { bankUiSettings } from "../../settings.js"; import { useBusinessAccountDetails } from "../../hooks/circuit.js"; import { useSettings } from "../../hooks/settings.js"; +import { useEffect, useMemo, useState } from "preact/hooks"; +import { undefinedIfEmpty } from "../../utils.js"; +import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { QR } from "../../components/QR.js"; -export function InvalidPaytoView({ error }: State.InvalidPayto) { +export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) { return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    +
    Payto from server is not valid "{payto}"
    ); } -export function InvalidWithdrawalView({ error }: State.InvalidWithdrawal) { +export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) { return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    +
    Withdrawal uri from server is not valid "{uri}"
    ); } -export function InvalidReserveView({ error }: State.InvalidReserve) { +export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) { return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    +
    Reserve from server is not valid "{reserve}"
    ); } -export function NeedConfirmationView({ error }: State.NeedConfirmation) { +export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: State.NeedConfirmation) { + const { i18n } = useTranslationContext() + + const captchaNumbers = useMemo(() => { + return { + a: Math.floor(Math.random() * 10), + b: Math.floor(Math.random() * 10), + }; + }, []); + const [captchaAnswer, setCaptchaAnswer] = useState(); + const answer = parseInt(captchaAnswer ?? "", 10); + const errors = undefinedIfEmpty({ + answer: !captchaAnswer + ? i18n.str`Answer the question before continue` + : Number.isNaN(answer) + ? i18n.str`The answer should be a number` + : answer !== captchaNumbers.a + captchaNumbers.b + ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.` + : undefined, + }) ?? (busy ? {} as Record : undefined); + return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    +
    +
    +

    + Confirm the withdrawal operation +

    +
    +
    + + + + + + + +
    +
    +
    + +
    { + e.preventDefault() + }} + > +
    + +
    +
    + { + setCaptchaAnswer(e.currentTarget.value) + }} + /> +
    + +
    +
    +
    + + +
    + +
    +
    +
    + {/*
    +
    +

    Wire transfer details

    +
    +
    +
    + {((): VNode => { + switch (details.account.targetType) { + case "iban": { + const p = details.account as PaytoUriIBAN + const name = p.params["receiver-name"] + return +
    +
    Exchange account
    +
    {p.iban}
    +
    + {name && +
    +
    Exchange name
    +
    {p.params["receiver-name"]}
    +
    + } +
    + } + case "x-taler-bank": { + const p = details.account as PaytoUriTalerBank + const name = p.params["receiver-name"] + return +
    +
    Exchange account
    +
    {p.account}
    +
    + {name && +
    +
    Exchange name
    +
    {p.params["receiver-name"]}
    +
    + } +
    + } + default: + return
    +
    Exchange account
    +
    {details.account.targetPath}
    +
    + + } + })()} +
    +
    Withdrawal identification
    +
    {details.reserve}
    +
    +
    +
    Amount
    +
    To be added
    + // {/* Amounts.stringifyValue(details.amount) +
    +
    +
    +
    */} + +
    +
    +
    + ); } -export function AbortedView({ error }: State.Aborted) { +export function AbortedView({ error, onClose }: State.Aborted) { return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    +
    aborted
    ); } -export function ConfirmedView({ error }: State.Confirmed) { +export function ConfirmedView({ error, onClose }: State.Confirmed) { + const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings() return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    + + +
    + +
    + +
    +
    + +
    +

    + + The wire transfer to the Taler exchange bank's account is completed, now the + exchange will send the requested amount into your GNU Taler wallet. + +

    +
    +
    +
    +
    +
    + + + Do not show this again + + + +
    +
    +
    + +
    +
    + ); } -export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> { +export function ReadyView({ uri, onClose }: State.Ready): VNode<{}> { const { i18n } = useTranslationContext(); - return
    + useEffect(() => { + //Taler Wallet WebExtension is listening to headers response and tab updates. + //In the SPA there is no header response with the Taler URI so + //this hack manually triggers the tab update after the QR is in the DOM. + // WebExtension will be using + // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated + document.title = `${document.title} ${uri.withdrawalOperationId}`; + }, []); + const talerWithdrawUri = stringifyWithdrawUri(uri); + const [show, setShow] = useState(false) + return + +
    +
    +

    + On this device +

    +
    +
    +

    + If you are using a desktop browser you can open the popup now or click the link if you have the "Inject Taler support" option enabled. +

    +
    + +
    +
    +
    +
    +
    +

    + On a mobile phone +

    +
    +
    +

    + Scan the QR code with your mobile device. +

    +
    +
    + +
    +
    + {show && +
    + +
    + } +
    +
    + +
    + +
    +
    } diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 573f8c769..2830f5c1e 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -26,12 +26,11 @@ import { useSettings } from "../hooks/settings.js"; * Let the user choose a payment option, * then specify the details trigger the action. */ -export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { +export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJson, goToConfirmOperation: (id: string) => void }): VNode { const { i18n } = useTranslationContext(); - const [settings, updateSettings] = useSettings(); + const [settings] = useSettings(); const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); - // const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined); return (
    @@ -56,6 +55,14 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { Withdraw digital money into your mobile wallet or browser extension + {!!settings.currentWithdrawalOperationId && + + + Operation in progress + + }
    -
    -

    - If you have a Taler wallet installed in this device -

    - -
    -

    - You will see the details of the operation in your wallet including the fees (if applies). - If you still one you can install it from here. -

    -
    -
    -
    - -
    -
    -
    - -
    -
    -

    - Or if you have the wallet in another device -

    -
    - Scan the QR below to start the withdrawal -
    -
    - -
    -
    -
    - -
    -
    - - - ); -} - diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index 08f706919..8dbdd9da6 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -36,33 +36,52 @@ import { useAccessAPI } from "../hooks/access.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { Amount } from "./PaytoWireTransferForm.js"; import { useSettings } from "../hooks/settings.js"; -import { WithdrawalOperationState } from "./WithdrawalQRCode.js"; -import { Loading } from "../components/Loading.js"; +import { OperationState } from "./OperationState/index.js"; const logger = new Logger("WalletWithdrawForm"); const RefAmount = forwardRef(Amount); -export function WalletWithdrawForm({ - focus, - limit, - onSuccess, - onCancel, -}: { + +function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { limit: AmountJson; focus?: boolean; - onSuccess: (operationId: string) => void; + goToConfirmOperation: (operationId: string) => void; onCancel: () => void; }): VNode { const { i18n } = useTranslationContext(); - const { createWithdrawal } = useAccessAPI(); const [settings, updateSettings] = useSettings() + const { createWithdrawal } = useAccessAPI(); const [amountStr, setAmountStr] = useState(`${settings.maxWithdrawalAmount}`); const ref = useRef(null); useEffect(() => { if (focus) ref.current?.focus(); }, [focus]); + if (!!settings.currentWithdrawalOperationId) { + return
    +
    +
    + +
    +
    +

    + There is an operation already +

    +
    +

    + + To complete or cancel the operation click here + +

    +
    +
    +
    +
    + } + const trimmedAmountStr = amountStr?.trim(); const parsedAmount = trimmedAmountStr @@ -92,7 +111,8 @@ export function WalletWithdrawForm({ i18n.str`Server responded with an invalid withdraw URI`, i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`); } else { - onSuccess(uri.withdrawalOperationId); + updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId) + goToConfirmOperation(uri.withdrawalOperationId); } } catch (error) { if (error instanceof RequestError) { @@ -115,113 +135,168 @@ export function WalletWithdrawForm({ } } + return
    { + e.preventDefault() + }} + > +
    +
    +
    + + { + setAmountStr(v); + }} + error={errors?.amount} + ref={ref} + /> +
    +
    + + + + + + +
    + +
    +
    +
    + + +
    + +
    +} + + +export function WalletWithdrawForm({ + focus, + limit, + onCancel, + goToConfirmOperation, +}: { + limit: AmountJson; + focus?: boolean; + goToConfirmOperation: (operationId: string) => void; + onCancel: () => void; +}): VNode { + const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings() + return (

    Prepare your wallet

    - Upon starting you will receive the money in your digital wallet, if you don't have one please install one from here. -

    -

    - After using your wallet you will be redirected here to confirm or cancel the operation. + After using your wallet you will confirm or cancel the operation.

    - {!settings.fastWithdrawal ? -
    { - e.preventDefault() - }} - > -
    -
    -
    - - { - setAmountStr(v); - }} - error={errors?.amount} - ref={ref} - /> -
    -
    - - - - - -
    -
    -
    - - -
    +
    } - - : settings.currentWithdrawalOperationId === undefined ? - : - + : + { - onCancel() - }} + onClose={onCancel} + goToConfirmOperation={goToConfirmOperation} /> - } + } +
    ); } diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 9976babdb..25c571e28 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -18,23 +18,17 @@ import { Amounts, HttpStatusCode, Logger, - TranslatedString, WithdrawUriResult, - parsePaytoUri, - parseWithdrawUri, - stringifyWithdrawUri, + parsePaytoUri } from "@gnu-taler/taler-util"; -import { ErrorType, RequestError, notify, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ErrorType, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { Loading } from "../components/Loading.js"; -import { useAccessAPI, useWithdrawalDetails } from "../hooks/access.js"; +import { useWithdrawalDetails } from "../hooks/access.js"; import { useSettings } from "../hooks/settings.js"; import { handleNotOkResult } from "./HomePage.js"; -import { QrCodeSection, QrCodeSectionSimpler } from "./QrCodeSection.js"; +import { QrCodeSection } from "./QrCodeSection.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; -import { useEffect, useState } from "preact/hooks"; -import { buildRequestErrorMessage } from "../utils.js"; -import { getInitialBackendBaseURL } from "../hooks/backend.js"; const logger = new Logger("WithdrawalQRCode"); @@ -54,18 +48,11 @@ export function WithdrawalQRCode({ const [settings, updateSettings] = useSettings(); const { i18n } = useTranslationContext(); const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId); + if (!result.ok) { if (result.loading) { return ; } - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) { - onClose() - return
    operation not found
    ; - } - // onLoadNotOk(); return handleNotOkResult(i18n)(result); } const { data } = result; @@ -127,22 +114,6 @@ export function WithdrawalQRCode({
    -
    -
    - - - Do not show this again - - - -
    -
    +
    +

    Got status "{error.info.status}" on {error.info.url}

    +
    ); diff --git a/packages/demobank-ui/src/components/Transactions/state.ts b/packages/demobank-ui/src/components/Transactions/state.ts index 30c48aa45..4b62b005e 100644 --- a/packages/demobank-ui/src/components/Transactions/state.ts +++ b/packages/demobank-ui/src/components/Transactions/state.ts @@ -44,7 +44,7 @@ export function useComponentState({ account }: Props): State { cp.targetType === "bitcoin" ? `${cp.targetPath.substring(0, 6)}...` : undefined) ?? "unkown"; - const when = AbsoluteTime.fromMilliseconds(tx.date / 1000); + const when = AbsoluteTime.fromProtocolTimestamp(tx.date); const amount = Amounts.parse(tx.amount); const subject = tx.subject; return { diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx index ebda31035..a587c6f1e 100644 --- a/packages/demobank-ui/src/components/app.tsx +++ b/packages/demobank-ui/src/components/app.tsx @@ -29,6 +29,8 @@ import { useEffect, useState } from "preact/hooks"; import { Loading } from "./Loading.js"; import { getInitialBackendBaseURL } from "../hooks/backend.js"; import { BANK_INTEGRATION_PROTOCOL_VERSION, useConfigState } from "../hooks/config.js"; +import { ErrorLoading } from "./ErrorLoading.js"; +import { BankFrame } from "../pages/BankFrame.js"; const WITH_LOCAL_STORAGE_CACHE = false; /** @@ -76,12 +78,18 @@ function VersionCheck({ children }: { children: ComponentChildren }): VNode { if (checked === undefined) { return } - if (checked === false) { - return
    - the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}" -
    + if (typeof checked === "string") { + return + the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}" + } - return {children} + if (checked === true) { + return {children} + } + + return + + } function localStorageProvider(): Map { diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts index 8d729c1f7..d3d9e02ef 100644 --- a/packages/demobank-ui/src/declaration.d.ts +++ b/packages/demobank-ui/src/declaration.d.ts @@ -205,8 +205,7 @@ namespace SandboxBackend { // Transaction unique ID. Matches // $transaction_id from the URI. row_id: number; - date: number; - // date: Timestamp; + date: Timestamp; } interface CreateBankAccountTransactionCreate { diff --git a/packages/demobank-ui/src/hooks/config.ts b/packages/demobank-ui/src/hooks/config.ts index 4b22e8ad3..4cf677d35 100644 --- a/packages/demobank-ui/src/hooks/config.ts +++ b/packages/demobank-ui/src/hooks/config.ts @@ -1,5 +1,5 @@ import { LibtoolVersion } from "@gnu-taler/taler-util"; -import { useApiContext } from "@gnu-taler/web-util/browser"; +import { ErrorType, HttpError, HttpResponseServerError, RequestError, useApiContext } from "@gnu-taler/web-util/browser"; import { useEffect, useState } from "preact/hooks"; import { getInitialBackendBaseURL } from "./backend.js"; @@ -12,38 +12,32 @@ export const BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0"; async function getConfigState( request: ReturnType["request"], -): Promise { - try { - const url = getInitialBackendBaseURL(); - const result = await request( - url, - `config`, - ); - return result.data; - } catch (error) { - return undefined; - } +): Promise { + const url = getInitialBackendBaseURL(); + const result = await request(url, `config`); + return result.data; } -export function useConfigState(): boolean | undefined { - const [checked, setChecked] = useState() +export function useConfigState(): undefined | true | string | HttpError { + const [checked, setChecked] = useState>() const { request } = useApiContext(); useEffect(() => { - getConfigState(request) - .then((result) => { - if (!result) { - setChecked(false) + .then((s) => { + const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, s.version) + if (r?.compatible) { + setChecked(true); } else { - const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, result.version) - setChecked(r?.compatible); + setChecked(s.version) } }) - .catch((error) => { - setChecked(false); + .catch((error: unknown) => { + if (error instanceof RequestError) { + setChecked(error.cause); + } }); - }); + }, []); return checked; } diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts index c212e7484..ca7e1d447 100644 --- a/packages/demobank-ui/src/pages/AccountPage/state.ts +++ b/packages/demobank-ui/src/pages/AccountPage/state.ts @@ -75,9 +75,7 @@ export function useComponentState({ account, goToBusinessAccount, goToConfirmOpe }; } - // FIXME: balance - const balanceIsDebit = true; - // data.balance.credit_debit_indicator == "debit"; + const balanceIsDebit = data.balance.credit_debit_indicator == "debit"; const limit = balanceIsDebit ? Amounts.sub(debitThreshold, balance).amount : Amounts.add(balance, debitThreshold).amount; diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index c4f872679..5c43d2c3e 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -15,7 +15,7 @@ */ import { Amounts, Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; -import { notifyError, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks"; import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js"; @@ -54,7 +54,12 @@ export function BankFrame({ useEffect(() => { if (error) { - notifyError(i18n.str`Internal error, please report.`, (error instanceof Error ? error.message : String(error)) as TranslatedString) + const desc = (error instanceof Error ? error.stack : String(error)) as TranslatedString + if (error instanceof Error) { + notifyException(i18n.str`Internal error, please report.`, error) + } else { + notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString) + } resetError() } }, [error]) @@ -386,6 +391,11 @@ function StatusBanner(): VNode { {n.message.description} } + {n.message.debug && +
    + {n.message.debug} +
    + } case "info": return
    diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 25c571e28..8f4e175f6 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -45,7 +45,6 @@ export function WithdrawalQRCode({ withdrawUri, onClose, }: Props): VNode { - const [settings, updateSettings] = useSettings(); const { i18n } = useTranslationContext(); const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId); diff --git a/packages/web-util/src/hooks/index.ts b/packages/web-util/src/hooks/index.ts index c29de9023..cc3267dbd 100644 --- a/packages/web-util/src/hooks/index.ts +++ b/packages/web-util/src/hooks/index.ts @@ -4,6 +4,7 @@ export { useMemoryStorage } from "./useMemoryStorage.js"; export { useNotifications, notifyError, + notifyException, notifyInfo, notify, ErrorNotification, diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts index 2f9df24f9..792095b06 100644 --- a/packages/web-util/src/hooks/useNotifications.ts +++ b/packages/web-util/src/hooks/useNotifications.ts @@ -36,6 +36,17 @@ export function notifyError( debug, }); } +export function notifyException( + title: TranslatedString, + ex: Error, +) { + notify({ + type: "error" as const, + title, + description: ex.message as TranslatedString, + debug: ex.stack, + }); +} export function notifyInfo(title: TranslatedString) { notify({ type: "info" as const, -- cgit v1.2.3 From 1708d49a2d5da1f325173a030695223e5a24e75c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 29 Sep 2023 16:02:15 -0300 Subject: more ui --- packages/demobank-ui/dev.mjs | 2 +- packages/demobank-ui/src/components/Attention.tsx | 59 +++++ .../demobank-ui/src/components/ErrorLoading.tsx | 22 +- .../src/components/Transactions/views.tsx | 51 ++-- packages/demobank-ui/src/demobank-ui-settings.js | 21 ++ packages/demobank-ui/src/hooks/access.ts | 3 +- packages/demobank-ui/src/hooks/circuit.ts | 2 +- packages/demobank-ui/src/hooks/settings.ts | 3 + .../demobank-ui/src/pages/AccountPage/views.tsx | 62 ++--- packages/demobank-ui/src/pages/BankFrame.tsx | 104 ++++---- packages/demobank-ui/src/pages/HomePage.tsx | 6 +- packages/demobank-ui/src/pages/LoginForm.tsx | 8 +- .../demobank-ui/src/pages/OperationState/state.ts | 4 +- .../demobank-ui/src/pages/OperationState/views.tsx | 5 +- packages/demobank-ui/src/pages/PaymentOptions.tsx | 5 +- .../src/pages/PaytoWireTransferForm.tsx | 268 ++++++++++++--------- packages/demobank-ui/src/pages/QrCodeSection.tsx | 1 - .../src/pages/UpdateAccountPassword.tsx | 8 +- .../demobank-ui/src/pages/WalletWithdrawForm.tsx | 183 +++++--------- .../src/pages/WithdrawalConfirmationQuestion.tsx | 14 +- .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 12 +- .../demobank-ui/src/pages/admin/AccountForm.tsx | 8 +- packages/demobank-ui/src/pages/admin/Home.tsx | 2 + .../demobank-ui/src/pages/admin/RemoveAccount.tsx | 65 +---- 24 files changed, 448 insertions(+), 470 deletions(-) create mode 100644 packages/demobank-ui/src/components/Attention.tsx create mode 100644 packages/demobank-ui/src/demobank-ui-settings.js (limited to 'packages/demobank-ui/src/pages/WithdrawalQRCode.tsx') diff --git a/packages/demobank-ui/dev.mjs b/packages/demobank-ui/dev.mjs index 9c09e5716..f29a05e49 100755 --- a/packages/demobank-ui/dev.mjs +++ b/packages/demobank-ui/dev.mjs @@ -18,7 +18,7 @@ import { serve } from "@gnu-taler/web-util/node"; import { initializeDev } from "@gnu-taler/web-util/build"; -const devEntryPoints = ["src/stories.tsx", "src/index.tsx"]; +const devEntryPoints = ["src/stories.tsx", "src/index.tsx", "src/demobank-ui-settings.js"]; const build = initializeDev({ type: "development", diff --git a/packages/demobank-ui/src/components/Attention.tsx b/packages/demobank-ui/src/components/Attention.tsx new file mode 100644 index 000000000..3313e5796 --- /dev/null +++ b/packages/demobank-ui/src/components/Attention.tsx @@ -0,0 +1,59 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { assertUnreachable } from "./Routing.js"; + +interface Props { + type?: "info" | "success" | "warning" | "danger", + onClose?: () => void, + title: TranslatedString, + children?: ComponentChildren , +} +export function Attention({ type = "info", title, children, onClose }: Props): VNode { + return
    +
    +
    +
    + + {(() => { + switch (type) { + case "info": + return + case "warning": + return + case "danger": + return + case "success": + return + default: + assertUnreachable(type) + } + })()} + +
    +
    +

    + {title} +

    +
    + {children} +
    +
    + {onClose && +
    + +
    + } +
    +
    + +
    +} diff --git a/packages/demobank-ui/src/components/ErrorLoading.tsx b/packages/demobank-ui/src/components/ErrorLoading.tsx index f83b61234..ee62671ce 100644 --- a/packages/demobank-ui/src/components/ErrorLoading.tsx +++ b/packages/demobank-ui/src/components/ErrorLoading.tsx @@ -17,25 +17,13 @@ import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; +import { Attention } from "./Attention.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; export function ErrorLoading({ error }: { error: HttpError }): VNode { const { i18n } = useTranslationContext() - return ( -
    -
    -
    - -
    -
    -

    {error.message}

    -
    -
    -
    -

    Got status "{error.info.status}" on {error.info.url}

    -
    -
    -
    + return ( +

    Got status "{error.info.status}" on {error.info.url}

    +
    ); } diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx index f8b2e3113..f92c874f3 100644 --- a/packages/demobank-ui/src/components/Transactions/views.tsx +++ b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -19,6 +19,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { State } from "./index.js"; import { format, isToday } from "date-fns"; import { Amounts } from "@gnu-taler/taler-util"; +import { useEffect, useRef } from "preact/hooks"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { const { i18n } = useTranslationContext(); @@ -55,9 +56,9 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode {i18n.str`Date`} - {i18n.str`Amount`} - {i18n.str`Counterpart`} - {i18n.str`Subject`} + {i18n.str`Amount`} + {i18n.str`Counterpart`} + {i18n.str`Subject`} @@ -69,22 +70,38 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode {txs.map(item => { + const time = item.when.t_ms === "never" ? "" : format(item.when.t_ms, "HH:mm:ss") + const amount = + {item.negative ? "-" : ""} + {item.amount ? ( + `${Amounts.stringifyValue(item.amount)} ${item.amount.currency + }` + ) : ( + <{i18n.str`invalid value`}> + )} + return ( -
    {item.when.t_ms === "never" - ? "" - : format(item.when.t_ms, "HH:mm:ss")}
    +
    {time}
    +
    +
    Amount
    +
    + {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? ( + `${Amounts.stringifyValue(item.amount)}` + ) : ( + <{i18n.str`invalid value`}> + )}
    +
    Counterpart
    +
    + {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart} +
    +
    - {item.negative ? "-" : ""} - {item.amount ? ( - `${Amounts.stringifyValue(item.amount)} ${item.amount.currency - }` - ) : ( - <{i18n.str`invalid value`}> - )} - {item.counterpart} + class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> + {amount} + + {item.counterpart} {item.subject} ) })} @@ -94,8 +111,8 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode - -
    - - + return { + updateSettings("showDemoDescription", false); + }}> + {IS_PUBLIC_ACCOUNT_ENABLED ? ( + + This part of the demo shows how a bank that supports Taler + directly would work. In addition to using your own bank + account, you can also see the transaction history of some{" "} + Public Accounts. + + ) : ( + + This part of the demo shows how a bank that supports Taler + directly would work. + + )} + } export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> { diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 15ef8a036..29334cae4 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -15,7 +15,7 @@ */ import { Amounts, Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; -import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { NotificationMessage, notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks"; import { LangSelector } from "../components/LangSelector.js"; @@ -26,6 +26,7 @@ import { useSettings } from "../hooks/settings.js"; import { CopyButton, CopyIcon } from "../components/CopyButton.js"; import logo from "../assets/logo-2021.svg"; import { useAccountDetails } from "../hooks/access.js"; +import { Attention } from "../components/Attention.js"; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -108,7 +109,7 @@ export function BankFrame({ setOpen(!open) }}> - Open main menu + Open settings @@ -227,6 +228,22 @@ export function BankFrame({
  • +
  • +
    + + + Show debug info + + + +
    +
  • @@ -286,10 +303,10 @@ export function BankFrame({ }
    +
    - {children}
    @@ -301,79 +318,46 @@ export function BankFrame({ ); } +function MaybeShowDebugInfo({ info }: { info: any }): VNode { + const [settings] = useSettings() + if (settings.showDebugInfo) { + return
    +    {info}
    +  
    + } + return +} + function StatusBanner(): VNode { const notifs = useNotifications() - return
    { + if (notifs.length === 0) return + return
    { notifs.map(n => { switch (n.message.type) { case "error": - return
    -
    -
    - -
    -
    -

    {n.message.title}

    -
    -
    -

    - -

    -
    -
    + return { + n.remove() + }}> {n.message.description &&
    {n.message.description}
    } + + {/* + show debug info + {n.message.debug &&
    {n.message.debug}
    - } -
    + } */} + case "info": - return
    -
    -
    - -
    -
    -

    {n.message.title}

    - -

    - -

    -
    - -
    -
    + return { + n.remove(); + }} /> } })}
    diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index d945d80d1..95144f086 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -137,8 +137,8 @@ export function handleNotOkResult( const errorData = result.payload; notify({ type: "error", - title: i18n.str`Could not load due to a client error`, - description: errorData?.error?.description as TranslatedString, + title: i18n.str`Could not load due to a request error`, + description: i18n.str`Request to url "${result.info.url}" returned ${result.info.status}`, debug: JSON.stringify(result), }); break; @@ -174,7 +174,7 @@ export function handleNotOkResult( assertUnreachable(result); } } - route("/") + // route("/") return
    error
    ; } return
    ; diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index 14d261622..3ea94b899 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -23,6 +23,7 @@ import { useBackendContext } from "../context/backend.js"; import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js"; import { bankUiSettings } from "../settings.js"; import { undefinedIfEmpty } from "../utils.js"; +import { doAutoFocus } from "./PaytoWireTransferForm.js"; /** @@ -98,8 +99,8 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode { }); } else { saveError({ - title: i18n.str`Could not load due to a client error`, - // description: cause.payload.error.description, + title: i18n.str`Could not load due to a request error`, + description: i18n.str`Request to url "${cause.info.url}" returned ${cause.info.status}`, debug: JSON.stringify(cause.payload), }); } @@ -159,8 +160,7 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {

    - The wire transfer to the Taler exchange bank's account is completed, now the - exchange will send the requested amount into your GNU Taler wallet. + The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.

    diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 49419d0dc..fef272831 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -30,7 +30,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ const { i18n } = useTranslationContext(); const [settings] = useSettings(); - const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); + const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>("wire-transfer"); return (
    @@ -82,7 +82,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ another bank account - Make a wire transfer to an account which you know the address. + Make a wire transfer to an account which you know the bank account number @@ -108,6 +108,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ limit={limit} onSuccess={() => { notifyInfo(i18n.str`Wire transfer created!`); + setTab(undefined) }} onCancel={() => { setTab(undefined) diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index 5f5a6ce3b..785dc4264 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -55,10 +55,11 @@ export function PaytoWireTransferForm({ onCancel: (() => void) | undefined; limit: AmountJson; }): VNode { - const [isRawPayto, setIsRawPayto] = useState(false); - const [iban, setIban] = useState(undefined); - const [subject, setSubject] = useState(undefined); - const [amount, setAmount] = useState(undefined); + const [isRawPayto, setIsRawPayto] = useState(true); + // FIXME: remove this + const [iban, setIban] = useState("DE4745461198061"); + const [subject, setSubject] = useState("ASD"); + const [amount, setAmount] = useState("1.00001"); const [rawPaytoInput, rawPaytoInputSetter] = useState( undefined, @@ -76,17 +77,17 @@ export function PaytoWireTransferForm({ const errorsWire = undefinedIfEmpty({ iban: !iban - ? i18n.str`Missing IBAN` + ? i18n.str`required` : !IBAN_REGEX.test(iban) ? i18n.str`IBAN should have just uppercased letters and numbers` : validateIBAN(iban, i18n), - subject: !subject ? i18n.str`Missing subject` : undefined, + subject: !subject ? i18n.str`required` : undefined, amount: !trimmedAmountStr - ? i18n.str`Missing amount` + ? i18n.str`required` : !parsedAmount - ? i18n.str`Amount is not valid` + ? i18n.str`not valid` : Amounts.isZero(parsedAmount) - ? i18n.str`Should be greater than 0` + ? i18n.str`should be greater than 0` : Amounts.cmp(limit, parsedAmount) === -1 ? i18n.str`balance is not enough` : undefined, @@ -101,14 +102,14 @@ export function PaytoWireTransferForm({ ? i18n.str`required` : !parsed ? i18n.str`does not follow the pattern` - : !parsed.params.amount - ? i18n.str`use the "amount" parameter to specify the amount to be transferred` - : Amounts.parse(parsed.params.amount) === undefined - ? i18n.str`the amount is not valid` - : !parsed.params.message - ? i18n.str`use the "message" parameter to specify a reference text for the transfer` - : !parsed.isKnown || parsed.targetType !== "iban" - ? i18n.str`only "IBAN" target are supported` + : !parsed.isKnown || parsed.targetType !== "iban" + ? i18n.str`only "IBAN" target are supported` + : !parsed.params.amount + ? i18n.str`use the "amount" parameter to specify the amount to be transferred` + : Amounts.parse(parsed.params.amount) === undefined + ? i18n.str`the amount is not valid` + : !parsed.params.message + ? i18n.str`use the "message" parameter to specify a reference text for the transfer` : !IBAN_REGEX.test(parsed.iban) ? i18n.str`IBAN should have just uppercased letters and numbers` : validateIBAN(parsed.iban, i18n), @@ -159,6 +160,9 @@ export function PaytoWireTransferForm({ } return (
    + {/** + * FIXME: Scan a qr code + */}

    {title} @@ -167,6 +171,17 @@ export function PaytoWireTransferForm({
    { @@ -203,105 +228,106 @@ export function PaytoWireTransferForm({ }} >
    -
    - {!isRawPayto ? - - -
    - -
    - { - setIban(e.currentTarget.value); - }} - /> - -
    -

    the receiver of the money

    -
    + {!isRawPayto ? +
    -
    - -
    - { - setSubject(e.currentTarget.value); - }} - /> - -
    -

    some text to identify the transfer

    +
    + +
    + { + setIban(e.currentTarget.value.toUpperCase()); + }} + /> +
    +

    + IBAN of the recipient's account +

    +
    -
    - - { - setAmount(d) +
    + +
    + { + setSubject(e.currentTarget.value); }} /> -

    amount to transfer

    +

    some text to identify the transfer

    +
    - : - -
    - -
    - { - rawPaytoInputSetter(e.currentTarget.value); - }} - /> - -
    -
    +
    + + { + setAmount(d) + }} + /> + +

    amount to transfer

    +
    -
    - } -
    +
    : +
    +
    + +
    +