summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-01-11 16:41:24 -0300
committerSebastian <sebasjm@gmail.com>2024-01-11 16:41:42 -0300
commit82d4ed90caa4a6ea3bdda1fb80ccecf3dc3637f9 (patch)
tree59162f0565311e8699ca643a8bd60337ee7f582b
parentca67640f9f94f1150c0fb67c148dc79daa9d3fa0 (diff)
downloadwallet-core-82d4ed90caa4a6ea3bdda1fb80ccecf3dc3637f9.tar.gz
wallet-core-82d4ed90caa4a6ea3bdda1fb80ccecf3dc3637f9.tar.bz2
wallet-core-82d4ed90caa4a6ea3bdda1fb80ccecf3dc3637f9.zip
2fa
-rw-r--r--packages/demobank-ui/src/Routing.tsx57
-rw-r--r--packages/demobank-ui/src/components/app.tsx22
-rw-r--r--packages/demobank-ui/src/context/config.ts72
-rw-r--r--packages/demobank-ui/src/hooks/access.ts36
-rw-r--r--packages/demobank-ui/src/hooks/backend.ts30
-rw-r--r--packages/demobank-ui/src/hooks/bank-state.ts104
-rw-r--r--packages/demobank-ui/src/hooks/circuit.ts20
-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
-rw-r--r--packages/taler-util/src/http-client/bank-core.ts33
-rw-r--r--packages/taler-util/src/http-client/types.ts17
-rw-r--r--packages/taler-util/src/http-common.ts2
-rw-r--r--packages/web-util/src/components/LocalNotificationBanner.tsx18
-rw-r--r--packages/web-util/src/utils/http-impl.browser.ts11
-rw-r--r--packages/web-util/src/utils/http-impl.sw.ts13
35 files changed, 1233 insertions, 413 deletions
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 }) => (
<WithdrawalOperationPage
operationId={wopid}
+ onAuthorizationRequired={() => {
+ route(`/2fa`)
+ }}
onContinue={() => {
route("/account");
}}
@@ -113,6 +117,19 @@ export function Routing(): VNode {
onContinue={() => {
route("/account");
}}
+ onAuthorizationRequired={() => {
+ route(`/2fa`)
+ }}
+ />
+ )}
+ />
+ <Route
+ path="/2fa"
+ component={({ }: {}) => (
+ <SolveChallengePage
+ onContinue={() => {
+ route("/account");
+ }}
/>
)}
/>
@@ -122,7 +139,7 @@ export function Routing(): VNode {
/>
<Route
path="/download-stats"
- component={() => <DownloadStats
+ component={() => <DownloadStats
onCancel={() => {
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={() => (
<CreateCashout
account={username}
- onComplete={(cid) => {
- route(`/cashout/${cid}`);
+ onAuthorizationRequired={() => {
+ route(`/2fa`)
}}
onCancel={() => {
route("/account");
@@ -293,6 +334,9 @@ export function Routing(): VNode {
component={({ dest }: { dest: string }) => (
<WireTransfer
toAccount={dest}
+ onAuthorizationRequired={() => {
+ route(`/2fa`)
+ }}
onCancel={() => {
route("/account")
}}
@@ -308,8 +352,8 @@ export function Routing(): VNode {
component={() => {
if (isUserAdministrator) {
return <AdminHome
- onRegister={() => {
- route("/register");
+ onAuthorizationRequired={() => {
+ route(`/2fa`)
}}
onCreateAccount={() => {
route("/new-account")
@@ -331,6 +375,9 @@ export function Routing(): VNode {
} else {
return <AccountPage
account={username}
+ onAuthorizationRequired={() => {
+ 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 <Loading />;
-
+
const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
return (
<SettingsProvider value={settings}>
@@ -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,
}}
>
<Routing />
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 <http://www.gnu.org/licenses/>
*/
-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<Type>(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<ConfigResult>()
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 <http://www.gnu.org/licenses/>
*/
-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<TalerCoreBankResultByMethod<"getAccount">, 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<WithdrawalOperationStatus>()
@@ -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<number | undefined>(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<any> {
- 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<OpType extends string, ReqType> = {
+ 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<UpdatePasswordChallenge> =>
+ buildCodecForObject<UpdatePasswordChallenge>()
+ .property("operation", codecForConstString("update-password"))
+ .property("id", codecForString())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("UpdatePasswordChallenge");
+
+const codecForChallengeDeleteAccount = (): Codec<DeleteAccountChallenge> =>
+ buildCodecForObject<DeleteAccountChallenge>()
+ .property("operation", codecForConstString("delete-account"))
+ .property("id", codecForString())
+ .property("sent", codecForAbsoluteTime)
+ .property("request", codecForString())
+ .property("info", codecOptional(codecForTanTransmission()))
+ .build("DeleteAccountChallenge");
+
+const codecForChallengeUpdateAccount = (): Codec<UpdateAccountChallenge> =>
+ buildCodecForObject<UpdateAccountChallenge>()
+ .property("operation", codecForConstString("update-account"))
+ .property("id", codecForString())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("UpdateAccountChallenge");
+
+const codecForChallengeCreateTransaction = (): Codec<CreateTransactionChallenge> =>
+ buildCodecForObject<CreateTransactionChallenge>()
+ .property("operation", codecForConstString("create-transaction"))
+ .property("id", codecForString())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("CreateTransactionChallenge");
+
+const codecForChallengeConfirmWithdrawal = (): Codec<ConfirmWithdrawalChallenge> =>
+ buildCodecForObject<ConfirmWithdrawalChallenge>()
+ .property("operation", codecForConstString("confirm-withdrawal"))
+ .property("id", codecForString())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForString())
+ .build("ConfirmWithdrawalChallenge");
+
+const codecForChallengeCashout = (): Codec<CashoutChallenge> =>
+ buildCodecForObject<CashoutChallenge>()
+ .property("operation", codecForConstString("create-cashout"))
+ .property("id", codecForString())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("CashoutChallenge");
+
+const codecForChallenge = (): Codec<ChallengeInProgess> =>
+ buildCodecForUnion<ChallengeInProgess>()
+ .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<BankState> =>
buildCodecForObject<BankState>()
.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 <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>
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
index 50cedefa9..dbb6c7112 100644
--- a/packages/taler-util/src/http-client/bank-core.ts
+++ b/packages/taler-util/src/http-client/bank-core.ts
@@ -122,12 +122,13 @@ export class TalerCoreBankHttpClient {
* https://docs.taler.net/core/api-corebank.html#delete--accounts-$USERNAME
*
*/
- async deleteAccount(auth: UserAndToken) {
+ async deleteAccount(auth: UserAndToken, cid?: string) {
const url = new URL(`accounts/${auth.username}`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
headers: {
- Authorization: makeBearerTokenAuthHeader(auth.token)
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
},
});
switch (resp.status) {
@@ -152,13 +153,14 @@ export class TalerCoreBankHttpClient {
* https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME
*
*/
- async updateAccount(auth: UserAndToken, body: TalerCorebankApi.AccountReconfiguration) {
+ async updateAccount(auth: UserAndToken, body: TalerCorebankApi.AccountReconfiguration, cid?: string) {
const url = new URL(`accounts/${auth.username}`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "PATCH",
body,
headers: {
- Authorization: makeBearerTokenAuthHeader(auth.token)
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
},
});
switch (resp.status) {
@@ -186,13 +188,14 @@ export class TalerCoreBankHttpClient {
* https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME-auth
*
*/
- async updatePassword(auth: UserAndToken, body: TalerCorebankApi.AccountPasswordChange) {
+ async updatePassword(auth: UserAndToken, body: TalerCorebankApi.AccountPasswordChange, cid?: string) {
const url = new URL(`accounts/${auth.username}/auth`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "PATCH",
body,
headers: {
- Authorization: makeBearerTokenAuthHeader(auth.token)
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
},
});
switch (resp.status) {
@@ -328,12 +331,13 @@ export class TalerCoreBankHttpClient {
* https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-transactions
*
*/
- async createTransaction(auth: UserAndToken, body: TalerCorebankApi.CreateTransactionRequest) {
+ async createTransaction(auth: UserAndToken, body: TalerCorebankApi.CreateTransactionRequest, cid?: string) {
const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
headers: {
- Authorization: makeBearerTokenAuthHeader(auth.token)
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
},
body,
});
@@ -409,12 +413,13 @@ export class TalerCoreBankHttpClient {
* https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals-$WITHDRAWAL_ID-confirm
*
*/
- async confirmWithdrawalById(auth: UserAndToken, wid: string) {
+ async confirmWithdrawalById(auth: UserAndToken, wid: string, cid?: string) {
const url = new URL(`accounts/${auth.username}/withdrawals/${wid}/confirm`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
headers: {
- Authorization: makeBearerTokenAuthHeader(auth.token)
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
},
});
switch (resp.status) {
@@ -470,12 +475,13 @@ export class TalerCoreBankHttpClient {
* https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts
*
*/
- async createCashout(auth: UserAndToken, body: TalerCorebankApi.CashoutRequest) {
+ async createCashout(auth: UserAndToken, body: TalerCorebankApi.CashoutRequest, cid?: string) {
const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
headers: {
- Authorization: makeBearerTokenAuthHeader(auth.token)
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
},
body,
});
@@ -685,13 +691,14 @@ export class TalerCoreBankHttpClient {
}
}
- async confirmChallenge(auth: UserAndToken, cid: string) {
+ async confirmChallenge(auth: UserAndToken, cid: string, body: TalerCorebankApi.ChallengeSolve) {
const url = new URL(`accounts/${auth.username}/challenge/${cid}/confirm`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
headers: {
Authorization: makeBearerTokenAuthHeader(auth.token)
},
+ body,
});
switch (resp.status) {
case HttpStatusCode.NoContent: return opEmptySuccess()
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
index 740d4204e..75241aa30 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -358,6 +358,13 @@ export const codecForAccountData = (): Codec<TalerCorebankApi.AccountData> =>
.property("cashout_payto_uri", codecOptional(codecForPaytoString()))
.property("is_public", codecForBoolean())
.property("is_taler_exchange", codecForBoolean())
+ .property(
+ "tan_channel",
+ codecOptional(codecForEither(
+ codecForConstString(TalerCorebankApi.TanChannel.SMS),
+ codecForConstString(TalerCorebankApi.TanChannel.EMAIL),
+ )),
+ )
.build("TalerCorebankApi.AccountData");
export const codecForChallengeContactData =
@@ -740,7 +747,7 @@ export const codecForAmlDecisionDetail =
export const codecForChallenge =
(): Codec<TalerCorebankApi.Challenge> =>
buildCodecForObject<TalerCorebankApi.Challenge>()
- .property("challenge_id", codecForString())
+ .property("challenge_id", codecForNumber())
.build("TalerCorebankApi.Challenge");
export const codecForTanTransmission =
@@ -1824,9 +1831,15 @@ export namespace TalerCorebankApi {
export interface Challenge {
// Unique identifier of the challenge to solve to run this protected
// operation.
- challenge_id: string;
+ challenge_id: number;
}
+ export interface ChallengeSolve {
+ // The TAN code that solves $CHALLENGE_ID
+ tan: string;
+ }
+
+
export enum TanChannel {
SMS = "sms",
EMAIL = "email"
diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts
index 7c58b3874..68e1d2816 100644
--- a/packages/taler-util/src/http-common.ts
+++ b/packages/taler-util/src/http-common.ts
@@ -51,7 +51,7 @@ export const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
export interface HttpRequestOptions {
method?: "POST" | "PATCH" | "PUT" | "GET" | "DELETE";
- headers?: { [name: string]: string };
+ headers?: { [name: string]: string | undefined };
/**
* Timeout after which the request should be aborted.
diff --git a/packages/web-util/src/components/LocalNotificationBanner.tsx b/packages/web-util/src/components/LocalNotificationBanner.tsx
index ab46703cb..62733ab3c 100644
--- a/packages/web-util/src/components/LocalNotificationBanner.tsx
+++ b/packages/web-util/src/components/LocalNotificationBanner.tsx
@@ -1,9 +1,8 @@
import { h, Fragment, VNode } from "preact";
import { Attention } from "./Attention.js";
import { Notification } from "../index.browser.js";
-// import { useSettings } from "../hooks/settings.js";
-export function LocalNotificationBanner({ notification }: { notification?: Notification }): VNode {
+export function LocalNotificationBanner({ notification, showDebug }: { notification?: Notification, showDebug?: boolean }): VNode {
if (!notification) return <Fragment />
switch (notification.message.type) {
case "error":
@@ -17,7 +16,9 @@ export function LocalNotificationBanner({ notification }: { notification?: Notif
{notification.message.description}
</div>
}
- {/* <MaybeShowDebugInfo info={notification.message.debug} /> */}
+ {showDebug && <pre class="whitespace-break-spaces ">
+ {notification.message.debug}
+ </pre>}
</Attention>
</div>
</div>
@@ -30,14 +31,3 @@ export function LocalNotificationBanner({ notification }: { notification?: Notif
}
}
-
-// function MaybeShowDebugInfo({ info }: { info: any }): VNode {
-// const [settings] = useSettings()
-// if (settings.showDebugInfo) {
-// return <pre class="whitespace-break-spaces ">
-// {info}
-// </pre>
-// }
-// return <Fragment />
-// }
-
diff --git a/packages/web-util/src/utils/http-impl.browser.ts b/packages/web-util/src/utils/http-impl.browser.ts
index 974a7d1b8..18140ef13 100644
--- a/packages/web-util/src/utils/http-impl.browser.ts
+++ b/packages/web-util/src/utils/http-impl.browser.ts
@@ -73,10 +73,13 @@ export class BrowserHttpLib implements HttpRequestLibrary {
? encodeBody(requestBody)
: undefined;
- const requestHeadersMap = {
- ...getDefaultHeaders(requestMethod),
- ...requestHeader,
- };
+ const requestHeadersMap = getDefaultHeaders(requestMethod);
+ if (requestHeader) {
+ Object.entries(requestHeader).forEach(([key, value]) => {
+ if (value === undefined) return;
+ requestHeadersMap[key] = value
+ })
+ }
return new Promise<HttpResponse>((resolve, reject) => {
const myRequest = new XMLHttpRequest();
diff --git a/packages/web-util/src/utils/http-impl.sw.ts b/packages/web-util/src/utils/http-impl.sw.ts
index 3120309f4..3c269e695 100644
--- a/packages/web-util/src/utils/http-impl.sw.ts
+++ b/packages/web-util/src/utils/http-impl.sw.ts
@@ -68,10 +68,13 @@ export class ServiceWorkerHttpLib implements HttpRequestLibrary {
let myBody: ArrayBuffer | undefined =
requestMethod === "POST" ? encodeBody(requestBody) : undefined;
- const requestHeadersMap = {
- ...getDefaultHeaders(requestMethod),
- ...requestHeader,
- };
+ const requestHeadersMap = getDefaultHeaders(requestMethod);
+ if (requestHeader) {
+ Object.entries(requestHeader).forEach(([key, value]) => {
+ if (value === undefined) return;
+ requestHeadersMap[key] = value
+ })
+ }
const controller = new AbortController();
let timeoutId: any | undefined;
@@ -190,7 +193,7 @@ function makeJsonHandler(
requestMethod,
httpStatusCode: response.status,
},
- message,
+ message,
);
}
}