test login with an endpoint and cleaner calculation
This commit is contained in:
parent
0700bbe9d1
commit
0bf92a44df
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
|
import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
|
ErrorType,
|
||||||
RequestError,
|
RequestError,
|
||||||
useLocalStorage,
|
useLocalStorage,
|
||||||
} from "@gnu-taler/web-util/lib/index.browser";
|
} 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 {
|
export function useAuthenticatedBackend(): useBackendType {
|
||||||
const { state } = useBackendContext();
|
const { state } = useBackendContext();
|
||||||
const { request: requestHandler } = useApiContext();
|
const { request: requestHandler } = useApiContext();
|
||||||
|
@ -288,9 +288,10 @@ export function useRatiosAndFeeConfig(): HttpResponse<
|
|||||||
HttpResponseOk<SandboxBackend.Circuit.Config>,
|
HttpResponseOk<SandboxBackend.Circuit.Config>,
|
||||||
RequestError<SandboxBackend.SandboxError>
|
RequestError<SandboxBackend.SandboxError>
|
||||||
>([`circuit-api/config`], fetcher, {
|
>([`circuit-api/config`], fetcher, {
|
||||||
refreshInterval: 1000,
|
refreshInterval: 60 * 1000,
|
||||||
refreshWhenHidden: false,
|
refreshWhenHidden: false,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
revalidateOnReconnect: false,
|
revalidateOnReconnect: false,
|
||||||
refreshWhenOffline: false,
|
refreshWhenOffline: false,
|
||||||
errorRetryCount: 0,
|
errorRetryCount: 0,
|
||||||
|
@ -36,9 +36,9 @@ import {
|
|||||||
} from "../context/pageState.js";
|
} from "../context/pageState.js";
|
||||||
import { useAccountDetails } from "../hooks/access.js";
|
import { useAccountDetails } from "../hooks/access.js";
|
||||||
import {
|
import {
|
||||||
|
useAdminAccountAPI,
|
||||||
useBusinessAccountDetails,
|
useBusinessAccountDetails,
|
||||||
useBusinessAccounts,
|
useBusinessAccounts,
|
||||||
useAdminAccountAPI,
|
|
||||||
} from "../hooks/circuit.js";
|
} from "../hooks/circuit.js";
|
||||||
import {
|
import {
|
||||||
buildRequestErrorMessage,
|
buildRequestErrorMessage,
|
||||||
@ -50,7 +50,6 @@ import {
|
|||||||
} from "../utils.js";
|
} from "../utils.js";
|
||||||
import { ErrorBannerFloat } from "./BankFrame.js";
|
import { ErrorBannerFloat } from "./BankFrame.js";
|
||||||
import { ShowCashoutDetails } from "./BusinessAccount.js";
|
import { ShowCashoutDetails } from "./BusinessAccount.js";
|
||||||
import { PaymentOptions } from "./PaymentOptions.js";
|
|
||||||
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
|
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
|
||||||
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
||||||
|
|
||||||
@ -581,7 +580,6 @@ function CreateNewAccount({
|
|||||||
template={undefined}
|
template={undefined}
|
||||||
purpose="create"
|
purpose="create"
|
||||||
onChange={(a) => {
|
onChange={(a) => {
|
||||||
console.log(a);
|
|
||||||
setSubmitAccount(a);
|
setSubmitAccount(a);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -831,6 +829,7 @@ function RemoveAccount({
|
|||||||
title: i18n.str`Can't delete the account`,
|
title: i18n.str`Can't delete the account`,
|
||||||
description: i18n.str`Balance is not empty`,
|
description: i18n.str`Balance is not empty`,
|
||||||
}}
|
}}
|
||||||
|
onClear={() => saveError(undefined)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
|
@ -237,6 +237,7 @@ function CreateCashout({
|
|||||||
if (!result.ok) return onLoadNotOk(result);
|
if (!result.ok) return onLoadNotOk(result);
|
||||||
if (!ratiosResult.ok) return onLoadNotOk(ratiosResult);
|
if (!ratiosResult.ok) return onLoadNotOk(ratiosResult);
|
||||||
const config = ratiosResult.data;
|
const config = ratiosResult.data;
|
||||||
|
|
||||||
const balance = Amounts.parseOrThrow(result.data.balance.amount);
|
const balance = Amounts.parseOrThrow(result.data.balance.amount);
|
||||||
const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
|
const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
|
||||||
const zero = Amounts.zeroOfCurrency(balance.currency);
|
const zero = Amounts.zeroOfCurrency(balance.currency);
|
||||||
@ -254,23 +255,14 @@ function CreateCashout({
|
|||||||
if (!sellRate || sellRate < 0) return <div>error rate</div>;
|
if (!sellRate || sellRate < 0) return <div>error rate</div>;
|
||||||
|
|
||||||
const amount = Amounts.parse(`${balance.currency}:${form.amount}`);
|
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 calc = !amount
|
||||||
const amount_credit = Amounts.parseOrThrow(
|
? { debit: zero, credit: zero, beforeFee: zero }
|
||||||
`${fiatCurrency}:${Amounts.stringifyValue(__amount_credit)}`,
|
: !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 {
|
function updateForm(newForm: typeof form): void {
|
||||||
setForm(newForm);
|
setForm(newForm);
|
||||||
@ -280,11 +272,11 @@ function CreateCashout({
|
|||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: !amount
|
: !amount
|
||||||
? i18n.str`could not be parsed`
|
? i18n.str`could not be parsed`
|
||||||
: Amounts.cmp(limit, amount_debit) === -1
|
: Amounts.cmp(limit, calc.debit) === -1
|
||||||
? i18n.str`balance is not enough`
|
? 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`
|
? 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`
|
? i18n.str`the total transfer at destination will be zero`
|
||||||
: undefined,
|
: undefined,
|
||||||
channel: !form.channel ? i18n.str`required` : undefined,
|
channel: !form.channel ? i18n.str`required` : undefined,
|
||||||
@ -408,7 +400,7 @@ function CreateCashout({
|
|||||||
id="withdraw-amount"
|
id="withdraw-amount"
|
||||||
disabled
|
disabled
|
||||||
name="withdraw-amount"
|
name="withdraw-amount"
|
||||||
value={amount_debit ? Amounts.stringifyValue(amount_debit) : ""}
|
value={Amounts.stringifyValue(calc.debit)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -454,7 +446,7 @@ function CreateCashout({
|
|||||||
// type="number"
|
// type="number"
|
||||||
style={{ color: "black" }}
|
style={{ color: "black" }}
|
||||||
disabled
|
disabled
|
||||||
value={Amounts.stringifyValue(credit_before_fee)}
|
value={Amounts.stringifyValue(calc.beforeFee)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -503,7 +495,7 @@ function CreateCashout({
|
|||||||
id="withdraw-amount"
|
id="withdraw-amount"
|
||||||
disabled
|
disabled
|
||||||
name="withdraw-amount"
|
name="withdraw-amount"
|
||||||
value={amount_credit ? Amounts.stringifyValue(amount_credit) : ""}
|
value={Amounts.stringifyValue(calc.credit)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -584,8 +576,10 @@ function CreateCashout({
|
|||||||
if (errors) return;
|
if (errors) return;
|
||||||
try {
|
try {
|
||||||
const res = await createCashout({
|
const res = await createCashout({
|
||||||
amount_credit: Amounts.stringify(amount_credit),
|
amount_credit: `${fiatCurrency}:${Amounts.stringifyValue(
|
||||||
amount_debit: Amounts.stringify(amount_debit),
|
calc.credit,
|
||||||
|
)}`,
|
||||||
|
amount_debit: Amounts.stringify(calc.debit),
|
||||||
subject: form.subject,
|
subject: form.subject,
|
||||||
tan_channel: form.channel,
|
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 {
|
interface ShowCashoutProps {
|
||||||
id: string;
|
id: string;
|
||||||
onCancel: () => void;
|
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 {
|
export function assertUnreachable(x: never): never {
|
||||||
throw new Error("Didn't expect to get here");
|
throw new Error("Didn't expect to get here");
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,18 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
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 { Fragment, h, VNode } from "preact";
|
||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
|
import { ErrorMessage } from "../context/pageState.js";
|
||||||
|
import { useCredentialsChecker } from "../hooks/backend.js";
|
||||||
import { bankUiSettings } from "../settings.js";
|
import { bankUiSettings } from "../settings.js";
|
||||||
import { undefinedIfEmpty } from "../utils.js";
|
import { undefinedIfEmpty } from "../utils.js";
|
||||||
|
import { ErrorBannerFloat } from "./BankFrame.js";
|
||||||
import { USERNAME_REGEX } from "./RegistrationPage.js";
|
import { USERNAME_REGEX } from "./RegistrationPage.js";
|
||||||
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
||||||
|
|
||||||
@ -31,6 +37,8 @@ export function LoginForm({ onRegister }: { onRegister: () => void }): VNode {
|
|||||||
const [username, setUsername] = useState<string | undefined>();
|
const [username, setUsername] = useState<string | undefined>();
|
||||||
const [password, setPassword] = useState<string | undefined>();
|
const [password, setPassword] = useState<string | undefined>();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const testLogin = useCredentialsChecker();
|
||||||
|
const [error, saveError] = useState<ErrorMessage | undefined>();
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
useEffect(function focusInput() {
|
useEffect(function focusInput() {
|
||||||
ref.current?.focus();
|
ref.current?.focus();
|
||||||
@ -48,6 +56,9 @@ export function LoginForm({ onRegister }: { onRegister: () => void }): VNode {
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
|
<h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
|
||||||
|
{error && (
|
||||||
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
||||||
|
)}
|
||||||
<div class="login-div">
|
<div class="login-div">
|
||||||
<form
|
<form
|
||||||
class="login-form"
|
class="login-form"
|
||||||
@ -105,10 +116,41 @@ export function LoginForm({ onRegister }: { onRegister: () => void }): VNode {
|
|||||||
type="submit"
|
type="submit"
|
||||||
class="pure-button pure-button-primary"
|
class="pure-button pure-button-primary"
|
||||||
disabled={!!errors}
|
disabled={!!errors}
|
||||||
onClick={(e) => {
|
onClick={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!username || !password) return;
|
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);
|
setUsername(undefined);
|
||||||
setPassword(undefined);
|
setPassword(undefined);
|
||||||
}}
|
}}
|
||||||
|
Loading…
Reference in New Issue
Block a user