diff options
Diffstat (limited to 'packages/demobank-ui/src')
-rw-r--r-- | packages/demobank-ui/src/hooks/backend.ts | 28 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/circuit.ts | 3 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/AdminPage.tsx | 5 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/BusinessAccount.tsx | 111 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/LoginForm.tsx | 48 |
5 files changed, 146 insertions, 49 deletions
diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index 3eaf1f186..851a3fb5b 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -16,6 +16,7 @@ import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; import { + ErrorType, RequestError, useLocalStorage, } from "@gnu-taler/web-util/lib/index.browser"; @@ -192,6 +193,33 @@ export function usePublicBackend(): useBackendType { }; } +export function useCredentialsChecker() { + const { request } = useApiContext(); + const baseUrl = getInitialBackendBaseURL(); + //check against account details endpoint + //while sandbox backend doesn't have a login endpoint + return async function testLogin( + username: string, + password: string, + ): Promise<{ + valid: boolean; + cause?: ErrorType; + }> { + try { + await request(baseUrl, `access-api/accounts/${username}/`, { + basicAuth: { username, password }, + preventCache: true, + }); + return { valid: true }; + } catch (error) { + if (error instanceof RequestError) { + return { valid: false, cause: error.cause.type }; + } + return { valid: false, cause: ErrorType.UNEXPECTED }; + } + }; +} + export function useAuthenticatedBackend(): useBackendType { const { state } = useBackendContext(); const { request: requestHandler } = useApiContext(); diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 548862d85..137a7aee2 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -288,9 +288,10 @@ export function useRatiosAndFeeConfig(): HttpResponse< HttpResponseOk<SandboxBackend.Circuit.Config>, RequestError<SandboxBackend.SandboxError> >([`circuit-api/config`], fetcher, { - refreshInterval: 1000, + refreshInterval: 60 * 1000, refreshWhenHidden: false, revalidateOnFocus: false, + revalidateIfStale: false, revalidateOnReconnect: false, refreshWhenOffline: false, errorRetryCount: 0, diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx index f565455bb..ac0457e9b 100644 --- a/packages/demobank-ui/src/pages/AdminPage.tsx +++ b/packages/demobank-ui/src/pages/AdminPage.tsx @@ -36,9 +36,9 @@ import { } from "../context/pageState.js"; import { useAccountDetails } from "../hooks/access.js"; import { + useAdminAccountAPI, useBusinessAccountDetails, useBusinessAccounts, - useAdminAccountAPI, } from "../hooks/circuit.js"; import { buildRequestErrorMessage, @@ -50,7 +50,6 @@ import { } from "../utils.js"; import { ErrorBannerFloat } from "./BankFrame.js"; import { ShowCashoutDetails } from "./BusinessAccount.js"; -import { PaymentOptions } from "./PaymentOptions.js"; import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; @@ -581,7 +580,6 @@ function CreateNewAccount({ template={undefined} purpose="create" onChange={(a) => { - console.log(a); setSubmitAccount(a); }} /> @@ -831,6 +829,7 @@ function RemoveAccount({ title: i18n.str`Can't delete the account`, description: i18n.str`Balance is not empty`, }} + onClear={() => saveError(undefined)} /> )} {error && ( diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/BusinessAccount.tsx index 128b47114..258802e58 100644 --- a/packages/demobank-ui/src/pages/BusinessAccount.tsx +++ b/packages/demobank-ui/src/pages/BusinessAccount.tsx @@ -237,6 +237,7 @@ function CreateCashout({ 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); @@ -254,23 +255,14 @@ function CreateCashout({ if (!sellRate || sellRate < 0) return <div>error rate</div>; const amount = Amounts.parse(`${balance.currency}:${form.amount}`); - const amount_debit = !amount - ? zero - : form.isDebit - ? amount - : truncate(Amounts.divide(Amounts.add(amount, sellFee).amount, sellRate)); - const credit_before_fee = !amount - ? zero - : form.isDebit - ? truncate(Amounts.divide(amount, 1 / sellRate)) - : Amounts.add(amount, sellFee).amount; - const __amount_credit = Amounts.sub(credit_before_fee, sellFee).amount; - const amount_credit = Amounts.parseOrThrow( - `${fiatCurrency}:${Amounts.stringifyValue(__amount_credit)}`, - ); + const calc = !amount + ? { debit: zero, credit: zero, beforeFee: zero } + : !form.isDebit + ? calculateFromCredit(amount, sellFee, sellRate) + : calculateFromDebit(amount, sellFee, sellRate); - const balanceAfter = Amounts.sub(balance, amount_debit).amount; + const balanceAfter = Amounts.sub(balance, calc.debit).amount; function updateForm(newForm: typeof form): void { setForm(newForm); @@ -280,11 +272,11 @@ function CreateCashout({ ? i18n.str`required` : !amount ? i18n.str`could not be parsed` - : Amounts.cmp(limit, amount_debit) === -1 + : Amounts.cmp(limit, calc.debit) === -1 ? i18n.str`balance is not enough` - : Amounts.cmp(credit_before_fee, sellFee) === -1 + : Amounts.cmp(calc.beforeFee, sellFee) === -1 ? i18n.str`the total amount to transfer does not cover the fees` - : Amounts.isZero(amount_credit) + : Amounts.isZero(calc.credit) ? i18n.str`the total transfer at destination will be zero` : undefined, channel: !form.channel ? i18n.str`required` : undefined, @@ -408,7 +400,7 @@ function CreateCashout({ id="withdraw-amount" disabled name="withdraw-amount" - value={amount_debit ? Amounts.stringifyValue(amount_debit) : ""} + value={Amounts.stringifyValue(calc.debit)} /> </div> </fieldset> @@ -454,7 +446,7 @@ function CreateCashout({ // type="number" style={{ color: "black" }} disabled - value={Amounts.stringifyValue(credit_before_fee)} + value={Amounts.stringifyValue(calc.beforeFee)} /> </div> </fieldset> @@ -503,7 +495,7 @@ function CreateCashout({ id="withdraw-amount" disabled name="withdraw-amount" - value={amount_credit ? Amounts.stringifyValue(amount_credit) : ""} + value={Amounts.stringifyValue(calc.credit)} /> </div> </fieldset> @@ -584,8 +576,10 @@ function CreateCashout({ if (errors) return; try { const res = await createCashout({ - amount_credit: Amounts.stringify(amount_credit), - amount_debit: Amounts.stringify(amount_debit), + amount_credit: `${fiatCurrency}:${Amounts.stringifyValue( + calc.credit, + )}`, + amount_debit: Amounts.stringify(calc.debit), subject: form.subject, tan_channel: form.channel, }); @@ -630,25 +624,6 @@ function CreateCashout({ ); } -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); -} - interface ShowCashoutProps { id: string; onCancel: () => void; @@ -836,6 +811,58 @@ export function ShowCashoutDetails({ ); } +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); +} + +type TransferCalculation = { + debit: AmountJson; + credit: AmountJson; + beforeFee: AmountJson; +}; + +function calculateFromDebit( + amount: AmountJson, + sellFee: AmountJson, + sellRate: number, +): TransferCalculation { + const debit = amount; + + const beforeFee = truncate(Amounts.divide(debit, 1 / sellRate)); + + const credit = Amounts.sub(beforeFee, sellFee).amount; + return { debit, credit, beforeFee }; +} + +function calculateFromCredit( + amount: AmountJson, + sellFee: AmountJson, + sellRate: number, +): TransferCalculation { + const credit = amount; + + const beforeFee = Amounts.add(credit, sellFee).amount; + + const debit = truncate(Amounts.divide(beforeFee, sellRate)); + + return { debit, credit, beforeFee }; +} + export function assertUnreachable(x: never): never { throw new Error("Didn't expect to get here"); } diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index 2452745a5..16d2373da 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -14,12 +14,18 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser"; +import { + ErrorType, + useTranslationContext, +} from "@gnu-taler/web-util/lib/index.browser"; import { Fragment, h, VNode } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; import { useBackendContext } from "../context/backend.js"; +import { ErrorMessage } from "../context/pageState.js"; +import { useCredentialsChecker } from "../hooks/backend.js"; import { bankUiSettings } from "../settings.js"; import { undefinedIfEmpty } from "../utils.js"; +import { ErrorBannerFloat } from "./BankFrame.js"; import { USERNAME_REGEX } from "./RegistrationPage.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; @@ -31,6 +37,8 @@ export function LoginForm({ onRegister }: { onRegister: () => void }): VNode { const [username, setUsername] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>(); const { i18n } = useTranslationContext(); + const testLogin = useCredentialsChecker(); + const [error, saveError] = useState<ErrorMessage | undefined>(); const ref = useRef<HTMLInputElement>(null); useEffect(function focusInput() { ref.current?.focus(); @@ -48,6 +56,9 @@ export function LoginForm({ onRegister }: { onRegister: () => void }): VNode { return ( <Fragment> <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1> + {error && ( + <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} /> + )} <div class="login-div"> <form class="login-form" @@ -105,10 +116,41 @@ export function LoginForm({ onRegister }: { onRegister: () => void }): VNode { type="submit" class="pure-button pure-button-primary" disabled={!!errors} - onClick={(e) => { + onClick={async (e) => { e.preventDefault(); if (!username || !password) return; - backend.logIn({ username, password }); + const { valid, cause } = await testLogin(username, password); + if (valid) { + backend.logIn({ username, password }); + } else { + switch (cause) { + case ErrorType.CLIENT: { + saveError({ + title: i18n.str`Wrong credentials or username`, + }); + break; + } + case ErrorType.SERVER: { + saveError({ + title: i18n.str`Server had a problem, try again later or report.`, + }); + break; + } + case ErrorType.TIMEOUT: { + saveError({ + title: i18n.str`Could not reach the server, please report.`, + }); + break; + } + default: { + saveError({ + title: i18n.str`Unexpected error, please report.`, + }); + break; + } + } + backend.logOut(); + } setUsername(undefined); setPassword(undefined); }} |