198 lines
7.1 KiB
TypeScript
198 lines
7.1 KiB
TypeScript
/*
|
|
This file is part of GNU Taler
|
|
(C) 2022 Taler Systems S.A.
|
|
|
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
|
terms of the GNU General Public License as published by the Free Software
|
|
Foundation; either version 3, or (at your option) any later version.
|
|
|
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
import {
|
|
HttpStatusCode,
|
|
Logger,
|
|
WithdrawUriResult,
|
|
} from "@gnu-taler/taler-util";
|
|
import {
|
|
RequestError,
|
|
useTranslationContext,
|
|
} from "@gnu-taler/web-util/browser";
|
|
import { Fragment, VNode, h } from "preact";
|
|
import { useMemo, useState } from "preact/hooks";
|
|
import { useAccessAnonAPI } from "../hooks/access.js";
|
|
import { notifyError } from "../hooks/notification.js";
|
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
|
|
|
const logger = new Logger("WithdrawalConfirmationQuestion");
|
|
|
|
interface Props {
|
|
onConfirmed: () => void;
|
|
onAborted: () => void;
|
|
withdrawUri: WithdrawUriResult;
|
|
}
|
|
/**
|
|
* Additional authentication required to complete the operation.
|
|
* Not providing a back button, only abort.
|
|
*/
|
|
export function WithdrawalConfirmationQuestion({
|
|
onConfirmed,
|
|
onAborted,
|
|
withdrawUri,
|
|
}: Props): VNode {
|
|
const { i18n } = useTranslationContext();
|
|
|
|
const captchaNumbers = useMemo(() => {
|
|
return {
|
|
a: Math.floor(Math.random() * 10),
|
|
b: Math.floor(Math.random() * 10),
|
|
};
|
|
}, []);
|
|
|
|
const { confirmWithdrawal, abortWithdrawal } = useAccessAnonAPI();
|
|
const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
|
|
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,
|
|
});
|
|
return (
|
|
<Fragment>
|
|
<h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
|
|
<article>
|
|
<div class="challenge-div">
|
|
<form
|
|
class="challenge-form"
|
|
noValidate
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
}}
|
|
autoCapitalize="none"
|
|
autoCorrect="off"
|
|
>
|
|
<div class="pure-form" id="captcha" name="capcha-form">
|
|
<h2>{i18n.str`Authorize withdrawal by solving challenge`}</h2>
|
|
<p>
|
|
<label for="answer">
|
|
{i18n.str`What is`}
|
|
<em>
|
|
{captchaNumbers.a} + {captchaNumbers.b}
|
|
</em>
|
|
?
|
|
</label>
|
|
|
|
<input
|
|
name="answer"
|
|
id="answer"
|
|
value={captchaAnswer ?? ""}
|
|
type="text"
|
|
autoFocus
|
|
required
|
|
onInput={(e): void => {
|
|
setCaptchaAnswer(e.currentTarget.value);
|
|
}}
|
|
/>
|
|
<ShowInputErrorLabel
|
|
message={errors?.answer}
|
|
isDirty={captchaAnswer !== undefined}
|
|
/>
|
|
</p>
|
|
<p>
|
|
<button
|
|
type="submit"
|
|
class="pure-button pure-button-primary btn-confirm"
|
|
disabled={!!errors}
|
|
onClick={async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
await confirmWithdrawal(
|
|
withdrawUri.withdrawalOperationId,
|
|
);
|
|
onConfirmed();
|
|
} catch (error) {
|
|
if (error instanceof RequestError) {
|
|
notifyError(
|
|
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({
|
|
title: i18n.str`Operation failed, please report`,
|
|
description:
|
|
error instanceof Error
|
|
? error.message
|
|
: JSON.stringify(error),
|
|
});
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
{i18n.str`Confirm`}
|
|
</button>
|
|
|
|
<button
|
|
class="pure-button pure-button-secondary btn-cancel"
|
|
onClick={async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
await abortWithdrawal(withdrawUri.withdrawalOperationId);
|
|
onAborted();
|
|
} catch (error) {
|
|
if (error instanceof RequestError) {
|
|
notifyError(
|
|
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({
|
|
title: i18n.str`Operation failed, please report`,
|
|
description:
|
|
error instanceof Error
|
|
? error.message
|
|
: JSON.stringify(error),
|
|
});
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
{i18n.str`Cancel`}
|
|
</button>
|
|
</p>
|
|
</div>
|
|
</form>
|
|
<div class="hint">
|
|
<p>
|
|
<i18n.Translate>
|
|
A this point, a <b>real</b> bank would ask for an additional
|
|
authentication proof (PIN/TAN, one time password, ..), instead
|
|
of a simple calculation.
|
|
</i18n.Translate>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</Fragment>
|
|
);
|
|
}
|