aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'packages/demobank-ui/src/pages')
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/index.ts2
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/state.ts3
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/views.tsx48
-rw-r--r--packages/demobank-ui/src/pages/OperationState/index.ts4
-rw-r--r--packages/demobank-ui/src/pages/OperationState/state.ts14
-rw-r--r--packages/demobank-ui/src/pages/OperationState/views.tsx71
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx15
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx23
-rw-r--r--packages/demobank-ui/src/pages/SolveChallengePage.tsx553
-rw-r--r--packages/demobank-ui/src/pages/WalletWithdrawForm.tsx3
-rw-r--r--packages/demobank-ui/src/pages/WireTransfer.tsx8
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx211
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx3
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx28
-rw-r--r--packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx5
-rw-r--r--packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx19
-rw-r--r--packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx21
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx104
-rw-r--r--packages/demobank-ui/src/pages/admin/AdminHome.tsx17
-rw-r--r--packages/demobank-ui/src/pages/admin/RemoveAccount.tsx16
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx38
-rw-r--r--packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx5
22 files changed, 897 insertions, 314 deletions
diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts
index 7261af69a..cfe184612 100644
--- a/packages/demobank-ui/src/pages/AccountPage/index.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/index.ts
@@ -23,6 +23,7 @@ import { InvalidIbanView, ReadyView } from "./views.js";
export interface Props {
account: string;
+ onAuthorizationRequired: () => void;
goToConfirmOperation: (id: string) => void;
}
@@ -48,6 +49,7 @@ export namespace State {
error: undefined;
account: string,
limit: AmountJson,
+ onAuthorizationRequired: () => void;
goToConfirmOperation: (id: string) => void;
}
diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts
index 88e8cf747..38b4d9f36 100644
--- a/packages/demobank-ui/src/pages/AccountPage/state.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/state.ts
@@ -20,7 +20,7 @@ import { useAccountDetails } from "../../hooks/access.js";
import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { Props, State } from "./index.js";
-export function useComponentState({ account, goToConfirmOperation }: Props): State {
+export function useComponentState({ account, goToConfirmOperation, onAuthorizationRequired }: Props): State {
const result = useAccountDetails(account);
const { i18n } = useTranslationContext();
@@ -78,6 +78,7 @@ export function useComponentState({ account, goToConfirmOperation }: Props): Sta
status: "ready",
goToConfirmOperation,
error: undefined,
+ onAuthorizationRequired,
account,
limit,
};
diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx
index d760543c6..59a6db7b9 100644
--- a/packages/demobank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx
@@ -14,15 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { TalerError, TranslatedString } from "@gnu-taler/taler-util";
+import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { Attention } from "@gnu-taler/web-util/browser";
import { Transactions } from "../../components/Transactions/index.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { useOnePendingCashouts } from "../../hooks/circuit.js";
import { usePreferences } from "../../hooks/preferences.js";
import { PaymentOptions } from "../PaymentOptions.js";
import { State } from "./index.js";
-import { useCashouts, useOnePendingCashouts } from "../../hooks/circuit.js";
-import { TalerError } from "@gnu-taler/taler-util";
export function InvalidIbanView({ error }: State.InvalidIban) {
return (
@@ -55,27 +55,35 @@ function ShowDemoInfo(): VNode {
</Attention>
}
-export function ReadyView({ account, limit, goToConfirmOperation }: State.Ready): VNode<{}> {
+function ShowPedingOperation(): VNode {
+ const { i18n } = useTranslationContext();
+ const [bankState, updateBankState] = useBankState();
+ if (!bankState.currentChallenge) return <Fragment />;
+ const title = ((op): TranslatedString => {
+ switch (op) {
+ case "delete-account": return i18n.str`Pending account delete operation`
+ case "update-account": return i18n.str`Pending account update operation`
+ case "update-password": return i18n.str`Pending password update operation`
+ case "create-transaction": return i18n.str`Pending transaction operation`
+ case "confirm-withdrawal": return i18n.str`Pending withdrawal operation`
+ case "create-cashout": return i18n.str`Pending cashout operation`
+ }
+ })(bankState.currentChallenge.operation)
+ return <Attention title={title} type="warning" onClose={() => { updateBankState("currentChallenge", undefined); }}>
+ <i18n.Translate>
+ To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/2fa`}>here</a>
+ </i18n.Translate>
+ </Attention>
+}
+
+export function ReadyView({ account, limit, goToConfirmOperation, onAuthorizationRequired }: State.Ready): VNode<{}> {
return <Fragment>
+ <ShowPedingOperation />
<ShowDemoInfo />
- <PendingCashouts account={account}/>
- <PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} />
+ <PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} onAuthorizationRequired={onAuthorizationRequired} />
<Transactions account={account} />
</Fragment>;
}
-function PendingCashouts({account}: {account: string}):VNode {
- const { i18n } = useTranslationContext();
- const result = useOnePendingCashouts(account)
- if (!result || result instanceof TalerError || result.type !== "ok" || !result.body) {
- return <Fragment />
- }
-
- return <Attention title={i18n.str`You have pending cashout operation to complete`} >
- <i18n.Translate>
- Cashout with subject "{result.body.subject}", look for the code and complete the operation <a target="_blank" rel="noreferrer noopener" class="font-semibold text-blue-700 hover:text-blue-600" href={`#/cashout/${result.body.id}`}>here</a>.
- </i18n.Translate>
- </Attention>
-} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts
index e3aec21c5..53d07e44b 100644
--- a/packages/demobank-ui/src/pages/OperationState/index.ts
+++ b/packages/demobank-ui/src/pages/OperationState/index.ts
@@ -22,6 +22,7 @@ import { AbortedView, ConfirmedView, FailedView, InvalidPaytoView, InvalidReserv
export interface Props {
currency: string;
+ onAuthorizationRequired: () => void,
onClose: () => void;
}
@@ -82,11 +83,12 @@ export namespace State {
}
export interface NeedConfirmation {
status: "need-confirmation",
+ onAuthorizationRequired: () => void,
account: string,
onAbort: undefined | (() => Promise<TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined>);
onConfirm: undefined | (() => Promise<TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined>);
error: undefined;
- busy: boolean,
+ id: string,
}
export interface Aborted {
status: "aborted",
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts
index b214a400d..fbf43867f 100644
--- a/packages/demobank-ui/src/pages/OperationState/state.ts
+++ b/packages/demobank-ui/src/pages/OperationState/state.ts
@@ -14,26 +14,25 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, FailCasesByMethod, HttpStatusCode, TalerCoreBankErrorsByMethod, TalerError, TalerErrorDetail, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
-import { notify, notifyError, notifyInfo, useTranslationContext, utils } from "@gnu-taler/web-util/browser";
+import { Amounts, HttpStatusCode, TalerCoreBankErrorsByMethod, TalerError, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { utils } from "@gnu-taler/web-util/browser";
import { useEffect, useState } from "preact/hooks";
import { mutate } from "swr";
import { useBankCoreApiContext } from "../../context/config.js";
import { useWithdrawalDetails } from "../../hooks/access.js";
import { useBackendState } from "../../hooks/backend.js";
+import { useBankState } from "../../hooks/bank-state.js";
import { usePreferences } from "../../hooks/preferences.js";
import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { Props, State } from "./index.js";
-import { useBankState } from "../../hooks/bank-state.js";
-export function useComponentState({ currency, onClose }: Props): utils.RecursiveState<State> {
+export function useComponentState({ currency, onClose, onAuthorizationRequired, }: Props): utils.RecursiveState<State> {
const [settings] = usePreferences()
const [bankState, updateBankState] = useBankState();
const { state: credentials } = useBackendState()
const creds = credentials.status !== "loggedIn" ? undefined : credentials
const { api } = useBankCoreApiContext()
- const [busy, setBusy] = useState<Record<string, undefined>>()
const [failure, setFailure] = useState<TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined>()
const amount = settings.maxWithdrawalAmount
@@ -88,9 +87,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
async function doConfirm(): Promise<TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined> {
if (!creds) return;
- setBusy({})
const resp = await api.confirmWithdrawalById(creds, wid);
- setBusy(undefined)
if (resp.type === "ok") {
mutate(() => true)//clean withdrawal state
} else {
@@ -213,9 +210,10 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
return {
status: "need-confirmation",
error: undefined,
+ onAuthorizationRequired,
account: data.username,
+ id: withdrawalOperationId,
onAbort: !creds ? undefined : doAbort,
- busy: !!busy,
onConfirm: !creds ? undefined : doConfirm
}
}
diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx
index 5ebd66dac..0ebdeea47 100644
--- a/packages/demobank-ui/src/pages/OperationState/views.tsx
+++ b/packages/demobank-ui/src/pages/OperationState/views.tsx
@@ -14,17 +14,16 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpStatusCode, TalerErrorCode, TranslatedString, stringifyWithdrawUri } from "@gnu-taler/taler-util";
-import { Attention, LocalNotificationBanner, ShowInputErrorLabel, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { AbsoluteTime, HttpStatusCode, TalerErrorCode, TranslatedString, stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { Attention, LocalNotificationBanner, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { useEffect, useMemo, useState } from "preact/hooks";
+import { useEffect } from "preact/hooks";
import { QR } from "../../components/QR.js";
+import { useBankState } from "../../hooks/bank-state.js";
import { usePreferences } from "../../hooks/preferences.js";
-import { undefinedIfEmpty } from "../../utils.js";
import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js";
import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { State } from "./index.js";
-import { useBankState } from "../../hooks/bank-state.js";
export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) {
return (
@@ -42,30 +41,12 @@ export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) {
);
}
-export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doConfirm, busy, account }: State.NeedConfirmation) {
+export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doConfirm, account, id, onAuthorizationRequired, }: State.NeedConfirmation) {
const { i18n } = useTranslationContext()
const [settings] = usePreferences()
const [notification, notify, errorHandler] = useLocalNotification()
const [, updateBankState] = useBankState()
- const captchaNumbers = useMemo(() => {
- return {
- a: Math.floor(Math.random() * 10),
- b: Math.floor(Math.random() * 10),
- };
- }, []);
- 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,
- }) ?? (busy ? {} as Record<string, undefined> : undefined);
-
async function onCancel() {
errorHandler(async () => {
if (!doAbort) return;
@@ -137,11 +118,13 @@ export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doCon
debug: resp.detail,
});
case HttpStatusCode.Accepted: {
- updateBankState("currentChallengeId", resp.body.challenge_id)
- return notify({
- type: "info",
- title: i18n.str`The operation needs a confirmation to complete.`,
- });
+ updateBankState("currentChallenge", {
+ operation: "confirm-withdrawal",
+ id: String(resp.body.challenge_id),
+ sent: AbsoluteTime.never(),
+ request: id,
+ })
+ return onAuthorizationRequired()
}
default: assertUnreachable(resp)
}
@@ -165,35 +148,6 @@ export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doCon
e.preventDefault()
}}
>
- <div class="px-4 py-6 sm:p-8">
- <label for="withdraw-amount">{i18n.str`What is`}&nbsp;
- <em>
- {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
- </em>
- ?
- </label>
- <div class="mt-2">
- <div class="relative rounded-md shadow-sm">
- <input
- type="text"
- // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- aria-describedby="answer"
- autoFocus
- class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- value={captchaAnswer ?? ""}
- required
-
- name="answer"
- id="answer"
- autocomplete="off"
- onChange={(e): void => {
- setCaptchaAnswer(e.currentTarget.value)
- }}
- />
- </div>
- <ShowInputErrorLabel message={errors?.answer} isDirty={captchaAnswer !== undefined} />
- </div>
- </div>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
onClick={(e) => {
@@ -204,7 +158,6 @@ export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doCon
<i18n.Translate>Cancel</i18n.Translate></button>
<button type="submit"
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- disabled={!!errors}
onClick={(e) => {
e.preventDefault()
onConfirm()
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index 1a431a939..06d293097 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -16,18 +16,21 @@
import { AmountJson } from "@gnu-taler/taler-util";
import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { PaytoWireTransferForm, doAutoFocus } from "./PaytoWireTransferForm.js";
-import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
-import { usePreferences } from "../hooks/preferences.js";
import { useBankState } from "../hooks/bank-state.js";
+import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
+import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
/**
* Let the user choose a payment option,
* then specify the details trigger the action.
*/
-export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJson, goToConfirmOperation: (id: string) => void }): VNode {
+export function PaymentOptions({ limit, goToConfirmOperation, onAuthorizationRequired }: {
+ limit: AmountJson,
+ onAuthorizationRequired: () => void,
+ goToConfirmOperation: (id: string) => void,
+}): VNode {
const { i18n } = useTranslationContext();
const [bankState] = useBankState();
@@ -96,6 +99,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
<WalletWithdrawForm
focus
limit={limit}
+ onAuthorizationRequired={onAuthorizationRequired}
goToConfirmOperation={goToConfirmOperation}
onCancel={() => {
setTab(undefined)
@@ -107,6 +111,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
focus
title={i18n.str`Transfer details`}
limit={limit}
+ onAuthorizationRequired={onAuthorizationRequired}
onSuccess={() => {
notifyInfo(i18n.str`Wire transfer created!`);
setTab(undefined)
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 0c6c9ada2..f7b81be48 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -15,6 +15,7 @@
*/
import {
+ AbsoluteTime,
AmountJson,
AmountLike,
AmountString,
@@ -57,12 +58,14 @@ export function PaytoWireTransferForm({
toAccount,
onSuccess,
onCancel,
+ onAuthorizationRequired,
limit,
}: {
title: TranslatedString,
focus?: boolean;
toAccount?: string,
onSuccess: () => void;
+ onAuthorizationRequired: () => void;
onCancel: (() => void) | undefined;
limit: AmountJson;
}): VNode {
@@ -146,12 +149,14 @@ export function PaytoWireTransferForm({
sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString
}
const puri = payto_uri;
+ const sAmount = sendingAmount;
await handleError(async () => {
- const resp = await api.createTransaction(credentials, {
+ const request = {
payto_uri: puri,
- amount: sendingAmount,
- });
+ amount: sAmount,
+ }
+ const resp = await api.createTransaction(credentials, request);
mutate(() => true)
if (resp.type === "fail") {
switch (resp.case) {
@@ -192,11 +197,13 @@ export function PaytoWireTransferForm({
debug: resp.detail,
})
case HttpStatusCode.Accepted: {
- updateBankState("currentChallengeId", resp.body.challenge_id)
- return notify({
- type: "info",
- title: i18n.str`The operation needs a confirmation to complete.`,
- });
+ updateBankState("currentChallenge", {
+ operation: "create-transaction",
+ id: String(resp.body.challenge_id),
+ sent: AbsoluteTime.never(),
+ request,
+ })
+ return onAuthorizationRequired()
}
default: assertUnreachable(resp)
}
diff --git a/packages/demobank-ui/src/pages/SolveChallengePage.tsx b/packages/demobank-ui/src/pages/SolveChallengePage.tsx
new file mode 100644
index 000000000..e55038df5
--- /dev/null
+++ b/packages/demobank-ui/src/pages/SolveChallengePage.tsx
@@ -0,0 +1,553 @@
+/*
+ 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 {
+ AbsoluteTime,
+ Amounts,
+ HttpStatusCode,
+ Logger,
+ TalerCorebankApi,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+ parsePaytoUri
+} from "@gnu-taler/taler-util";
+import {
+ Loading,
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ useLocalNotification,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { useBankCoreApiContext } from "../context/config.js";
+import { useWithdrawalDetails } from "../hooks/access.js";
+import { useBackendState } from "../hooks/backend.js";
+import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js";
+import { useConversionInfo } from "../hooks/circuit.js";
+import { undefinedIfEmpty } from "../utils.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
+import { OperationNotFound } from "./WithdrawalQRCode.js";
+
+const logger = new Logger("SolveChallenge");
+
+export function SolveChallengePage({
+ onContinue,
+}: {
+ onContinue: () => void;
+}): VNode {
+ const { api } = useBankCoreApiContext()
+ const { i18n } = useTranslationContext();
+ const [bankState, updateBankState] = useBankState();
+ const [code, setCode] = useState<string | undefined>(undefined);
+ const [notification, notify, handleError] = useLocalNotification()
+ const { state } = useBackendState();
+ const creds = state.status !== "loggedIn" ? undefined : state
+
+ if (!bankState.currentChallenge) {
+ return <div>
+ <span>no challenge to solve </span>
+ <button type="button"
+ class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
+ onClick={() => {
+ onContinue()
+ }}
+ >
+ <i18n.Translate>Continue</i18n.Translate>
+ </button>
+ </div>
+ }
+
+ const ch = bankState.currentChallenge
+ const errors = undefinedIfEmpty({
+ code: !code ? i18n.str`required` : undefined,
+ });
+
+ async function startChallenge() {
+ if (!creds) return;
+ await handleError(async () => {
+ const resp = await api.sendChallenge(creds, ch.id);
+ if (resp.type === "ok") {
+ const newCh = structuredClone(ch)
+ newCh.sent = AbsoluteTime.now()
+ newCh.info = resp.body
+ updateBankState("currentChallenge", newCh)
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.NotFound: return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case HttpStatusCode.Unauthorized: return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ default: assertUnreachable(resp)
+ }
+ }
+ })
+ }
+
+ async function completeChallenge() {
+ if (!creds || !code) return;
+ await handleError(async () => {
+ {
+ const resp = await api.confirmChallenge(creds, ch.id, {
+ tan: code
+ });
+ if (resp.type === "fail") {
+ setCode("")
+ switch (resp.case) {
+ case HttpStatusCode.NotFound: return notify({
+ type: "error",
+ title: i18n.str`Challenge not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case HttpStatusCode.Unauthorized: return notify({
+ type: "error",
+ title: i18n.str`This user is not authorized to complete this challenge.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case HttpStatusCode.TooManyRequests: return notify({
+ type: "error",
+ title: i18n.str`Too many attemps, try another code.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED: return notify({
+ type: "error",
+ title: i18n.str`The confirmation code is wrong, try again.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED: return notify({
+ type: "error",
+ title: i18n.str`The operation expired.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ default: assertUnreachable(resp)
+ }
+ }
+ }
+ {
+ const resp = await (async (ch: ChallengeInProgess) => {
+ switch (ch.operation) {
+ case "delete-account": return await api.deleteAccount(creds, ch.id)
+ case "update-account": return await api.updateAccount(creds, ch.request, ch.id)
+ case "update-password": return await api.updatePassword(creds, ch.request, ch.id)
+ case "create-transaction": return await api.createTransaction(creds, ch.request, ch.id)
+ case "confirm-withdrawal": return await api.confirmWithdrawalById(creds, ch.request, ch.id)
+ case "create-cashout": return await api.createCashout(creds, ch.request, ch.id)
+ default: assertUnreachable(ch)
+ }
+ })(ch);
+
+ if (resp.type === "fail") {
+ if (resp.case !== HttpStatusCode.Accepted) {
+ return notify({
+ type: "error",
+ title: i18n.str`The operation failed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ }
+ // another challenge required
+ updateBankState("currentChallenge", {
+ operation: ch.operation,
+ id: String(resp.body.challenge_id),
+ sent: AbsoluteTime.never(),
+ request: ch.request as any,
+ })
+ return notify({
+ type: "info",
+ title: i18n.str`The operation needs another confirmation to complete.`,
+ })
+ }
+ updateBankState("currentChallenge", undefined)
+ return onContinue()
+ }
+ })
+ }
+
+ const subtitle = ((op): TranslatedString => {
+ switch (op) {
+ case "delete-account": return i18n.str`Account delete`
+ case "update-account": return i18n.str`Account update`
+ case "update-password": return i18n.str`Password update`
+ case "create-transaction": return i18n.str`Wire transfer`
+ case "confirm-withdrawal": return i18n.str`Withdrawal`
+ case "create-cashout": return i18n.str`Cashout`
+ }
+ })(ch.operation)
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <span class="text-sm text-black font-semibold leading-6 " id="availability-label">
+ <i18n.Translate>Confirm the operation</i18n.Translate>
+ </span>
+ </h2>
+ <span>
+ {subtitle}
+ </span>
+ </div>
+
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
+ <ChallengeDetails challenge={bankState.currentChallenge} onStart={startChallenge} />
+ {ch.info &&
+ <div class="mt-3 text-sm leading-6">
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={e => {
+ e.preventDefault()
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ <label for="withdraw-amount">
+ <i18n.Translate>Enter the confirmation code</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <div class="relative rounded-md shadow-sm">
+ <input
+ type="text"
+ // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ aria-describedby="answer"
+ autoFocus
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={code ?? ""}
+ required
+
+ name="answer"
+ id="answer"
+ autocomplete="off"
+ onChange={(e): void => {
+ setCode(e.currentTarget.value)
+ }}
+ />
+ </div>
+ <ShowInputErrorLabel message={errors?.code} isDirty={code !== undefined} />
+ </div>
+ </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <button type="button"
+ class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
+ onClick={() => {
+ updateBankState("currentChallenge", undefined)
+ onContinue()
+ }}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button type="submit"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ disabled={!!errors}
+ onClick={(e) => {
+ completeChallenge()
+ }}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ </form>
+
+ {/* <ShouldBeSameUser username={details.username}> */}
+ {/* </ShouldBeSameUser> */}
+ </div>
+ }
+ </div>
+ </div>
+ </Fragment>
+
+ );
+}
+
+function ChallengeDetails({ challenge, onStart }: { challenge: ChallengeInProgess, onStart: () => void }): VNode {
+ const { i18n } = useTranslationContext();
+ const { config } = useBankCoreApiContext();
+
+ return <div class="px-4 mt-4 ">
+ <div class="w-full">
+ <div class="flex justify-center">
+
+ {challenge.info ?
+ <button type="submit"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ onClick={(e) => {
+ onStart()
+ }}
+ >
+ <i18n.Translate>Send again</i18n.Translate>
+ </button>
+ :
+ <button type="submit"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ onClick={(e) => {
+ onStart()
+ }}
+ >
+ <i18n.Translate>Send code</i18n.Translate>
+ </button>
+ }
+ </div>
+ <div class="mt-6 border-t border-gray-100">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <span class="text-sm text-black font-semibold leading-6 " id="availability-label">
+ <i18n.Translate>Operation details</i18n.Translate>
+ </span>
+ </h2>
+ <dl class="divide-y divide-gray-100">
+ {((): VNode => {
+ switch (challenge.operation) {
+ case "delete-account": return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{challenge.request}</dd>
+ </div>
+ case "create-transaction": {
+ const payto = parsePaytoUri(challenge.request.payto_uri)!
+ return <Fragment>
+ {challenge.request.amount &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount value={Amounts.parseOrThrow(challenge.request.amount)} spec={config.currency_specification} />
+ </dd>
+ </div>
+ }
+ {payto.isKnown && payto.targetType === "iban" &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">To account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {payto.iban}
+ </dd>
+ </div>
+ }
+ </Fragment>
+ }
+ case "confirm-withdrawal": return <ShowWithdrawalDetails id={challenge.request} />
+ case "create-cashout": {
+ return <ShowCashoutDetails request={challenge.request} />
+ }
+ case "update-account": {
+ return <Fragment>
+ {challenge.request.cashout_payto_uri !== undefined &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Cashout account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.cashout_payto_uri}
+ </dd>
+ </div>
+ }
+ {challenge.request.contact_data?.email !== undefined &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Email</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.contact_data?.email}
+ </dd>
+ </div>
+ }
+ {challenge.request.contact_data?.phone !== undefined &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Phone</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.contact_data?.phone}
+ </dd>
+ </div>
+ }
+ {challenge.request.debit_threshold !== undefined &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Debit threshold</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount value={Amounts.parseOrThrow(challenge.request.debit_threshold)} spec={config.currency_specification} />
+ </dd>
+ </div>
+ }
+ {challenge.request.is_public !== undefined &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Is public</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.is_public ? "enable" : "disable"}
+ </dd>
+ </div>
+ }
+ {challenge.request.name !== undefined &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Name</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.name}
+ </dd>
+ </div>
+ }
+ {challenge.request.tan_channel !== undefined &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Authentication channel</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.tan_channel}
+ </dd>
+ </div>
+ }
+ </Fragment>
+ }
+ case "update-password": {
+ return <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">New password</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.new_password}
+ </dd>
+ </div>
+ </Fragment>
+ }
+ default: assertUnreachable(challenge)
+ }
+ })()}
+
+ {challenge.info &&
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <span class="text-sm text-black font-semibold leading-6 " id="availability-label">
+ <i18n.Translate>Challenge details</i18n.Translate>
+ </span>
+ </h2>
+ }
+ {challenge.sent.t_ms !== "never" &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Sent at</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {format(challenge.sent.t_ms, "dd/MM/yyyy HH:mm:ss")}
+ </dd>
+ </div>
+ }
+ {challenge.info &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ {((ch: TalerCorebankApi.TanChannel): VNode => {
+ switch (ch) {
+ case TalerCorebankApi.TanChannel.SMS: return <i18n.Translate>To phone</i18n.Translate>
+ case TalerCorebankApi.TanChannel.EMAIL: return <i18n.Translate>To email</i18n.Translate>
+ default: assertUnreachable(ch)
+ }
+ })(challenge.info.tan_channel)}
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.info.tan_info}
+ </dd>
+ </div>
+ }
+
+ </dl>
+ </div>
+ </div>
+ </div>
+}
+
+function ShowWithdrawalDetails({ id }: { id: string }): VNode {
+ const { i18n } = useTranslationContext();
+ const details = useWithdrawalDetails(id)
+ const { config } = useBankCoreApiContext();
+ if (!details) {
+ return <Loading />
+ }
+ if (details instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={details} />
+ }
+ if (details.type === "fail") {
+ switch (details.case) {
+ case HttpStatusCode.BadRequest:
+ case HttpStatusCode.NotFound: return <OperationNotFound onClose={undefined} />
+ default: assertUnreachable(details)
+ }
+ }
+
+ return <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount value={Amounts.parseOrThrow(details.body.amount)} spec={config.currency_specification} />
+ </dd>
+ </div>
+ {details.body.selected_reserve_pub !== undefined &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Withdraw id</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0" title={details.body.selected_reserve_pub}>
+ {details.body.selected_reserve_pub.substring(0, 16)}...
+ </dd>
+ </div>
+ }
+ {details.body.selected_exchange_account !== undefined &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">To account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.body.selected_exchange_account}
+ </dd>
+ </div>
+ }
+ </Fragment>
+}
+
+function ShowCashoutDetails({ request }: { request: TalerCorebankApi.CashoutRequest }): VNode {
+ const { i18n } = useTranslationContext();
+ const info = useConversionInfo();
+ if (!info) {
+ return <Loading />
+ }
+
+ if (info instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={info} />
+ }
+ return <Fragment>
+ {request.subject !== undefined &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Subject</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {request.subject}
+ </dd>
+ </div>
+ }
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Debit</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount value={Amounts.parseOrThrow(request.amount_credit)} spec={info.body.regional_currency_specification} />
+ </dd>
+ </div>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Credit</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount value={Amounts.parseOrThrow(request.amount_credit)} spec={info.body.fiat_currency_specification} />
+ </dd>
+ </div>
+ </Fragment>
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index 6e13ae657..c04e85e0c 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -235,10 +235,12 @@ export function WalletWithdrawForm({
focus,
limit,
onCancel,
+ onAuthorizationRequired,
goToConfirmOperation,
}: {
limit: AmountJson;
focus?: boolean;
+ onAuthorizationRequired: () => void,
goToConfirmOperation: (operationId: string) => void;
onCancel: () => void;
}): VNode {
@@ -274,6 +276,7 @@ export function WalletWithdrawForm({
:
<OperationState
currency={limit.currency}
+ onAuthorizationRequired={onAuthorizationRequired}
onClose={onCancel}
/>
}
diff --git a/packages/demobank-ui/src/pages/WireTransfer.tsx b/packages/demobank-ui/src/pages/WireTransfer.tsx
index d6133b504..25d43a832 100644
--- a/packages/demobank-ui/src/pages/WireTransfer.tsx
+++ b/packages/demobank-ui/src/pages/WireTransfer.tsx
@@ -8,7 +8,12 @@ import { LoginForm } from "./LoginForm.js";
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
import { assertUnreachable } from "./WithdrawalOperationPage.js";
-export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { onSuccess?: () => void; toAccount?: string, onCancel?: () => void, onRegister?: () => void }): VNode {
+export function WireTransfer({ toAccount, onAuthorizationRequired, onCancel, onSuccess }: {
+ onSuccess?: () => void;
+ toAccount?: string,
+ onCancel?: () => void,
+ onAuthorizationRequired: () => void,
+}): VNode {
const { i18n } = useTranslationContext();
const r = useBackendState();
const account = r.state.status !== "loggedOut" ? r.state.username : "admin";
@@ -42,6 +47,7 @@ export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { o
title={i18n.str`Make a wire transfer`}
toAccount={toAccount}
limit={limit}
+ onAuthorizationRequired={onAuthorizationRequired}
onSuccess={() => {
notifyInfo(i18n.str`Wire transfer created!`);
if (onSuccess) onSuccess()
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 206b51008..890478f82 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -15,40 +15,33 @@
*/
import {
+ AbsoluteTime,
AmountJson,
HttpStatusCode,
Logger,
PaytoUri,
PaytoUriIBAN,
PaytoUriTalerBank,
- TalerError,
TalerErrorCode,
TranslatedString,
WithdrawUriResult
} from "@gnu-taler/taler-util";
import {
Attention,
- Loading,
LocalNotificationBanner,
- ShowInputErrorLabel,
notifyInfo,
useLocalNotification,
useTranslationContext
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, VNode, h } from "preact";
-import { useMemo, useState } from "preact/hooks";
import { mutate } from "swr";
-import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
import { useBankCoreApiContext } from "../context/config.js";
-import { useWithdrawalDetails } from "../hooks/access.js";
import { useBackendState } from "../hooks/backend.js";
+import { useBankState } from "../hooks/bank-state.js";
import { usePreferences } from "../hooks/preferences.js";
-import { undefinedIfEmpty } from "../utils.js";
import { LoginForm } from "./LoginForm.js";
import { RenderAmount } from "./PaytoWireTransferForm.js";
import { assertUnreachable } from "./WithdrawalOperationPage.js";
-import { OperationNotFound } from "./WithdrawalQRCode.js";
-import { useBankState } from "../hooks/bank-state.js";
const logger = new Logger("WithdrawalConfirmationQuestion");
@@ -60,7 +53,8 @@ interface Props {
reserve: string,
username: string,
amount: AmountJson,
- }
+ },
+ onAuthorizationRequired: () => void,
}
/**
* Additional authentication required to complete the operation.
@@ -69,52 +63,20 @@ interface Props {
export function WithdrawalConfirmationQuestion({
onAborted,
details,
+ onAuthorizationRequired,
withdrawUri,
}: Props): VNode {
const { i18n } = useTranslationContext();
const [settings] = usePreferences()
const { state: credentials } = useBackendState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials
- const withdrawalInfo = useWithdrawalDetails(withdrawUri.withdrawalOperationId)
const [, updateBankState] = useBankState()
- if (!withdrawalInfo) {
- return <Loading />
- }
- if (withdrawalInfo instanceof TalerError) {
- return <ErrorLoadingWithDebug error={withdrawalInfo} />
- }
- if (withdrawalInfo.type === "fail") {
- switch (withdrawalInfo.case) {
- case HttpStatusCode.NotFound: return <OperationNotFound onClose={onAborted} />
- case HttpStatusCode.BadRequest: return <OperationNotFound onClose={onAborted} />
- default: assertUnreachable(withdrawalInfo)
- }
- }
- const captchaNumbers = useMemo(() => {
- return {
- a: Math.floor(Math.random() * 10),
- b: Math.floor(Math.random() * 10),
- };
- }, []);
const [notification, notify, handleError] = useLocalNotification()
const { config, api } = useBankCoreApiContext()
- const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
- const answer = parseInt(captchaAnswer ?? "", 10);
- const [busy, setBusy] = useState<Record<string, undefined>>()
- 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,
- }) ?? busy;
async function doTransfer() {
- setBusy({})
await handleError(async () => {
if (!creds) return;
const resp = await api.confirmWithdrawalById(creds, withdrawUri.withdrawalOperationId);
@@ -156,21 +118,21 @@ export function WithdrawalConfirmationQuestion({
debug: resp.detail,
})
case HttpStatusCode.Accepted: {
- updateBankState("currentChallengeId", resp.body.challenge_id)
- return notify({
- type: "info",
- title: i18n.str`The operation needs a confirmation to complete.`,
- });
+ updateBankState("currentChallenge", {
+ operation: "confirm-withdrawal",
+ id: String(resp.body.challenge_id),
+ sent: AbsoluteTime.never(),
+ request: withdrawUri.withdrawalOperationId,
+ })
+ return onAuthorizationRequired()
}
default: assertUnreachable(resp)
}
}
})
- setBusy(undefined)
}
async function doCancel() {
- setBusy({})
await handleError(async () => {
if (!creds) return;
const resp = await api.abortWithdrawalById(creds, withdrawUri.withdrawalOperationId);
@@ -200,7 +162,6 @@ export function WithdrawalConfirmationQuestion({
}
}
})
- setBusy(undefined)
}
return (
@@ -215,10 +176,7 @@ export function WithdrawalConfirmationQuestion({
<div class="mt-3 text-sm leading-6">
<ShouldBeSameUser username={details.username}>
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
- <div class="px-4 sm:px-0">
- <h2 class="text-base font-semibold text-gray-900"><i18n.Translate>Answer the next question to authorize the wire transfer.</i18n.Translate></h2>
- </div>
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-2 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<form
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
autoCapitalize="none"
@@ -227,35 +185,65 @@ export function WithdrawalConfirmationQuestion({
e.preventDefault()
}}
>
- <div class="px-4 py-6 sm:p-8">
- <label for="withdraw-amount">{i18n.str`What is`}&nbsp;
- <em>
- {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
- </em>
- ?
- </label>
-
- <div class="mt-2">
- <div class="relative rounded-md shadow-sm">
- <input
- type="text"
- // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- aria-describedby="answer"
- autoFocus
- class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- value={captchaAnswer ?? ""}
- required
+ <div class="px-4 mt-4">
+ <div class="w-full">
+ <div class="px-4 sm:px-0 text-sm">
+ <p><i18n.Translate>Wire transfer details</i18n.Translate></p>
+ </div>
+ <div class="mt-6 border-t border-gray-100">
+ <dl class="divide-y divide-gray-100">
+ {((): VNode => {
+ switch (details.account.targetType) {
+ case "iban": {
+ const p = details.account as PaytoUriIBAN
+ const name = p.params["receiver-name"]
+ return <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd>
+ </div>
+ {name &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ </div>
+ }
+ </Fragment>
+ }
+ case "x-taler-bank": {
+ const p = details.account as PaytoUriTalerBank
+ const name = p.params["receiver-name"]
+ return <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd>
+ </div>
+ {name &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ </div>
+ }
+ </Fragment>
+ }
+ default:
+ return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd>
+ </div>
- name="answer"
- id="answer"
- autocomplete="off"
- onChange={(e): void => {
- setCaptchaAnswer(e.currentTarget.value)
- }}
- />
+ }
+ })()}
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount value={details.amount} spec={config.currency_specification} />
+ </dd>
+ </div>
+ </dl>
</div>
- <ShowInputErrorLabel message={errors?.answer} isDirty={captchaAnswer !== undefined} />
</div>
+
</div>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
@@ -265,7 +253,6 @@ export function WithdrawalConfirmationQuestion({
<i18n.Translate>Cancel</i18n.Translate></button>
<button type="submit"
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- disabled={!!errors}
onClick={(e) => {
e.preventDefault()
doTransfer()
@@ -279,66 +266,6 @@ export function WithdrawalConfirmationQuestion({
</div>
</ShouldBeSameUser>
</div>
- <div class="px-4 mt-4 ">
- <div class="w-full">
- <div class="px-4 sm:px-0 text-sm">
- <p><i18n.Translate>Wire transfer details</i18n.Translate></p>
- </div>
- <div class="mt-6 border-t border-gray-100">
- <dl class="divide-y divide-gray-100">
- {((): VNode => {
- switch (details.account.targetType) {
- case "iban": {
- const p = details.account as PaytoUriIBAN
- const name = p.params["receiver-name"]
- return <Fragment>
- <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
- <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd>
- </div>
- {name &&
- <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
- <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
- </div>
- }
- </Fragment>
- }
- case "x-taler-bank": {
- const p = details.account as PaytoUriTalerBank
- const name = p.params["receiver-name"]
- return <Fragment>
- <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
- <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd>
- </div>
- {name &&
- <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
- <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
- </div>
- }
- </Fragment>
- }
- default:
- return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
- <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd>
- </div>
-
- }
- })()}
- <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
- <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- <RenderAmount value={details.amount} spec={config.currency_specification} />
- </dd>
- </div>
- </dl>
- </div>
- </div>
-
- </div>
</div>
</div>
diff --git a/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx b/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx
index 4bb3b4d7b..7ed5e4b0a 100644
--- a/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx
@@ -32,8 +32,10 @@ const logger = new Logger("AccountPage");
export function WithdrawalOperationPage({
operationId,
+ onAuthorizationRequired,
onContinue,
}: {
+ onAuthorizationRequired: () => void;
operationId: string;
onContinue: () => void;
}): VNode {
@@ -56,6 +58,7 @@ export function WithdrawalOperationPage({
return (
<WithdrawalQRCode
withdrawUri={parsedUri}
+ onAuthorizationRequired={onAuthorizationRequired}
onClose={() => {
updateBankState("currentWithdrawalOperationId", undefined)
onContinue()
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index f05f183d4..97bc9f61f 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -35,6 +35,8 @@ const logger = new Logger("WithdrawalQRCode");
interface Props {
withdrawUri: WithdrawUriResult;
onClose: () => void;
+ onAuthorizationRequired: () => void,
+
}
/**
* Offer the QR code (and a clickable taler://-link) to
@@ -44,6 +46,7 @@ interface Props {
export function WithdrawalQRCode({
withdrawUri,
onClose,
+ onAuthorizationRequired,
}: Props): VNode {
const { i18n } = useTranslationContext();
const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
@@ -164,6 +167,7 @@ export function WithdrawalQRCode({
reserve: data.selected_reserve_pub,
amount: Amounts.parseOrThrow(data.amount)
}}
+ onAuthorizationRequired={onAuthorizationRequired}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
onClose()
@@ -173,7 +177,7 @@ export function WithdrawalQRCode({
}
-export function OperationNotFound({ onClose }: { onClose: () => void }): VNode {
+export function OperationNotFound({ onClose }: { onClose: (() => void) | undefined }): VNode {
const { i18n } = useTranslationContext();
return <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<div>
@@ -197,15 +201,17 @@ export function OperationNotFound({ onClose }: { onClose: () => void }): VNode {
</div>
</div>
</div>
- <div class="mt-5 sm:mt-6">
- <button type="button"
- class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={async (e) => {
- e.preventDefault();
- onClose()
- }}>
- <i18n.Translate>Cotinue to dashboard</i18n.Translate>
- </button>
- </div>
+ {onClose &&
+ <div class="mt-5 sm:mt-6">
+ <button type="button"
+ class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ onClick={async (e) => {
+ e.preventDefault();
+ onClose()
+ }}>
+ <i18n.Translate>Cotinue to dashboard</i18n.Translate>
+ </button>
+ </div>
+ }
</div>
} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
index f2972ed65..1676d8b6a 100644
--- a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
+++ b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
@@ -9,10 +9,11 @@ import { CreateCashout } from "../business/CreateCashout.js";
interface Props {
account: string,
onClose: () => void,
+ onAuthorizationRequired: () => void,
onSelected: (cid: number) => void
}
-export function CashoutListForAccount({ account, onSelected, onClose }: Props): VNode {
+export function CashoutListForAccount({ account, onAuthorizationRequired, onSelected, onClose }: Props): VNode {
const { i18n } = useTranslationContext();
const { state: credentials } = useBackendState();
@@ -29,7 +30,7 @@ export function CashoutListForAccount({ account, onSelected, onClose }: Props):
</h1>
}
- <CreateCashout focus onCancel={onClose} onComplete={() => { }} account={account} />
+ <CreateCashout focus onCancel={onClose} onAuthorizationRequired={onAuthorizationRequired} account={account} />
<Cashouts
account={account}
diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
index 28875bde6..ca3e2fbdf 100644
--- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -1,4 +1,4 @@
-import { HttpStatusCode, TalerCorebankApi, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
+import { AbsoluteTime, HttpStatusCode, TalerCorebankApi, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
import { Loading, LocalNotificationBanner, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
@@ -16,9 +16,11 @@ export function ShowAccountDetails({
account,
onClear,
onUpdateSuccess,
+ onAuthorizationRequired,
}: {
onClear?: () => void;
onUpdateSuccess: () => void;
+ onAuthorizationRequired: () => void,
account: string;
}): VNode {
const { i18n } = useTranslationContext();
@@ -54,7 +56,6 @@ export function ShowAccountDetails({
const resp = await api.updateAccount({
token: creds.token,
username: account,
-
}, submitAccount);
if (resp.type === "ok") {
@@ -99,11 +100,13 @@ export function ShowAccountDetails({
debug: resp.detail,
})
case HttpStatusCode.Accepted: {
- updateBankState("currentChallengeId", resp.body.challenge_id)
- return notify({
- type: "info",
- title: i18n.str`Cashout created but confirmation is required.`,
- });
+ updateBankState("currentChallenge", {
+ operation: "update-account",
+ id: String(resp.body.challenge_id),
+ sent: AbsoluteTime.never(),
+ request: submitAccount,
+ })
+ return onAuthorizationRequired()
}
case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: {
return notify({
@@ -122,7 +125,7 @@ export function ShowAccountDetails({
return (
<Fragment>
- <LocalNotificationBanner notification={notification} />
+ <LocalNotificationBanner notification={notification} showDebug={true} />
{accountIsTheCurrentUser ?
<ProfileNavigation current="details" />
:
diff --git a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
index 0ff1cf725..3c4a865ed 100644
--- a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
+++ b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
@@ -9,17 +9,19 @@ import { doAutoFocus } from "../PaytoWireTransferForm.js";
import { ProfileNavigation } from "../ProfileNavigation.js";
import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { LocalNotificationBanner } from "@gnu-taler/web-util/browser";
-import { HttpStatusCode, TalerErrorCode } from "@gnu-taler/taler-util";
+import { AbsoluteTime, HttpStatusCode, TalerErrorCode } from "@gnu-taler/taler-util";
import { useBankState } from "../../hooks/bank-state.js";
export function UpdateAccountPassword({
account: accountName,
onCancel,
onUpdateSuccess,
+ onAuthorizationRequired,
focus,
}: {
onCancel: () => void;
focus?: boolean,
+ onAuthorizationRequired: () => void,
onUpdateSuccess: () => void;
account: string;
}): VNode {
@@ -51,10 +53,11 @@ export function UpdateAccountPassword({
async function doChangePassword() {
if (!!errors || !password || !token) return;
await handleError(async () => {
- const resp = await api.updatePassword({ username: accountName, token }, {
+ const request = {
old_password: current,
new_password: password,
- });
+ }
+ const resp = await api.updatePassword({ username: accountName, token }, request);
if (resp.type === "ok") {
notifyInfo(i18n.str`Password changed`);
onUpdateSuccess();
@@ -77,11 +80,13 @@ export function UpdateAccountPassword({
title: i18n.str`Your current password doesn't match, can't change to a new password.`
})
case HttpStatusCode.Accepted: {
- updateBankState("currentChallengeId", resp.body.challenge_id)
- return notify({
- type: "info",
- title: i18n.str`Cashout created but confirmation is required.`,
- });
+ updateBankState("currentChallenge", {
+ operation: "update-password",
+ id: String(resp.body.challenge_id),
+ sent: AbsoluteTime.never(),
+ request,
+ })
+ return onAuthorizationRequired()
}
default: assertUnreachable(resp)
}
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index 859c04396..7296e7744 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -1,9 +1,9 @@
import { AmountString, Amounts, PaytoString, TalerCorebankApi, TranslatedString, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
-import { CopyButton, ShowInputErrorLabel, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Attention, CopyButton, ShowInputErrorLabel, useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { useBankCoreApiContext } from "../../context/config.js";
-import { ErrorMessageMappingFor, PartialButDefined, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
+import { VersionHint, useBankCoreApiContext } from "../../context/config.js";
+import { ErrorMessageMappingFor, PartialButDefined, TanChannel, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { getRandomPassword } from "../rnd.js";
@@ -24,6 +24,7 @@ export type AccountFormData = {
cashout_payto_uri?: string,
email?: string,
phone?: string,
+ tan_channel?: TanChannel | "remove",
}
type ChangeByPurposeType = {
@@ -55,7 +56,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
onChange: ChangeByPurposeType[PurposeType];
purpose: PurposeType;
}): VNode {
- const { config } = useBankCoreApiContext()
+ const { config, hints } = useBankCoreApiContext()
const { i18n } = useTranslationContext();
const { state: credentials } = useBackendState();
const [form, setForm] = useState<AccountFormData>({});
@@ -75,8 +76,11 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
email: template?.contact_data?.email ?? "",
phone: template?.contact_data?.phone ?? "",
username: username ?? "",
+ tan_channel: template?.tan_channel,
}
+ const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1
+
const showingCurrentUserInfo = credentials.status !== "loggedIn" ? false : username === credentials.username
const userIsAdmin = credentials.status !== "loggedIn" ? false : credentials.isUserAdministrator
@@ -86,6 +90,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
const editableThreshold = userIsAdmin && (purpose === "create" || purpose === "update")
const editableAccount = purpose === "create" && userIsAdmin
+ const hasPhone = !!defaultValue.phone || !!form.phone
+ const hasEmail = !!defaultValue.email || !!form.email
+
function updateForm(newForm: typeof defaultValue): void {
const cashoutParsed = !newForm.cashout_payto_uri
? undefined
@@ -173,6 +180,8 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
payto_uri: internalURI,
is_public: !!newForm.isPublic,
is_taler_exchange: !!newForm.isExchange,
+ // @ts-ignore
+ tan_channel: newForm.tan_channel === "remove" ? null : newForm.tan_channel,
}
callback(result)
return;
@@ -190,6 +199,8 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
debit_threshold: threshold,
is_public: !!newForm.isPublic,
name: newForm.name,
+ // @ts-ignore
+ tan_channel: newForm?.tan_channel === "remove" ? null : newForm.tan_channel,
}
callback(result)
return;
@@ -409,7 +420,87 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
<span aria-hidden="true" data-enabled={form.isExchange ?? defaultValue.isExchange ? "true" : "false"} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
- </div>}
+ </div>
+ }
+ {/* channel, not shown if old cashout api */}
+ {OLD_CASHOUT_API ? undefined : config.supported_tan_channels.length === 0 ?
+ <div class="sm:col-span-5">
+ <Attention type="warning" title={i18n.str`No cashout channel available`}>
+ <i18n.Translate>
+ This server doesn't support second factor authentication.
+ </i18n.Translate>
+ </Attention>
+ </div>
+ :
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="channel"
+ >
+ {i18n.str`Confirmation the operation using`}
+ </label>
+ <div class="mt-2 max-w-xl text-sm text-gray-500">
+ <div class="px-4 mt-4 grid grid-cols-1 gap-y-6">
+ {config.supported_tan_channels.indexOf(TanChannel.EMAIL) === -1 ? undefined :
+ <label onClick={(e) => {
+ if (!hasEmail) return;
+ if (form.tan_channel === TanChannel.EMAIL) {
+ form.tan_channel = "remove"
+ } else {
+ form.tan_channel = TanChannel.EMAIL
+ }
+ updateForm(structuredClone(form))
+ e.preventDefault()
+ }} data-disabled={purpose === "show" || !hasEmail} data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.EMAIL}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600">
+ <input type="radio" name="channel" value="Newsletter" class="sr-only" />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span id="project-type-0-label" class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>Email</i18n.Translate>
+ </span>
+ {purpose !== "show" && !hasEmail && i18n.str`add a email in your profile to enable this option`}
+ </span>
+ </span>
+ <svg data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.EMAIL} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+ </svg>
+ </label>
+ }
+
+ {config.supported_tan_channels.indexOf(TanChannel.SMS) === -1 ? undefined :
+ <label onClick={(e) => {
+ if (!hasPhone) return;
+ if (form.tan_channel === TanChannel.SMS) {
+ form.tan_channel = "remove"
+ } else {
+ form.tan_channel = TanChannel.SMS
+ }
+ updateForm(structuredClone(form))
+ e.preventDefault()
+ }} data-disabled={purpose === "show" || !hasPhone} data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.SMS}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600">
+ <input type="radio" name="channel" value="Existing Customers" class="sr-only" />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>SMS</i18n.Translate>
+ </span>
+ {purpose !== "show" && !hasPhone && i18n.str`add a phone number in your profile to enable this option`}
+ </span>
+ </span>
+ <svg data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.SMS} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+ </svg>
+ </label>
+ }
+ <pre>
+ {JSON.stringify(form, undefined, 2)}
+ </pre>
+ </div>
+ </div>
+ </div>
+ }
<div class="sm:col-span-5">
<div class="flex items-center justify-between">
@@ -434,9 +525,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
</div>
</div>
- <pre>
- {JSON.stringify(errors, undefined, 2)}
- </pre>
{children}
</form>
);
diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
index 82a341dbe..f5bce1396 100644
--- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
@@ -16,18 +16,17 @@ import { AccountList } from "./AccountList.js";
* Query account information and show QR code if there is pending withdrawal
*/
interface Props {
- onRegister: () => void;
-
onCreateAccount: () => void;
onShowAccountDetails: (aid: string) => void;
onRemoveAccount: (aid: string) => void;
onUpdateAccountPassword: (aid: string) => void;
onShowCashoutForAccount: (aid: string) => void;
+ onAuthorizationRequired: () => void;
}
-export function AdminHome({ onCreateAccount, onRegister, onRemoveAccount, onShowAccountDetails, onShowCashoutForAccount, onUpdateAccountPassword }: Props): VNode {
+export function AdminHome({ onCreateAccount, onAuthorizationRequired, onRemoveAccount, onShowAccountDetails, onShowCashoutForAccount, onUpdateAccountPassword }: Props): VNode {
return <Fragment>
<Metrics />
- <WireTransfer onRegister={onRegister} />
+ <WireTransfer onAuthorizationRequired={onAuthorizationRequired} />
<Transactions account="admin" />
<AccountList
@@ -184,11 +183,11 @@ function Metrics(): VNode {
</div>
</dl>
<div class="flex justify-end mt-2">
- <a href="#/download-stats"
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- ><i18n.Translate>
- download stats as csv
- </i18n.Translate></a>
+ <a href="#/download-stats"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ ><i18n.Translate>
+ download stats as csv
+ </i18n.Translate></a>
</div>
</Fragment>
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
index 3f7d62935..beadad957 100644
--- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -1,4 +1,4 @@
-import { Amounts, HttpStatusCode, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
+import { AbsoluteTime, Amounts, HttpStatusCode, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
import { Attention, Loading, LocalNotificationBanner, ShowInputErrorLabel, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
@@ -16,9 +16,11 @@ export function RemoveAccount({
account,
onCancel,
onUpdateSuccess,
+ onAuthorizationRequired,
focus,
}: {
focus?: boolean;
+ onAuthorizationRequired: () => void,
onCancel: () => void;
onUpdateSuccess: () => void;
account: string;
@@ -92,11 +94,13 @@ export function RemoveAccount({
debug: resp.detail,
})
case HttpStatusCode.Accepted: {
- updateBankState("currentChallengeId", resp.body.challenge_id)
- return notify({
- type: "info",
- title: i18n.str`The operation needs a confirmation to complete.`,
- });
+ updateBankState("currentChallenge", {
+ operation: "delete-account",
+ id: String(resp.body.challenge_id),
+ sent: AbsoluteTime.never(),
+ request: account,
+ })
+ return onAuthorizationRequired()
}
default: {
assertUnreachable(resp)
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index d97a00a2e..e4fda8fb6 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
+ AbsoluteTime,
Amounts,
HttpStatusCode,
TalerCorebankApi,
@@ -36,7 +37,7 @@ import {
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
-import { useBankCoreApiContext } from "../../context/config.js";
+import { VersionHint, useBankCoreApiContext } from "../../context/config.js";
import { useAccountDetails } from "../../hooks/access.js";
import { useBackendState } from "../../hooks/backend.js";
import {
@@ -55,7 +56,7 @@ import { useBankState } from "../../hooks/bank-state.js";
interface Props {
account: string;
focus?: boolean,
- onComplete: (id: string) => void;
+ onAuthorizationRequired: () => void,
onCancel?: () => void;
}
@@ -72,7 +73,7 @@ type ErrorFrom<T> = {
export function CreateCashout({
account: accountName,
- onComplete,
+ onAuthorizationRequired,
focus,
onCancel,
}: Props): VNode {
@@ -86,7 +87,7 @@ export function CreateCashout({
const creds = credentials.status !== "loggedIn" ? undefined : credentials
const [, updateBankState] = useBankState()
- const { api, config } = useBankCoreApiContext()
+ const { api, config, hints } = useBankCoreApiContext()
const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, });
const [notification, notify, handleError] = useLocalNotification()
const info = useConversionInfo();
@@ -96,6 +97,9 @@ export function CreateCashout({
<i18n.Translate>The bank configuration does not support cashout operations.</i18n.Translate>
</Attention>
}
+
+ const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1
+
if (!resultAccount) {
return <Loading />
}
@@ -179,33 +183,37 @@ export function CreateCashout({
: Amounts.isZero(calc.credit)
? i18n.str`the total transfer at destination will be zero`
: undefined,
- channel: !form.channel ? i18n.str`required` : undefined,
+ channel: OLD_CASHOUT_API && !form.channel ? i18n.str`required` : undefined,
});
const trimmedAmountStr = form.amount?.trim();
async function createCashout() {
const request_uid = encodeCrock(getRandomBytes(32))
await handleError(async () => {
- const validChannel = config.supported_tan_channels.length === 0 || form.channel
+ //new cashout api doesn't require channel
+ const validChannel = !OLD_CASHOUT_API || config.supported_tan_channels.length === 0 || form.channel
if (!creds || !form.subject || !validChannel) return;
- const resp = await api.createCashout(creds, {
+ const request = {
request_uid,
amount_credit: Amounts.stringify(calc.credit),
amount_debit: Amounts.stringify(calc.debit),
subject: form.subject,
tan_channel: form.channel,
- })
+ }
+ const resp = await api.createCashout(creds, request)
if (resp.type === "ok") {
notifyInfo(i18n.str`Cashout created`)
} else {
switch (resp.case) {
case HttpStatusCode.Accepted: {
- updateBankState("currentChallengeId", resp.body.challenge_id)
- return notify({
- type: "info",
- title: i18n.str`Cashout created but confirmation is required.`,
- });
+ updateBankState("currentChallenge", {
+ operation: "create-cashout",
+ id: String(resp.body.challenge_id),
+ sent: AbsoluteTime.never(),
+ request,
+ })
+ return onAuthorizationRequired()
}
case HttpStatusCode.NotFound: return notify({
type: "error",
@@ -444,8 +452,8 @@ export function CreateCashout({
</div>
)}
- {/* channel */}
- {config.supported_tan_channels.length === 0 ?
+ {/* channel, not shown if new cashout api */}
+ {!OLD_CASHOUT_API ? undefined : config.supported_tan_channels.length === 0 ?
<div class="sm:col-span-5">
<Attention type="warning" title={i18n.str`No cashout channel available`}>
<i18n.Translate>
diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
index 5d8db5aee..b517a7d42 100644
--- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
+++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
@@ -93,6 +93,9 @@ export function ShowCashoutDetails({
const errors = undefinedIfEmpty({
code: !code ? i18n.str`required` : undefined,
});
+ /**
+ * @deprecated
+ */
const isPending = String(result.body.status).toUpperCase() === "PENDING";
const { fiat_currency_specification, regional_currency_specification } = info.body
// won't implement in retry in old API 3:0:3 since request_uid is missing
@@ -266,7 +269,6 @@ export function ShowCashoutDetails({
{!isPending ? undefined :
<Fragment>
-
<div />
<form
class="bg-white shadow-sm ring-1 ring-gray-900/5"
@@ -318,7 +320,6 @@ export function ShowCashoutDetails({
<i18n.Translate>Confirm</i18n.Translate>
</button>
</div>
-
</form>
</Fragment>}
</div>