summaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-12-09 12:15:15 -0300
committerSebastian <sebasjm@gmail.com>2022-12-09 12:15:15 -0300
commit5fc8f95a5d4ce8dea03b2dbec7eb5c37e7ff3f15 (patch)
treed3153d7f8745264d378d00119385caa67f06aaab /packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
parent6b6f80466ee07f591203c28a724ce4e7128af85d (diff)
downloadwallet-core-5fc8f95a5d4ce8dea03b2dbec7eb5c37e7ff3f15.tar.gz
wallet-core-5fc8f95a5d4ce8dea03b2dbec7eb5c37e7ff3f15.tar.bz2
wallet-core-5fc8f95a5d4ce8dea03b2dbec7eb5c37e7ff3f15.zip
simplify directories
Diffstat (limited to 'packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx')
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx327
1 files changed, 327 insertions, 0 deletions
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
new file mode 100644
index 000000000..8cfdd4e9f
--- /dev/null
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -0,0 +1,327 @@
+/*
+ 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 { Logger } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { StateUpdater } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { PageStateType, usePageContext } from "../context/pageState.js";
+import {
+ InternationalizationAPI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { BackendState } from "../hooks/backend.js";
+import { prepareHeaders } from "../utils.js";
+
+const logger = new Logger("WithdrawalConfirmationQuestion");
+
+/**
+ * Additional authentication required to complete the operation.
+ * Not providing a back button, only abort.
+ */
+export function WithdrawalConfirmationQuestion(): VNode {
+ const { pageState, pageStateSetter } = usePageContext();
+ const backend = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const captchaNumbers = {
+ a: Math.floor(Math.random() * 10),
+ b: Math.floor(Math.random() * 10),
+ };
+ let captchaAnswer = "";
+
+ return (
+ <Fragment>
+ <h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
+ <article>
+ <div class="challenge-div">
+ <form class="challenge-form" noValidate>
+ <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`}&nbsp;
+ <em>
+ {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
+ </em>
+ ?&nbsp;
+ </label>
+ &nbsp;
+ <input
+ name="answer"
+ id="answer"
+ type="text"
+ autoFocus
+ required
+ onInput={(e): void => {
+ captchaAnswer = e.currentTarget.value;
+ }}
+ />
+ </p>
+ <p>
+ <button
+ class="pure-button pure-button-primary btn-confirm"
+ onClick={(e) => {
+ e.preventDefault();
+ if (
+ captchaAnswer ==
+ (captchaNumbers.a + captchaNumbers.b).toString()
+ ) {
+ confirmWithdrawalCall(
+ backend.state,
+ pageState.withdrawalId,
+ pageStateSetter,
+ i18n,
+ );
+ return;
+ }
+ pageStateSetter((prevState: PageStateType) => ({
+ ...prevState,
+
+ error: {
+ title: i18n.str`Answer is wrong.`,
+ },
+ }));
+ }}
+ >
+ {i18n.str`Confirm`}
+ </button>
+ &nbsp;
+ <button
+ class="pure-button pure-button-secondary btn-cancel"
+ onClick={async () =>
+ await abortWithdrawalCall(
+ backend.state,
+ pageState.withdrawalId,
+ pageStateSetter,
+ i18n,
+ )
+ }
+ >
+ {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>
+ );
+}
+
+/**
+ * This function confirms a withdrawal operation AFTER
+ * the wallet has given the exchange's payment details
+ * to the bank (via the Integration API). Such details
+ * can be given by scanning a QR code or by passing the
+ * raw taler://withdraw-URI to the CLI wallet.
+ *
+ * This function will set the confirmation status in the
+ * 'page state' and let the related components refresh.
+ */
+async function confirmWithdrawalCall(
+ backendState: BackendState,
+ withdrawalId: string | undefined,
+ pageStateSetter: StateUpdater<PageStateType>,
+ i18n: InternationalizationAPI,
+): Promise<void> {
+ if (backendState.status === "loggedOut") {
+ logger.error("No credentials found.");
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: i18n.str`No credentials found.`,
+ },
+ }));
+ return;
+ }
+ if (typeof withdrawalId === "undefined") {
+ logger.error("No withdrawal ID found.");
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: i18n.str`No withdrawal ID found.`,
+ },
+ }));
+ return;
+ }
+ let res: Response;
+ try {
+ const { username, password } = backendState;
+ const headers = prepareHeaders(username, password);
+ /**
+ * NOTE: tests show that when a same object is being
+ * POSTed, caching might prevent same requests from being
+ * made. Hence, trying to POST twice the same amount might
+ * get silently ignored.
+ *
+ * headers.append("cache-control", "no-store");
+ * headers.append("cache-control", "no-cache");
+ * headers.append("pragma", "no-cache");
+ * */
+
+ // Backend URL must have been stored _with_ a final slash.
+ const url = new URL(
+ `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/confirm`,
+ backendState.url,
+ );
+ res = await fetch(url.href, {
+ method: "POST",
+ headers,
+ });
+ } catch (error) {
+ logger.error("Could not POST withdrawal confirmation to the bank", error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: i18n.str`Could not confirm the withdrawal`,
+ description: (error as any).error.description,
+ debug: JSON.stringify(error),
+ },
+ }));
+ return;
+ }
+ if (!res || !res.ok) {
+ const response = await res.json();
+ // assume not ok if res is null
+ logger.error(
+ `Withdrawal confirmation gave response error (${res.status})`,
+ res.statusText,
+ );
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: i18n.str`Withdrawal confirmation gave response error`,
+ debug: JSON.stringify(response),
+ },
+ }));
+ return;
+ }
+ logger.trace("Withdrawal operation confirmed!");
+ pageStateSetter((prevState) => {
+ const { talerWithdrawUri, ...rest } = prevState;
+ return {
+ ...rest,
+
+ info: i18n.str`Withdrawal confirmed!`,
+ };
+ });
+}
+
+/**
+ * Abort a withdrawal operation via the Access API's /abort.
+ */
+async function abortWithdrawalCall(
+ backendState: BackendState,
+ withdrawalId: string | undefined,
+ pageStateSetter: StateUpdater<PageStateType>,
+ i18n: InternationalizationAPI,
+): Promise<void> {
+ if (backendState.status === "loggedOut") {
+ logger.error("No credentials found.");
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: i18n.str`No credentials found.`,
+ },
+ }));
+ return;
+ }
+ if (typeof withdrawalId === "undefined") {
+ logger.error("No withdrawal ID found.");
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: i18n.str`No withdrawal ID found.`,
+ },
+ }));
+ return;
+ }
+ let res: Response;
+ try {
+ const { username, password } = backendState;
+ const headers = prepareHeaders(username, password);
+ /**
+ * NOTE: tests show that when a same object is being
+ * POSTed, caching might prevent same requests from being
+ * made. Hence, trying to POST twice the same amount might
+ * get silently ignored. Needs more observation!
+ *
+ * headers.append("cache-control", "no-store");
+ * headers.append("cache-control", "no-cache");
+ * headers.append("pragma", "no-cache");
+ * */
+
+ // Backend URL must have been stored _with_ a final slash.
+ const url = new URL(
+ `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/abort`,
+ backendState.url,
+ );
+ res = await fetch(url.href, { method: "POST", headers });
+ } catch (error) {
+ logger.error("Could not abort the withdrawal", error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: i18n.str`Could not abort the withdrawal.`,
+ description: (error as any).error.description,
+ debug: JSON.stringify(error),
+ },
+ }));
+ return;
+ }
+ if (!res.ok) {
+ const response = await res.json();
+ logger.error(
+ `Withdrawal abort gave response error (${res.status})`,
+ res.statusText,
+ );
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: i18n.str`Withdrawal abortion failed.`,
+ description: response.error.description,
+ debug: JSON.stringify(response),
+ },
+ }));
+ return;
+ }
+ logger.trace("Withdrawal operation aborted!");
+ pageStateSetter((prevState) => {
+ const { ...rest } = prevState;
+ return {
+ ...rest,
+
+ info: i18n.str`Withdrawal aborted!`,
+ };
+ });
+}