diff --git a/packages/demobank-ui/TODO b/packages/demobank-ui/TODO index a399ada58..cc578cce0 100644 --- a/packages/demobank-ui/TODO +++ b/packages/demobank-ui/TODO @@ -1,45 +1,49 @@ Urgent TODOs: - General: - * not only Nora dark-theme, but default light! (CSS) - * auto-focus on input fields is not working well - * buttons should be visibly insensitive + + - not only Nora dark-theme, but default light! (CSS) + - DONE: auto-focus on input fields is not working well + - DONE: buttons should be visibly insensitive as long as required input fields are not working - * next required invalid/missing input field is + - DONE: next required invalid/missing input field is not properly highlighted in red - * Logout button needs more padding to the right (CSS) + - Logout button needs more padding to the right (CSS) - Error bar: - * shows JSON, should only show good error message + - DONE: hows JSON, should only show good error message and numeric code, not JSON syntax - * should auto-hide after next action, no need for + - should auto-hide after next action, no need for "clear"! - * need variant "status bar" in green (or blue) + - need variant "status bar" in green (or blue) which shows status of last operation * H1-Titles: - * Center more (currently way on the left) (CSS) + - Center more (currently way on the left) (CSS) - Assets: - * Numeric amount needs to be shown MUCH bigger (CSS) - * Center more? (CSS) + + - Numeric amount needs to be shown MUCH bigger (CSS) + - Center more? (CSS) - Payments: - * Amount to withdraw currently shown in white-on-white (CSS) - * Big frame drawn around notebook-tabs is not nice (CSS) - * Center more? (CSS) - * "Wire to bank account" + + - Amount to withdraw currently shown in white-on-white (CSS) + - Big frame drawn around notebook-tabs is not nice (CSS) + - Center more? (CSS) + - "Wire to bank account" - maybe split two types (payto and IBAN) into two tabs? - currently cannot switch back from payto to IBAN - Withdraw: - * Should use new 'status' bar at the end, instead + + - Should use new 'status' bar at the end, instead of extra dialog with "close" button - * ditto for bank-wire-transfer final stage + - ditto for bank-wire-transfer final stage - Footer: - * overlaps with transaction history or other + - overlaps with transaction history or other content, needs to consistently show at the end! => change rendering logic!? (CSS?) diff --git a/packages/demobank-ui/src/pages/home/index.tsx b/packages/demobank-ui/src/pages/home/index.tsx index 8ab815efd..ae4051b9b 100644 --- a/packages/demobank-ui/src/pages/home/index.tsx +++ b/packages/demobank-ui/src/pages/home/index.tsx @@ -31,7 +31,7 @@ import { QR } from "../../components/QR.js"; import { useLocalStorage, useNotNullLocalStorage } from "../../hooks/index.js"; import { Translate, useTranslator } from "../../i18n/index.js"; import "../../scss/main.scss"; -import { parsePaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util"; /** * If the first argument does not look like a placeholder, return it. @@ -80,7 +80,22 @@ const UI_BANK_NAME = replacementOrDefault( * Contexts * ***********/ const CurrencyContext = createContext(null); -const PageContext = createContext(null); +type PageContextType = [PageStateType, StateUpdater]; +const PageContextDefault: PageContextType = [ + { + hasError: false, + hasInfo: false, + isLoggedIn: false, + isRawPayto: false, + showPublicHistories: false, + tryRegister: false, + withdrawalInProgress: false, + }, + () => { + null; + }, +]; +const PageContext = createContext(PageContextDefault); /********************************************** * Type definitions for states and API calls. * @@ -91,9 +106,9 @@ const PageContext = createContext(null); * authenticate at the bank's backend. */ interface BackendStateType { - url: string; - username: string; - password: string; + url?: string; + username?: string; + password?: string; } /** @@ -112,9 +127,9 @@ interface TransactionRequestType { * Request body of /register. */ interface CredentialsRequestType { - username: string; - password: string; - repeatPassword: string; + username?: string; + password?: string; + repeatPassword?: string; } /** @@ -126,14 +141,9 @@ interface CredentialsRequestType { // } interface WireTransferRequestType { - iban: string; - subject: string; - amount: string; -} - -interface Amount { - value: string; - currency: string; + iban?: string; + subject?: string; + amount?: string; } /** @@ -147,7 +157,12 @@ interface PageStateType { hasError: boolean; hasInfo: boolean; withdrawalInProgress: boolean; - error?: string; + error?: { + description?: string; + title: string; + debug?: string; + }; + info?: string; talerWithdrawUri?: string; /** @@ -225,12 +240,13 @@ function getIbanFromPayto(url: string): string { /** * Extract value and currency from a $currency:x.y amount. */ -function parseAmount(val: string): Amount { - const format = /^[A-Z]+:[0-9]+(\.[0-9]+)?$/; - if (!format.test(val)) throw Error(`Backend gave invalid amount: ${val}.`); - const amountSplit = val.split(":"); - return { value: amountSplit[1], currency: amountSplit[0] }; -} +// function parseAmount(val: string): Amount { +// Amounts.parse(val) +// const format = /^[A-Z]+:[0-9]+(\.[0-9]+)?$/; +// if (!format.test(val)) throw Error(`Backend gave invalid amount: ${val}.`); +// const amountSplit = val.split(":"); +// return { value: amountSplit[1], currency: amountSplit[0] }; +// } /** * Get username from the backend state, and throw @@ -240,6 +256,9 @@ function getUsername(backendState: BackendStateTypeOpt): string { if (typeof backendState === "undefined") throw Error("Username can't be found in a undefined backend state."); + if (!backendState.username) { + throw Error("No username, must login first."); + } return backendState.username; } @@ -283,12 +302,14 @@ function useTransactionPageNumber(): [number, StateUpdater] { /** * Craft headers with Authorization and Content-Type. */ -function prepareHeaders(username: string, password: string): Headers { +function prepareHeaders(username?: string, password?: string): Headers { const headers = new Headers(); - headers.append( - "Authorization", - `Basic ${window.btoa(`${username}:${password}`)}`, - ); + if (username && password) { + headers.append( + "Authorization", + `Basic ${window.btoa(`${username}:${password}`)}`, + ); + } headers.append("Content-Type", "application/json"); return headers; } @@ -461,13 +482,13 @@ function usePageState( ): [PageStateType, StateUpdater] { const ret = useNotNullLocalStorage("page-state", JSON.stringify(state)); const retObj: PageStateType = JSON.parse(ret[0]); - console.log("Current page state", retObj); + const retSetter: StateUpdater = function (val) { const newVal = val instanceof Function ? JSON.stringify(val(retObj)) : JSON.stringify(val); - console.log("Setting new page state", newVal); + ret[1](newVal); }; return [retObj, retSetter]; @@ -502,7 +523,9 @@ async function abortWithdrawalCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: "No credentials found.", + error: { + title: `No credentials found.`, + }, })); return; } @@ -511,7 +534,9 @@ async function abortWithdrawalCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: "No withdrawal ID found.", + error: { + title: `No withdrawal ID found.`, + }, })); return; } @@ -541,11 +566,16 @@ async function abortWithdrawalCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: `Could not abort the withdrawal: ${error}`, + error: { + title: `Could not abort the withdrawal.`, + description: (error as any).error.description, + debug: JSON.stringify(error), + }, })); return; } if (!res.ok) { + const response = await res.json(); console.log( `Withdrawal abort gave response error (${res.status})`, res.statusText, @@ -553,7 +583,11 @@ async function abortWithdrawalCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: `Withdrawal abortion gave response error (${res.status})`, + error: { + title: `Withdrawal abortion failed.`, + description: response.error.description, + debug: JSON.stringify(res.status), + }, })); return; } @@ -587,7 +621,9 @@ async function confirmWithdrawalCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: "No credentials found.", + error: { + title: "No credentials found.", + }, })); return; } @@ -596,7 +632,9 @@ async function confirmWithdrawalCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: "No withdrawal ID found.", + error: { + title: "No withdrawal ID found.", + }, })); return; } @@ -629,11 +667,15 @@ async function confirmWithdrawalCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: `Could not confirm the withdrawal: ${error}`, + error: { + title: `Could not confirm the withdrawal`, + description: (error as any).error.description, + debug: JSON.stringify(error), + }, })); return; } - if (res ? !res.ok : true) { + if (!res || !res.ok) { // assume not ok if res is null console.log( `Withdrawal confirmation gave response error (${res.status})`, @@ -642,7 +684,10 @@ async function confirmWithdrawalCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: `Withdrawal confirmation gave response error (${res.status})`, + error: { + title: `Withdrawal confirmation gave response error`, + debug: JSON.stringify(res.status), + }, })); return; } @@ -684,20 +729,28 @@ async function createTransactionCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: `Could not create the wire transfer: ${error}`, + error: { + title: `Could not create the wire transfer`, + description: (error as any).error.description, + debug: JSON.stringify(error), + }, })); return; } // POST happened, status not sure yet. if (!res.ok) { - const responseText = JSON.stringify(await res.json()); + const response = await res.json(); console.log( - `Transfer creation gave response error: ${responseText} (${res.status})`, + `Transfer creation gave response error: ${response} (${res.status})`, ); pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: `Transfer creation gave response error: ${responseText} (${res.status})`, + error: { + title: `Transfer creation gave response error`, + description: response.error.description, + debug: JSON.stringify(response), + }, })); return; } @@ -733,7 +786,9 @@ async function createWithdrawalCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: "No credentials given.", + error: { + title: "No credentials given.", + }, })); return; } @@ -758,19 +813,27 @@ async function createWithdrawalCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: `Could not create withdrawal operation: ${error}`, + error: { + title: `Could not create withdrawal operation`, + description: (error as any).error.description, + debug: JSON.stringify(error), + }, })); return; } if (!res.ok) { - const responseText = await res.text(); + const response = await res.text(); console.log( - `Withdrawal creation gave response error: ${responseText} (${res.status})`, + `Withdrawal creation gave response error: ${response} (${res.status})`, ); pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: `Withdrawal creation gave response error: ${responseText} (${res.status})`, + error: { + title: `Withdrawal creation gave response error`, + description: response.error.description, + debug: `${response} (${res.status})`, + }, })); return; } @@ -853,7 +916,10 @@ async function registrationCall( pageStateSetter((prevState) => ({ ...prevState, hasError: true, - error: "Registration failed, please report.", + error: { + title: `Registration failed, please report`, + debug: JSON.stringify(error), + }, })); return; } @@ -893,7 +959,24 @@ function ErrorBanner(Props: any): VNode | null { if (!pageState.hasError) return null; const rval = ( -

{pageState.error}

+
+
+

+ {pageState.error.title} +

+
+ { + pageStateSetter((prev) => ({ ...prev, error: undefined })); + }} + /> +
+
+

{pageState.error.description}

+
); delete pageState.error; pageState.hasError = false; @@ -1024,7 +1107,11 @@ function ShowInputErrorLabel({ isDirty: boolean; }): VNode { if (message && isDirty) - return

{message}

; + return ( +
+ {message} +
+ ); return ; } @@ -1046,6 +1133,33 @@ function PaytoWireTransfer(Props: any): VNode { if (focus) ref.current?.focus(); }, [focus, pageState.isRawPayto]); + // typeof submitData === "undefined" || + // typeof submitData.iban === "undefined" || + // submitData.iban === "" || + // typeof submitData.subject === "undefined" || + // submitData.subject === "" || + // typeof submitData.amount === "undefined" || + // submitData.amount === "" + let parsedAmount = undefined; + + const errorsWire = !submitData + ? undefined + : undefinedIfEmpty({ + iban: !submitData.iban + ? i18n`Missing IBAN` + : !/^[A-Z0-9]*$/.test(submitData.iban) + ? i18n`IBAN should have just uppercased letters and numbers` + : undefined, + subject: !submitData.subject ? i18n`Missing subject` : undefined, + amount: !submitData.amount + ? i18n`Missing amount` + : !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`)) + ? i18n`Amount is not valid` + : Amounts.isZero(parsedAmount) + ? i18n`Should be greater than 0` + : undefined, + }); + if (!pageState.isRawPayto) return (
@@ -1069,6 +1183,10 @@ function PaytoWireTransfer(Props: any): VNode { }} />
+
 
+
  { submitDataSetter((submitData: any) => ({ ...submitData, - amount: e.currentTarget.value.replace(",", "."), + amount: e.currentTarget.value, })); }} /> @@ -1113,11 +1235,17 @@ function PaytoWireTransfer(Props: any): VNode { tabIndex={-1} value={currency} /> +

-

+ +

{ if ( @@ -1133,7 +1261,9 @@ function PaytoWireTransfer(Props: any): VNode { pageStateSetter((prevState: PageStateType) => ({ ...prevState, hasError: true, - error: i18n`Field(s) missing.`, + error: { + title: i18n`Field(s) missing.`, + }, })); return; } @@ -1147,15 +1277,18 @@ function PaytoWireTransfer(Props: any): VNode { transactionData, backendState, pageStateSetter, - () => - submitDataSetter((p) => ({ - amount: "", - iban: "", - subject: "", - })), + () => submitDataSetter((p) => ({})), ); }} /> + { + submitDataSetter((p) => ({})); + }} + />

@@ -1175,7 +1308,7 @@ function PaytoWireTransfer(Props: any): VNode { ); - const errors = undefinedIfEmpty({ + const errorsPayto = undefinedIfEmpty({ rawPaytoInput: !rawPaytoInput ? i18n`Missing payto address` : !parsePaytoUri(rawPaytoInput) @@ -1204,7 +1337,7 @@ function PaytoWireTransfer(Props: any): VNode { }} />
@@ -1220,7 +1353,7 @@ function PaytoWireTransfer(Props: any): VNode { { // empty string evaluates to false. @@ -1323,7 +1456,9 @@ function TalerWithdrawalConfirmationQuestion(Props: any): VNode { pageStateSetter((prevState: PageStateType) => ({ ...prevState, hasError: true, - error: i18n`Answer is wrong.`, + error: { + title: i18n`Answer is wrong.`, + }, })); }} > @@ -1366,7 +1501,7 @@ function QrCodeSection({ }: { talerWithdrawUri: string; abortButton: h.JSX.Element; -}) { +}): VNode { const i18n = useTranslator(); useEffect(() => { //Taler Wallet WebExtension is listening to headers response and tab updates. @@ -1405,8 +1540,12 @@ function TalerWithdrawalQRCode(Props: any): VNode { class="pure-button" onClick={() => { pageStateSetter((prevState: PageStateType) => { - const { withdrawalId, talerWithdrawUri, ...rest } = prevState; - return { ...rest, withdrawalInProgress: false }; + return { + ...prevState, + withdrawalId: undefined, + talerWithdrawUri: undefined, + withdrawalInProgress: false, + }; }); }} >{i18n`Abort`} @@ -1415,8 +1554,9 @@ function TalerWithdrawalQRCode(Props: any): VNode { console.log(`Showing withdraw URI: ${talerWithdrawUri}`); // waiting for the wallet: - const { data, error, mutate } = useSWR( + const { data, error } = useSWR( `integration-api/withdrawal-operation/${withdrawalId}`, + { refreshInterval: 1000 }, ); if (typeof error !== "undefined") { @@ -1427,7 +1567,9 @@ function TalerWithdrawalQRCode(Props: any): VNode { pageStateSetter((prevState: PageStateType) => ({ ...prevState, hasError: true, - error: i18n`withdrawal (${withdrawalId}) was never (correctly) created at the bank...`, + error: { + title: i18n`withdrawal (${withdrawalId}) was never (correctly) created at the bank...`, + }, })); return ( @@ -1453,12 +1595,13 @@ function TalerWithdrawalQRCode(Props: any): VNode { ...rest, withdrawalInProgress: false, hasError: true, - error: i18n`This withdrawal was aborted!`, + error: { + title: i18n`This withdrawal was aborted!`, + }, }; }); if (!data.selection_done) { - setTimeout(() => mutate(), 1000); // check again after 1 second. return ( {i18n`Login`} @@ -1963,7 +2103,13 @@ function Account(Props: any): VNode { const { accountLabel, backendState } = Props; // Getting the bank account balance: const endpoint = `access-api/accounts/${accountLabel}`; - const { data, error } = useSWR(endpoint); + const { data, error } = useSWR(endpoint, { + // refreshInterval: 0, + // revalidateIfStale: false, + // revalidateOnMount: false, + // revalidateOnFocus: false, + // revalidateOnReconnect: false, + }); const [pageState, pageStateSetter] = useContext(PageContext); const { withdrawalInProgress, withdrawalId, isLoggedIn, talerWithdrawUri } = pageState; @@ -1978,7 +2124,7 @@ function Account(Props: any): VNode { txsPages.push(); if (typeof error !== "undefined") { - console.log("account error", error); + console.log("account error", error, endpoint); /** * FIXME: to minimize the code, try only one invocation * of pageStateSetter, after having decided the error @@ -1990,7 +2136,9 @@ function Account(Props: any): VNode { ...prevState, hasError: true, isLoggedIn: false, - error: i18n`Username or account label '${accountLabel}' not found. Won't login.`, + error: { + title: i18n`Username or account label '${accountLabel}' not found. Won't login.`, + }, })); /** @@ -2014,7 +2162,9 @@ function Account(Props: any): VNode { ...prevState, hasError: true, isLoggedIn: false, - error: i18n`Wrong credentials given.`, + error: { + title: i18n`Wrong credentials given.`, + }, })); return

Wrong credentials...

; } @@ -2022,8 +2172,11 @@ function Account(Props: any): VNode { pageStateSetter((prevState: PageStateType) => ({ ...prevState, hasError: true, - isLoggedIn: false, - error: i18n`Account information could not be retrieved.`, + // isLoggedIn: false, + error: { + title: i18n`Account information could not be retrieved.`, + debug: JSON.stringify(error), + }, })); return

Unknown problem...

; } @@ -2057,7 +2210,8 @@ function Account(Props: any): VNode { ); } - const balance = parseAmount(data.balance.amount); + const balance = Amounts.parseOrThrow(data.balance.amount); + const balanceValue = Amounts.stringifyValue(balance); return ( @@ -2073,7 +2227,7 @@ function Account(Props: any): VNode {

{i18n`Bank account balance`}

{data.balance.credit_debit_indicator == "debit" ? - : null}
- {`${balance.value}`}  + {`${balanceValue}`}  {`${balance.currency}`}
@@ -2292,7 +2446,9 @@ export function BankHome(): VNode { ...prevState, hasError: true, isLoggedIn: false, - error: i18n`Page has a problem: logged in but backend state is lost.`, + error: { + title: i18n`Page has a problem: logged in but backend state is lost.`, + }, })); return

Error: waiting for details...

; }