From 82d4ed90caa4a6ea3bdda1fb80ccecf3dc3637f9 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 11 Jan 2024 16:41:24 -0300 Subject: 2fa --- packages/demobank-ui/src/Routing.tsx | 57 ++- packages/demobank-ui/src/components/app.tsx | 22 +- packages/demobank-ui/src/context/config.ts | 72 ++- packages/demobank-ui/src/hooks/access.ts | 36 +- packages/demobank-ui/src/hooks/backend.ts | 30 +- packages/demobank-ui/src/hooks/bank-state.ts | 104 +++- packages/demobank-ui/src/hooks/circuit.ts | 20 +- .../demobank-ui/src/pages/AccountPage/index.ts | 2 + .../demobank-ui/src/pages/AccountPage/state.ts | 3 +- .../demobank-ui/src/pages/AccountPage/views.tsx | 48 +- .../demobank-ui/src/pages/OperationState/index.ts | 4 +- .../demobank-ui/src/pages/OperationState/state.ts | 14 +- .../demobank-ui/src/pages/OperationState/views.tsx | 71 +-- packages/demobank-ui/src/pages/PaymentOptions.tsx | 15 +- .../src/pages/PaytoWireTransferForm.tsx | 23 +- .../demobank-ui/src/pages/SolveChallengePage.tsx | 553 +++++++++++++++++++++ .../demobank-ui/src/pages/WalletWithdrawForm.tsx | 3 + packages/demobank-ui/src/pages/WireTransfer.tsx | 8 +- .../src/pages/WithdrawalConfirmationQuestion.tsx | 211 +++----- .../src/pages/WithdrawalOperationPage.tsx | 3 + .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 28 +- .../src/pages/account/CashoutListForAccount.tsx | 5 +- .../src/pages/account/ShowAccountDetails.tsx | 19 +- .../src/pages/account/UpdateAccountPassword.tsx | 21 +- .../demobank-ui/src/pages/admin/AccountForm.tsx | 104 +++- packages/demobank-ui/src/pages/admin/AdminHome.tsx | 17 +- .../demobank-ui/src/pages/admin/RemoveAccount.tsx | 16 +- .../src/pages/business/CreateCashout.tsx | 38 +- .../src/pages/business/ShowCashoutDetails.tsx | 5 +- 29 files changed, 1178 insertions(+), 374 deletions(-) create mode 100644 packages/demobank-ui/src/pages/SolveChallengePage.tsx (limited to 'packages/demobank-ui/src') diff --git a/packages/demobank-ui/src/Routing.tsx b/packages/demobank-ui/src/Routing.tsx index 4a250a0d5..4caa1dff0 100644 --- a/packages/demobank-ui/src/Routing.tsx +++ b/packages/demobank-ui/src/Routing.tsx @@ -39,6 +39,7 @@ import { AccountPage } from "./pages/AccountPage/index.js"; import { useSettingsContext } from "./context/settings.js"; import { useBankCoreApiContext } from "./context/config.js"; import { DownloadStats } from "./pages/DownloadStats.js"; +import { SolveChallengePage } from "./pages/SolveChallengePage.js"; export function Routing(): VNode { const history = createHashHistory(); @@ -75,6 +76,9 @@ export function Routing(): VNode { component={({ wopid }: { wopid: string }) => ( { + route(`/2fa`) + }} onContinue={() => { route("/account"); }} @@ -113,6 +117,19 @@ export function Routing(): VNode { onContinue={() => { route("/account"); }} + onAuthorizationRequired={() => { + route(`/2fa`) + }} + /> + )} + /> + ( + { + route("/account"); + }} /> )} /> @@ -122,7 +139,7 @@ export function Routing(): VNode { /> { route("/account") }} @@ -149,6 +166,9 @@ export function Routing(): VNode { onUpdateSuccess={() => { route("/account") }} + onAuthorizationRequired={() => { + route(`/2fa`) + }} onClear={() => { route("/account") }} @@ -165,6 +185,9 @@ export function Routing(): VNode { onUpdateSuccess={() => { route("/account") }} + onAuthorizationRequired={() => { + route(`/2fa`) + }} onCancel={() => { route("/account") }} @@ -179,6 +202,9 @@ export function Routing(): VNode { onUpdateSuccess={() => { route("/account") }} + onAuthorizationRequired={() => { + route(`/2fa`) + }} onCancel={() => { route("/account") }} @@ -194,6 +220,9 @@ export function Routing(): VNode { onSelected={(cid) => { route(`/cashout/${cid}`) }} + onAuthorizationRequired={() => { + route(`/2fa`) + }} onClose={() => { route("/account") }} @@ -209,6 +238,9 @@ export function Routing(): VNode { onUpdateSuccess={() => { route("/") }} + onAuthorizationRequired={() => { + route(`/2fa`) + }} onCancel={() => { route("/account") }} @@ -223,6 +255,9 @@ export function Routing(): VNode { onUpdateSuccess={() => { route("/account") }} + onAuthorizationRequired={() => { + route(`/2fa`) + }} onClear={() => { route("/account") }} @@ -238,6 +273,9 @@ export function Routing(): VNode { onUpdateSuccess={() => { route("/account") }} + onAuthorizationRequired={() => { + route(`/2fa`) + }} onCancel={() => { route("/account") }} @@ -253,6 +291,9 @@ export function Routing(): VNode { onSelected={(cid) => { route(`/cashout/${cid}`) }} + onAuthorizationRequired={() => { + route(`/2fa`) + }} onClose={() => { route("/account"); }} @@ -265,8 +306,8 @@ export function Routing(): VNode { component={() => ( { - route(`/cashout/${cid}`); + onAuthorizationRequired={() => { + route(`/2fa`) }} onCancel={() => { route("/account"); @@ -293,6 +334,9 @@ export function Routing(): VNode { component={({ dest }: { dest: string }) => ( { + route(`/2fa`) + }} onCancel={() => { route("/account") }} @@ -308,8 +352,8 @@ export function Routing(): VNode { component={() => { if (isUserAdministrator) { return { - route("/register"); + onAuthorizationRequired={() => { + route(`/2fa`) }} onCreateAccount={() => { route("/new-account") @@ -331,6 +375,9 @@ export function Routing(): VNode { } else { return { + route(`/2fa`) + }} goToConfirmOperation={(wopid) => { route(`/operation/${wopid}`); }} diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx index 4921b6bff..3d1a43803 100644 --- a/packages/demobank-ui/src/components/app.tsx +++ b/packages/demobank-ui/src/components/app.tsx @@ -38,7 +38,7 @@ const App: FunctionalComponent = () => { fetchSettings(setSettings) }, []) if (!settings) return ; - + const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL); return ( @@ -50,6 +50,26 @@ const App: FunctionalComponent = () => { provider: WITH_LOCAL_STORAGE_CACHE ? localStorageProvider : undefined, + // normally, do not revalidate + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + revalidateOnMount: undefined, + focusThrottleInterval: undefined, + + // normally, do not refresh + refreshInterval: undefined, + dedupingInterval: 2000, + refreshWhenHidden: false, + refreshWhenOffline: false, + + //ignore errors + shouldRetryOnError: false, + errorRetryCount: 0, + errorRetryInterval: undefined, + + // do not go to loading again if already has data + keepPreviousData: true, }} > diff --git a/packages/demobank-ui/src/context/config.ts b/packages/demobank-ui/src/context/config.ts index 2d70cf932..0bf920006 100644 --- a/packages/demobank-ui/src/context/config.ts +++ b/packages/demobank-ui/src/context/config.ts @@ -14,10 +14,13 @@ GNU Taler; see the file COPYING. If not, see */ -import { LibtoolVersion, TalerCorebankApi, TalerCoreBankHttpClient, TalerError } from "@gnu-taler/taler-util"; +import { AccessToken, HttpStatusCode, LibtoolVersion, OperationAlternative, OperationFail, OperationOk, TalerCorebankApi, TalerCoreBankHttpClient, TalerError, TalerErrorCode, UserAndToken } from "@gnu-taler/taler-util"; +import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { BrowserHttpLib, ErrorLoading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, createContext, FunctionComponent, h, VNode } from "preact"; import { useContext, useEffect, useState } from "preact/hooks"; +import { revalidateAccountDetails, revalidatePublicAccounts, revalidateTransactions } from "../hooks/access.js"; +import { revalidateBusinessAccounts, revalidateCashouts } from "../hooks/circuit.js"; /** * @@ -28,13 +31,14 @@ export type Type = { url: URL, config: TalerCorebankApi.Config, api: TalerCoreBankHttpClient, + hints: VersionHint[] }; const Context = createContext(undefined as any); export const useBankCoreApiContext = (): Type => useContext(Context); -enum VersionHint { +export enum VersionHint { /** * when this flag is on, server is running an old version with cashout before implementing 2fa API */ @@ -42,7 +46,7 @@ enum VersionHint { } export type ConfigResult = undefined - | { type: "ok", config: TalerCorebankApi.Config, hint: VersionHint[] } + | { type: "ok", config: TalerCorebankApi.Config, hints: VersionHint[] } | { type: "incompatible", result: TalerCorebankApi.Config, supported: string } | { type: "error", error: TalerError } @@ -58,17 +62,17 @@ export const BankCoreApiProvider = ({ const [checked, setChecked] = useState() const { i18n } = useTranslationContext(); const url = new URL(baseUrl) - const api = new TalerCoreBankHttpClient(url.href, new BrowserHttpLib()) + const api = new CacheAwareApi(url.href, new BrowserHttpLib()) useEffect(() => { api.getConfig() .then((resp) => { if (api.isCompatible(resp.body.version)) { - setChecked({ type: "ok", config: resp.body, hint: [] }); + setChecked({ type: "ok", config: resp.body, hints: [] }); } else { //this API supports version 3.0.3 const compare = LibtoolVersion.compare("3:0:3", resp.body.version) if (compare?.compatible ?? false) { - setChecked({ type: "ok", config: resp.body, hint: [VersionHint.CASHOUT_BEFORE_2FA] }); + setChecked({ type: "ok", config: resp.body, hints: [VersionHint.CASHOUT_BEFORE_2FA] }); } else { setChecked({ type: "incompatible", result: resp.body, supported: api.PROTOCOL_VERSION }) } @@ -91,7 +95,7 @@ export const BankCoreApiProvider = ({ return h(frameOnError, { children: h("div", {}, i18n.str`the bank backend is not supported. supported version "${checked.supported}", server version "${checked.result.version}"`) }) } const value: Type = { - url, config: checked.config, api + url, config: checked.config, api: api, hints: checked.hints, } return h(Context.Provider, { value, @@ -99,6 +103,59 @@ export const BankCoreApiProvider = ({ }); }; +export class CacheAwareApi extends TalerCoreBankHttpClient { + constructor(baseUrl: string, httpClient?: HttpRequestLibrary) { + super(baseUrl, httpClient) + } + async deleteAccount(auth: UserAndToken, cid?: string | undefined) { + const resp = await super.deleteAccount(auth, cid) + if (resp.type === "ok") { + revalidatePublicAccounts() + revalidateBusinessAccounts() + } + return resp; + } + async createAccount(auth: AccessToken, body: TalerCorebankApi.RegisterAccountRequest) { + const resp = await super.createAccount(auth, body) + if (resp.type === "ok") { + revalidatePublicAccounts() + revalidateBusinessAccounts() + } + return resp; + } + async updateAccount(auth: UserAndToken, body: TalerCorebankApi.AccountReconfiguration, cid?: string | undefined) { + const resp = await super.updateAccount(auth, body, cid) + if (resp.type === "ok") { + revalidateAccountDetails() + } + return resp; + } + async createTransaction(auth: UserAndToken, body: TalerCorebankApi.CreateTransactionRequest, cid?: string | undefined) { + const resp = await super.createTransaction(auth, body, cid) + if (resp.type === "ok") { + revalidateAccountDetails() + revalidateTransactions() + } + return resp; + } + async confirmWithdrawalById(auth: UserAndToken, wid: string, cid?: string | undefined) { + const resp = await super.confirmWithdrawalById(auth, wid, cid) + if (resp.type === "ok") { + revalidateAccountDetails() + revalidateTransactions() + } + return resp; + } + async createCashout(auth: UserAndToken, body: TalerCorebankApi.CashoutRequest, cid?: string | undefined) { + const resp = await super.createCashout(auth, body, cid) + if (resp.type === "ok") { + revalidateAccountDetails() + revalidateCashouts() + } + return resp; + } +} + export const BankCoreApiProviderTesting = ({ children, state, @@ -112,6 +169,7 @@ export const BankCoreApiProviderTesting = ({ url: new URL(url), config: state, api: undefined as any, + hints: [], }; return h(Context.Provider, { diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts index fc1cff129..80ef1874f 100644 --- a/packages/demobank-ui/src/hooks/access.ts +++ b/packages/demobank-ui/src/hooks/access.ts @@ -14,13 +14,13 @@ GNU Taler; see the file COPYING. If not, see */ -import { AccessToken, TalerBankIntegrationResultByMethod, TalerCoreBankResultByMethod, TalerHttpError, WithdrawalOperationStatus } from "@gnu-taler/taler-util"; +import { AccessToken, TalerCoreBankResultByMethod, TalerHttpError, WithdrawalOperationStatus } from "@gnu-taler/taler-util"; import { useEffect, useState } from "preact/hooks"; import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; import { useBackendState } from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; +import _useSWR, { SWRHook, mutate } from "swr"; import { useBankCoreApiContext } from "../context/config.js"; const useSWR = _useSWR as unknown as SWRHook; @@ -30,6 +30,10 @@ export interface InstanceTemplateFilter { position?: string; } +export function revalidateAccountDetails() { + mutate(key => Array.isArray(key) && key[key.length - 1] === "getAccount", undefined, { revalidate: true }) +} + export function useAccountDetails(account: string) { const { state: credentials } = useBackendState(); const { api } = useBankCoreApiContext(); @@ -40,15 +44,6 @@ export function useAccountDetails(account: string) { const token = credentials.status !== "loggedIn" ? undefined : credentials.token const { data, error } = useSWR, TalerHttpError>( [account, token, "getAccount"], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - keepPreviousData: true, }); if (data) return data @@ -56,6 +51,10 @@ export function useAccountDetails(account: string) { return undefined; } +export function revalidateWithdrawalDetails() { + mutate(key => Array.isArray(key) && key[key.length - 1] === "getWithdrawalById") +} + export function useWithdrawalDetails(wid: string) { const { api } = useBankCoreApiContext(); const [latestStatus, setLatestStatus] = useState() @@ -90,6 +89,9 @@ export function useWithdrawalDetails(wid: string) { return undefined; } +export function revalidateTransactionDetails() { + mutate(key => Array.isArray(key) && key[key.length - 1] === "getTransactionById") +} export function useTransactionDetails(account: string, tid: number) { const { state: credentials } = useBackendState(); const token = credentials.status !== "loggedIn" ? undefined : credentials.token @@ -117,6 +119,9 @@ export function useTransactionDetails(account: string, tid: number) { return undefined; } +export function revalidatePublicAccounts() { + mutate(key => Array.isArray(key) && key[key.length - 1] === "getPublicAccounts") +} export function usePublicAccounts(filterAccount: string | undefined, initial?: number) { const [offset, setOffset] = useState(initial); const { api } = useBankCoreApiContext(); @@ -171,12 +176,9 @@ export function usePublicAccounts(filterAccount: string | undefined, initial?: n return undefined; } -/** - - * @param account - * @param args - * @returns - */ +export function revalidateTransactions() { + mutate(key => Array.isArray(key) && key[key.length - 1] === "getTransactions", undefined, { revalidate: true }) +} export function useTransactions(account: string, initial?: number) { const { state: credentials } = useBackendState(); const token = credentials.status !== "loggedIn" ? undefined : credentials.token diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index 863b47bf3..46918ac10 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -19,16 +19,15 @@ import { Codec, buildCodecForObject, buildCodecForUnion, - canonicalizeBaseUrl, codecForBoolean, codecForConstString, - codecForString, + codecForString } from "@gnu-taler/taler-util"; import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; -import { useSWRConfig } from "swr"; +import { mutate } from "swr"; /** * Has the information to reach and @@ -105,7 +104,6 @@ export function useBackendState(): BackendStateHandler { BACKEND_STATE_KEY, defaultState, ); - const mutateAll = useMatchMutate(); return { state, @@ -129,29 +127,11 @@ export function useBackendState(): BackendStateHandler { isUserAdministrator: info.username === "admin", }; update(nextState); - mutateAll(/.*/) + cleanAllCache() }, }; } -export function useMatchMutate(): ( - re: RegExp, - value?: unknown, -) => Promise { - const { cache, mutate } = useSWRConfig(); - - if (!(cache instanceof Map)) { - throw new Error( - "matchMutate requires the cache provider to be a Map instance", - ); - } - - return function matchRegexMutate(re: RegExp, value?: unknown) { - const allKeys = Array.from(cache.keys()); - const keys = allKeys.filter((key) => re.test(key)); - const mutations = keys.map((key) => { - return mutate(key, value, true); - }); - return Promise.all(mutations); - }; +function cleanAllCache(): void { + mutate(() => true, undefined, { revalidate: false }) } diff --git a/packages/demobank-ui/src/hooks/bank-state.ts b/packages/demobank-ui/src/hooks/bank-state.ts index addbbfc0f..99d835c9c 100644 --- a/packages/demobank-ui/src/hooks/bank-state.ts +++ b/packages/demobank-ui/src/hooks/bank-state.ts @@ -15,31 +15,127 @@ */ import { + AbsoluteTime, Codec, + TalerCorebankApi, buildCodecForObject, + buildCodecForUnion, + codecForAbsoluteTime, + codecForAny, + codecForConstString, codecForString, + codecForTanTransmission, codecOptional } from "@gnu-taler/taler-util"; import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; +export type ChallengeInProgess = + DeleteAccountChallenge | + UpdateAccountChallenge | + UpdatePasswordChallenge | + CreateTransactionChallenge | + ConfirmWithdrawalChallenge | + CashoutChallenge; + +type BaseChallenge = { + id: string, + operation: OpType, + sent: AbsoluteTime, + info?: TalerCorebankApi.TanTransmission, + request: ReqType +} + +type DeleteAccountChallenge = BaseChallenge<"delete-account", string> +type UpdateAccountChallenge = BaseChallenge<"update-account", TalerCorebankApi.AccountReconfiguration> +type UpdatePasswordChallenge = BaseChallenge<"update-password", TalerCorebankApi.AccountPasswordChange> +type CreateTransactionChallenge = BaseChallenge<"create-transaction", TalerCorebankApi.CreateTransactionRequest> +type ConfirmWithdrawalChallenge = BaseChallenge<"confirm-withdrawal", string> +type CashoutChallenge = BaseChallenge<"create-cashout", TalerCorebankApi.CashoutRequest> + +const codecForChallengeUpdatePassword = (): Codec => + buildCodecForObject() + .property("operation", codecForConstString("update-password")) + .property("id", codecForString()) + .property("sent", codecForAbsoluteTime) + .property("info", codecOptional(codecForTanTransmission())) + .property("request", codecForAny()) + .build("UpdatePasswordChallenge"); + +const codecForChallengeDeleteAccount = (): Codec => + buildCodecForObject() + .property("operation", codecForConstString("delete-account")) + .property("id", codecForString()) + .property("sent", codecForAbsoluteTime) + .property("request", codecForString()) + .property("info", codecOptional(codecForTanTransmission())) + .build("DeleteAccountChallenge"); + +const codecForChallengeUpdateAccount = (): Codec => + buildCodecForObject() + .property("operation", codecForConstString("update-account")) + .property("id", codecForString()) + .property("sent", codecForAbsoluteTime) + .property("info", codecOptional(codecForTanTransmission())) + .property("request", codecForAny()) + .build("UpdateAccountChallenge"); + +const codecForChallengeCreateTransaction = (): Codec => + buildCodecForObject() + .property("operation", codecForConstString("create-transaction")) + .property("id", codecForString()) + .property("sent", codecForAbsoluteTime) + .property("info", codecOptional(codecForTanTransmission())) + .property("request", codecForAny()) + .build("CreateTransactionChallenge"); + +const codecForChallengeConfirmWithdrawal = (): Codec => + buildCodecForObject() + .property("operation", codecForConstString("confirm-withdrawal")) + .property("id", codecForString()) + .property("sent", codecForAbsoluteTime) + .property("info", codecOptional(codecForTanTransmission())) + .property("request", codecForString()) + .build("ConfirmWithdrawalChallenge"); + +const codecForChallengeCashout = (): Codec => + buildCodecForObject() + .property("operation", codecForConstString("create-cashout")) + .property("id", codecForString()) + .property("sent", codecForAbsoluteTime) + .property("info", codecOptional(codecForTanTransmission())) + .property("request", codecForAny()) + .build("CashoutChallenge"); + +const codecForChallenge = (): Codec => + buildCodecForUnion() + .discriminateOn("operation") + .alternative("confirm-withdrawal", codecForChallengeConfirmWithdrawal()) + .alternative("create-cashout", codecForChallengeCashout()) + .alternative("create-transaction", codecForChallengeCreateTransaction()) + .alternative("delete-account", codecForChallengeDeleteAccount()) + .alternative("update-account", codecForChallengeUpdateAccount()) + .alternative("update-password", codecForChallengeUpdatePassword()) + .build("ChallengeInProgess"); + + interface BankState { currentWithdrawalOperationId: string | undefined; - currentChallengeId: string | undefined; + currentChallenge: ChallengeInProgess | undefined; } export const codecForBankState = (): Codec => buildCodecForObject() .property("currentWithdrawalOperationId", codecOptional(codecForString())) - .property("currentChallengeId", codecOptional(codecForString())) + .property("currentChallenge", codecOptional(codecForChallenge())) .build("BankState"); const defaultBankState: BankState = { currentWithdrawalOperationId: undefined, - currentChallengeId: undefined, + currentChallenge: undefined, }; const BANK_STATE_KEY = buildStorageKey( - "bank-state", + "bank-app-state", codecForBankState(), ); diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 8a27f652c..8bff6858d 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -19,7 +19,7 @@ import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; import { useBackendState } from "./backend.js"; import { AccessToken, AmountJson, AmountString, Amounts, OperationOk, TalerBankConversionResultByMethod, TalerCoreBankErrorsByMethod, TalerCoreBankResultByMethod, TalerCorebankApi, TalerError, TalerHttpError, opFixedSuccess } from "@gnu-taler/taler-util"; -import _useSWR, { SWRHook } from "swr"; +import _useSWR, { SWRHook, mutate } from "swr"; import { useBankCoreApiContext } from "../context/config.js"; import { assertUnreachable } from "../pages/WithdrawalOperationPage.js"; import { format, getDate, getDay, getHours, getMonth, getYear, set, sub } from "date-fns"; @@ -42,6 +42,9 @@ type CashoutEstimators = { estimateByDebit: EstimatorFunction; }; +export function revalidateConversionInfo() { + mutate(key => Array.isArray(key) && key[key.length - 1] === "getConversionInfoAPI") +} export function useConversionInfo() { const { api, config } = useBankCoreApiContext() @@ -114,6 +117,9 @@ export function useEstimator(): CashoutEstimators { }; } +export function revalidateBusinessAccounts() { + mutate(key => Array.isArray(key) && key[key.length - 1] === "getAccounts") +} export function useBusinessAccounts() { const { state: credentials } = useBackendState(); const token = credentials.status !== "loggedIn" ? undefined : credentials.token @@ -174,6 +180,9 @@ type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number } function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId { return c !== undefined } +export function revalidateOnePendingCashouts() { + mutate(key => Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts") +} export function useOnePendingCashouts(account: string) { const { state: credentials } = useBackendState(); const { api, config } = useBankCoreApiContext(); @@ -211,6 +220,9 @@ export function useOnePendingCashouts(account: string) { return undefined; } +export function revalidateCashouts() { + mutate(key => Array.isArray(key) && key[key.length - 1] === "useCashouts") +} export function useCashouts(account: string) { const { state: credentials } = useBackendState(); const { api, config } = useBankCoreApiContext(); @@ -251,6 +263,9 @@ export function useCashouts(account: string) { return undefined; } +export function revalidateCashoutDetails() { + mutate(key => Array.isArray(key) && key[key.length - 1] === "getCashoutById") +} export function useCashoutDetails(cashoutId: number | undefined) { const { state: credentials } = useBackendState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials @@ -284,6 +299,9 @@ export type MonitorMetrics = { } export type LastMonitor = { current: TalerCoreBankResultByMethod<"getMonitor">, previous: TalerCoreBankResultByMethod<"getMonitor"> } +export function revalidateLastMonitorInfo() { + mutate(key => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo") +} export function useLastMonitorInfo(currentMoment: number, previousMoment: number, timeframe: TalerCorebankApi.MonitorTimeframeParam) { const { api, config } = useBankCoreApiContext(); const { state: credentials } = useBackendState(); 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 */ -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 { } -export function ReadyView({ account, limit, goToConfirmOperation }: State.Ready): VNode<{}> { +function ShowPedingOperation(): VNode { + const { i18n } = useTranslationContext(); + const [bankState, updateBankState] = useBankState(); + if (!bankState.currentChallenge) return ; + 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 { updateBankState("currentChallenge", undefined); }}> + + To complete or cancel the operation click here + + +} + +export function ReadyView({ account, limit, goToConfirmOperation, onAuthorizationRequired }: State.Ready): VNode<{}> { return + - - + ; } -function PendingCashouts({account}: {account: string}):VNode { - const { i18n } = useTranslationContext(); - const result = useOnePendingCashouts(account) - if (!result || result instanceof TalerError || result.type !== "ok" || !result.body) { - return - } - - return - - Cashout with subject "{result.body.subject}", look for the code and complete the operation here. - - -} \ 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 | undefined>); onConfirm: undefined | (() => Promise | 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 */ -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 { +export function useComponentState({ currency, onClose, onAuthorizationRequired, }: Props): utils.RecursiveState { 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>() const [failure, setFailure] = useState | undefined>() const amount = settings.maxWithdrawalAmount @@ -88,9 +87,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive async function doConfirm(): Promise | 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 */ -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(); - 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 : 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() }} > -
- -
-
- { - setCaptchaAnswer(e.currentTarget.value) - }} - /> -
- -
-
+
+ } + + 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 ( + + +
+
+

+ + Confirm the operation + +

+ + {subtitle} + +
+ +
+ + {ch.info && +
+
{ + e.preventDefault() + }} + > +
+ +
+
+ { + setCode(e.currentTarget.value) + }} + /> +
+ +
+
+
+ + +
+
+ + {/* */} + {/* */} +
+ } +
+
+
+ + ); +} + +function ChallengeDetails({ challenge, onStart }: { challenge: ChallengeInProgess, onStart: () => void }): VNode { + const { i18n } = useTranslationContext(); + const { config } = useBankCoreApiContext(); + + return
+
+
+ + {challenge.info ? + + : + + } +
+
+

+ + Operation details + +

+
+ {((): VNode => { + switch (challenge.operation) { + case "delete-account": return
+
Account
+
{challenge.request}
+
+ case "create-transaction": { + const payto = parsePaytoUri(challenge.request.payto_uri)! + return + {challenge.request.amount && +
+
Amount
+
+ +
+
+ } + {payto.isKnown && payto.targetType === "iban" && +
+
To account
+
+ {payto.iban} +
+
+ } +
+ } + case "confirm-withdrawal": return + case "create-cashout": { + return + } + case "update-account": { + return + {challenge.request.cashout_payto_uri !== undefined && +
+
Cashout account
+
+ {challenge.request.cashout_payto_uri} +
+
+ } + {challenge.request.contact_data?.email !== undefined && +
+
Email
+
+ {challenge.request.contact_data?.email} +
+
+ } + {challenge.request.contact_data?.phone !== undefined && +
+
Phone
+
+ {challenge.request.contact_data?.phone} +
+
+ } + {challenge.request.debit_threshold !== undefined && +
+
Debit threshold
+
+ +
+
+ } + {challenge.request.is_public !== undefined && +
+
Is public
+
+ {challenge.request.is_public ? "enable" : "disable"} +
+
+ } + {challenge.request.name !== undefined && +
+
Name
+
+ {challenge.request.name} +
+
+ } + {challenge.request.tan_channel !== undefined && +
+
Authentication channel
+
+ {challenge.request.tan_channel} +
+
+ } +
+ } + case "update-password": { + return +
+
New password
+
+ {challenge.request.new_password} +
+
+
+ } + default: assertUnreachable(challenge) + } + })()} + + {challenge.info && +

+ + Challenge details + +

+ } + {challenge.sent.t_ms !== "never" && +
+
Sent at
+
+ {format(challenge.sent.t_ms, "dd/MM/yyyy HH:mm:ss")} +
+
+ } + {challenge.info && +
+
+ {((ch: TalerCorebankApi.TanChannel): VNode => { + switch (ch) { + case TalerCorebankApi.TanChannel.SMS: return To phone + case TalerCorebankApi.TanChannel.EMAIL: return To email + default: assertUnreachable(ch) + } + })(challenge.info.tan_channel)} +
+
+ {challenge.info.tan_info} +
+
+ } + +
+
+
+
+} + +function ShowWithdrawalDetails({ id }: { id: string }): VNode { + const { i18n } = useTranslationContext(); + const details = useWithdrawalDetails(id) + const { config } = useBankCoreApiContext(); + if (!details) { + return + } + if (details instanceof TalerError) { + return + } + if (details.type === "fail") { + switch (details.case) { + case HttpStatusCode.BadRequest: + case HttpStatusCode.NotFound: return + default: assertUnreachable(details) + } + } + + return +
+
Amount
+
+ +
+
+ {details.body.selected_reserve_pub !== undefined && +
+
Withdraw id
+
+ {details.body.selected_reserve_pub.substring(0, 16)}... +
+
+ } + {details.body.selected_exchange_account !== undefined && +
+
To account
+
+ {details.body.selected_exchange_account} +
+
+ } +
+} + +function ShowCashoutDetails({ request }: { request: TalerCorebankApi.CashoutRequest }): VNode { + const { i18n } = useTranslationContext(); + const info = useConversionInfo(); + if (!info) { + return + } + + if (info instanceof TalerError) { + return + } + return + {request.subject !== undefined && +
+
Subject
+
+ {request.subject} +
+
+ } +
+
Debit
+
+ +
+
+
+
Credit
+
+ +
+
+
+} \ 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({ : } 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 - } - if (withdrawalInfo instanceof TalerError) { - return - } - if (withdrawalInfo.type === "fail") { - switch (withdrawalInfo.case) { - case HttpStatusCode.NotFound: return - case HttpStatusCode.BadRequest: return - 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(); - const answer = parseInt(captchaAnswer ?? "", 10); - const [busy, setBusy] = useState>() - 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({
-
-
-

Answer the next question to authorize the wire transfer.

-
+
-
- - -
-
- +
+
+

Wire transfer details

+
+
+
+ {((): VNode => { + switch (details.account.targetType) { + case "iban": { + const p = details.account as PaytoUriIBAN + const name = p.params["receiver-name"] + return +
+
Exchange account
+
{p.iban}
+
+ {name && +
+
Exchange name
+
{p.params["receiver-name"]}
+
+ } +
+ } + case "x-taler-bank": { + const p = details.account as PaytoUriTalerBank + const name = p.params["receiver-name"] + return +
+
Exchange account
+
{p.account}
+
+ {name && +
+
Exchange name
+
{p.params["receiver-name"]}
+
+ } +
+ } + default: + return
+
Exchange account
+
{details.account.targetPath}
+
- name="answer" - id="answer" - autocomplete="off" - onChange={(e): void => { - setCaptchaAnswer(e.currentTarget.value) - }} - /> + } + })()} +
+
Amount
+
+ +
+
+
-
+
@@ -265,7 +253,6 @@ export function WithdrawalConfirmationQuestion({ Cancel
-
-
-
-

Wire transfer details

-
-
-
- {((): VNode => { - switch (details.account.targetType) { - case "iban": { - const p = details.account as PaytoUriIBAN - const name = p.params["receiver-name"] - return -
-
Exchange account
-
{p.iban}
-
- {name && -
-
Exchange name
-
{p.params["receiver-name"]}
-
- } -
- } - case "x-taler-bank": { - const p = details.account as PaytoUriTalerBank - const name = p.params["receiver-name"] - return -
-
Exchange account
-
{p.account}
-
- {name && -
-
Exchange name
-
{p.params["receiver-name"]}
-
- } -
- } - default: - return
-
Exchange account
-
{details.account.targetPath}
-
- - } - })()} -
-
Amount
-
- -
-
-
-
-
- -
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 ( { 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
@@ -197,15 +201,17 @@ export function OperationNotFound({ onClose }: { onClose: () => void }): VNode {
-
- -
+ {onClose && +
+ +
+ }
} \ 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): } - { }} account={account} /> + 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 ( - + {accountIsTheCurrentUser ? : 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({ onChange: ChangeByPurposeType[PurposeType]; purpose: PurposeType; }): VNode { - const { config } = useBankCoreApiContext() + const { config, hints } = useBankCoreApiContext() const { i18n } = useTranslationContext(); const { state: credentials } = useBackendState(); const [form, setForm] = useState({}); @@ -75,8 +76,11 @@ export function AccountForm({ 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({ 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({ 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({ 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({ - } + + } + {/* channel, not shown if old cashout api */} + {OLD_CASHOUT_API ? undefined : config.supported_tan_channels.length === 0 ? +
+ + + This server doesn't support second factor authentication. + + +
+ : +
+ +
+
+ {config.supported_tan_channels.indexOf(TanChannel.EMAIL) === -1 ? undefined : + + } + + {config.supported_tan_channels.indexOf(TanChannel.SMS) === -1 ? undefined : + + } +
+                    {JSON.stringify(form, undefined, 2)}
+                  
+
+
+
+ }
@@ -434,9 +525,6 @@ export function AccountForm({
-
-        {JSON.stringify(errors, undefined, 2)}
-      
{children} ); 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 - + 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 */ 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 = { 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>({ isDebit: true, }); const [notification, notify, handleError] = useLocalNotification() const info = useConversionInfo(); @@ -96,6 +97,9 @@ export function CreateCashout({ The bank configuration does not support cashout operations. } + + const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1 + if (!resultAccount) { return } @@ -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({ )} - {/* channel */} - {config.supported_tan_channels.length === 0 ? + {/* channel, not shown if new cashout api */} + {!OLD_CASHOUT_API ? undefined : config.supported_tan_channels.length === 0 ?
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 : -
Confirm
-
}
-- cgit v1.2.3