From 366cccb8fcae6a9971a1e8a9143d821e289339d1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 19 Oct 2023 02:55:57 -0300 Subject: integrate bank into the new taler-util API --- .../demobank-ui/src/components/Cashouts/index.ts | 6 +- .../demobank-ui/src/components/Cashouts/state.ts | 7 +- .../demobank-ui/src/components/Cashouts/views.tsx | 4 +- .../demobank-ui/src/components/ErrorLoading.tsx | 107 +++- packages/demobank-ui/src/components/Routing.tsx | 7 +- .../src/components/Transactions/index.ts | 4 +- .../src/components/Transactions/state.ts | 18 +- .../src/components/Transactions/test.ts | 94 ++-- packages/demobank-ui/src/components/app.tsx | 90 ++-- packages/demobank-ui/src/context/config.ts | 65 ++- packages/demobank-ui/src/declaration.d.ts | 520 ------------------- packages/demobank-ui/src/hooks/access.ts | 401 ++++----------- packages/demobank-ui/src/hooks/backend.ts | 265 +--------- packages/demobank-ui/src/hooks/circuit.ts | 548 +++++---------------- packages/demobank-ui/src/hooks/config.ts | 59 --- .../demobank-ui/src/hooks/useCredentialsChecker.ts | 135 ----- .../demobank-ui/src/pages/AccountPage/index.ts | 25 +- .../demobank-ui/src/pages/AccountPage/state.ts | 51 +- .../demobank-ui/src/pages/AccountPage/views.tsx | 34 +- packages/demobank-ui/src/pages/BankFrame.tsx | 36 +- packages/demobank-ui/src/pages/HomePage.tsx | 78 +-- packages/demobank-ui/src/pages/LoginForm.tsx | 104 ++-- .../demobank-ui/src/pages/OperationState/index.ts | 10 +- .../demobank-ui/src/pages/OperationState/state.ts | 162 +++--- .../src/pages/PaytoWireTransferForm.tsx | 77 ++- .../demobank-ui/src/pages/PublicHistoriesPage.tsx | 25 +- packages/demobank-ui/src/pages/QrCodeSection.tsx | 50 +- .../demobank-ui/src/pages/RegistrationPage.tsx | 110 +++-- .../demobank-ui/src/pages/ShowAccountDetails.tsx | 123 +++-- .../src/pages/UpdateAccountPassword.tsx | 60 ++- .../demobank-ui/src/pages/WalletWithdrawForm.tsx | 60 ++- .../src/pages/WithdrawalConfirmationQuestion.tsx | 77 +-- .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 98 ++-- packages/demobank-ui/src/pages/admin/Account.tsx | 37 +- .../demobank-ui/src/pages/admin/AccountForm.tsx | 72 +-- .../demobank-ui/src/pages/admin/AccountList.tsx | 34 +- .../src/pages/admin/CreateNewAccount.tsx | 80 ++- packages/demobank-ui/src/pages/admin/Home.tsx | 13 +- .../demobank-ui/src/pages/admin/RemoveAccount.tsx | 101 ++-- packages/demobank-ui/src/pages/business/Home.tsx | 377 +++++++------- packages/demobank-ui/src/stories.test.ts | 3 +- packages/demobank-ui/src/utils.ts | 70 +-- 42 files changed, 1559 insertions(+), 2738 deletions(-) delete mode 100644 packages/demobank-ui/src/hooks/config.ts delete mode 100644 packages/demobank-ui/src/hooks/useCredentialsChecker.ts (limited to 'packages/demobank-ui') diff --git a/packages/demobank-ui/src/components/Cashouts/index.ts b/packages/demobank-ui/src/components/Cashouts/index.ts index 05ef1f3b4..ae020cef6 100644 --- a/packages/demobank-ui/src/components/Cashouts/index.ts +++ b/packages/demobank-ui/src/components/Cashouts/index.ts @@ -18,7 +18,7 @@ import { HttpError, utils } from "@gnu-taler/web-util/browser"; import { Loading } from "../Loading.js"; // import { compose, StateViewMap } from "../../utils/index.js"; // import { wxApi } from "../../wxApi.js"; -import { AbsoluteTime, AmountJson } from "@gnu-taler/taler-util"; +import { AbsoluteTime, AmountJson, TalerCorebankApi, TalerError } from "@gnu-taler/taler-util"; import { useComponentState } from "./state.js"; import { LoadingUriView, ReadyView } from "./views.js"; @@ -37,7 +37,7 @@ export namespace State { export interface LoadingUriError { status: "loading-error"; - error: HttpError; + error: TalerError; } export interface BaseInfo { @@ -46,7 +46,7 @@ export namespace State { export interface Ready extends BaseInfo { status: "ready"; error: undefined; - cashouts: SandboxBackend.Circuit.CashoutStatusResponseWithId[]; + cashouts: (TalerCorebankApi.CashoutStatusResponse & { id: string })[]; onSelected: (id: string) => void; } } diff --git a/packages/demobank-ui/src/components/Cashouts/state.ts b/packages/demobank-ui/src/components/Cashouts/state.ts index 124f9bf9c..47ad0a297 100644 --- a/packages/demobank-ui/src/components/Cashouts/state.ts +++ b/packages/demobank-ui/src/components/Cashouts/state.ts @@ -14,18 +14,19 @@ GNU Taler; see the file COPYING. If not, see */ +import { TalerError } from "@gnu-taler/taler-util"; import { useCashouts } from "../../hooks/circuit.js"; import { Props, State } from "./index.js"; export function useComponentState({ account, onSelected }: Props): State { const result = useCashouts(account); - if (result.loading) { + if (!result) { return { status: "loading", error: undefined, }; } - if (!result.ok) { + if (result instanceof TalerError) { return { status: "loading-error", error: result, @@ -35,7 +36,7 @@ export function useComponentState({ account, onSelected }: Props): State { return { status: "ready", error: undefined, - cashouts: result.data, + cashouts: result.body.cashouts, onSelected, }; } diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx index a32deb266..0602f507e 100644 --- a/packages/demobank-ui/src/components/Cashouts/views.tsx +++ b/packages/demobank-ui/src/components/Cashouts/views.tsx @@ -57,10 +57,10 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode { {cashouts.map((item, idx) => { return ( - {format(item.creation_time, "dd/MM/yyyy HH:mm:ss")} + {item.creation_time.t_s === "never" ? i18n.str`never` : format(item.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")} {item.confirmation_time - ? format(item.confirmation_time, "dd/MM/yyyy HH:mm:ss") + ? item.confirmation_time.t_s === "never" ? i18n.str`never` : format(item.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss") : "-"} diff --git a/packages/demobank-ui/src/components/ErrorLoading.tsx b/packages/demobank-ui/src/components/ErrorLoading.tsx index ee62671ce..84e72c5a1 100644 --- a/packages/demobank-ui/src/components/ErrorLoading.tsx +++ b/packages/demobank-ui/src/components/ErrorLoading.tsx @@ -15,15 +15,106 @@ GNU Taler; see the file COPYING. If not, see */ -import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { TalerError, TalerErrorCode } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; import { Attention } from "./Attention.js"; -import { TranslatedString } from "@gnu-taler/taler-util"; +import { assertUnreachable } from "./Routing.js"; -export function ErrorLoading({ error }: { error: HttpError }): VNode { +export function ErrorLoading({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode { const { i18n } = useTranslationContext() - return ( -

Got status "{error.info.status}" on {error.info.url}

-
- ); + switch (error.errorDetail.code) { + ////////////////// + // Every error that can be produce in a Http Request + ////////////////// + case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { + if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) { + const { requestMethod, requestUrl, timeoutMs } = error.errorDetail + return + {error.message} + {showDetail && +
+              {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)}
+            
+ } +
+ } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: { + if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) { + const { requestMethod, requestUrl, throttleStats } = error.errorDetail + return + {error.message} + {showDetail && +
+              {JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2)}
+            
+ } +
+ } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: { + if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) { + const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail + return + {error.message} + {showDetail && +
+              {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2)}
+            
+ } +
+ } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_NETWORK_ERROR: { + if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) { + const { requestMethod, requestUrl } = error.errorDetail + return + {error.message} + {showDetail && +
+              {JSON.stringify({ requestMethod, requestUrl }, undefined, 2)}
+            
+ } +
+ } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { + if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) { + const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail + return + {error.message} + {showDetail && +
+              {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2)}
+            
+ } +
+ } + assertUnreachable(1 as never) + } + ////////////////// + // Every other error + ////////////////// + // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { + // return + // + // } + ////////////////// + // Default message for unhandled case + ////////////////// + default: return + {error.message} + {showDetail && +
+          {JSON.stringify(error.errorDetail, undefined, 2)}
+        
+ } +
+ } } + diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx index aafc95687..04cf96190 100644 --- a/packages/demobank-ui/src/components/Routing.tsx +++ b/packages/demobank-ui/src/components/Routing.tsx @@ -32,8 +32,8 @@ import { bankUiSettings } from "../settings.js"; export function Routing(): VNode { const history = createHashHistory(); const backend = useBackendContext(); - const {i18n} = useTranslationContext(); - + const { i18n } = useTranslationContext(); + if (backend.state.status === "loggedOut") { return @@ -143,9 +143,6 @@ export function Routing(): VNode { onRegister={() => { route("/register"); }} - onLoadNotOk={() => { - route("/account"); - }} /> )} /> diff --git a/packages/demobank-ui/src/components/Transactions/index.ts b/packages/demobank-ui/src/components/Transactions/index.ts index 9df1a70e5..3c4fb5ce9 100644 --- a/packages/demobank-ui/src/components/Transactions/index.ts +++ b/packages/demobank-ui/src/components/Transactions/index.ts @@ -18,7 +18,7 @@ import { HttpError, utils } from "@gnu-taler/web-util/browser"; import { Loading } from "../Loading.js"; // import { compose, StateViewMap } from "../../utils/index.js"; // import { wxApi } from "../../wxApi.js"; -import { AbsoluteTime, AmountJson } from "@gnu-taler/taler-util"; +import { AbsoluteTime, AmountJson, TalerError } from "@gnu-taler/taler-util"; import { useComponentState } from "./state.js"; import { LoadingUriView, ReadyView } from "./views.js"; @@ -36,7 +36,7 @@ export namespace State { export interface LoadingUriError { status: "loading-error"; - error: HttpError; + error: TalerError; } export interface BaseInfo { diff --git a/packages/demobank-ui/src/components/Transactions/state.ts b/packages/demobank-ui/src/components/Transactions/state.ts index 4b62b005e..c85fba85b 100644 --- a/packages/demobank-ui/src/components/Transactions/state.ts +++ b/packages/demobank-ui/src/components/Transactions/state.ts @@ -14,34 +14,34 @@ GNU Taler; see the file COPYING. If not, see */ -import { AbsoluteTime, Amounts, parsePaytoUri } from "@gnu-taler/taler-util"; +import { AbsoluteTime, Amounts, TalerError, parsePaytoUri } from "@gnu-taler/taler-util"; import { useTransactions } from "../../hooks/access.js"; import { Props, State, Transaction } from "./index.js"; export function useComponentState({ account }: Props): State { const result = useTransactions(account); - if (result.loading) { + if (!result) { return { status: "loading", error: undefined, }; } - if (!result.ok) { + if (result instanceof TalerError) { return { status: "loading-error", error: result, }; } - const transactions = result.data.transactions + const transactions = result.data.type === "fail" ? [] : result.data.body.transactions .map((tx) => { const negative = tx.direction === "debit"; const cp = parsePaytoUri(negative ? tx.creditor_payto_uri : tx.debtor_payto_uri); const counterpart = (cp === undefined || !cp.isKnown ? undefined : - cp.targetType === "iban" ? cp.iban : - cp.targetType === "x-taler-bank" ? cp.account : - cp.targetType === "bitcoin" ? `${cp.targetPath.substring(0, 6)}...` : undefined) ?? + cp.targetType === "iban" ? cp.iban : + cp.targetType === "x-taler-bank" ? cp.account : + cp.targetType === "bitcoin" ? `${cp.targetPath.substring(0, 6)}...` : undefined) ?? "unkown"; const when = AbsoluteTime.fromProtocolTimestamp(tx.date); @@ -61,7 +61,7 @@ export function useComponentState({ account }: Props): State { status: "ready", error: undefined, transactions, - onNext: result.isReachingEnd ? undefined : result.loadMore, - onPrev: result.isReachingStart ? undefined : result.loadMorePrev, + onNext: result.isLastPage ? undefined : result.loadMore, + onPrev: result.isFirstPage ? undefined : result.loadMorePrev, }; } diff --git a/packages/demobank-ui/src/components/Transactions/test.ts b/packages/demobank-ui/src/components/Transactions/test.ts index 9b713bbc5..a206d9f52 100644 --- a/packages/demobank-ui/src/components/Transactions/test.ts +++ b/packages/demobank-ui/src/components/Transactions/test.ts @@ -26,7 +26,7 @@ import { expect } from "chai"; import { TRANSACTION_API_EXAMPLE } from "../../endpoints.js"; import { Props } from "./index.js"; import { useComponentState } from "./state.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { HttpStatusCode, TalerError, TalerErrorCode } from "@gnu-taler/taler-util"; describe("Transaction states", () => { it("should query backend and render transactions", async () => { @@ -116,47 +116,47 @@ describe("Transaction states", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); }); - it("should show error message on not found", async () => { - const env = new SwrMockEnvironment(); - - const props: Props = { - account: "myAccount", - }; - - env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, { - response: { - error: { - description: "Transaction page 0 could not be retrieved.", - }, - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - useComponentState, - props, - [ - ({ status, error }) => { - expect(status).equals("loading"); - expect(error).undefined; - }, - ({ status, error }) => { - expect(status).equals("loading-error"); - if (error === undefined || error.type !== ErrorType.CLIENT) { - throw Error("not the expected error"); - } - expect(error.payload).deep.equal({ - error: { - description: "Transaction page 0 could not be retrieved.", - }, - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); + // it("should show error message on not found", async () => { + // const env = new SwrMockEnvironment(); + + // const props: Props = { + // account: "myAccount", + // }; + + // env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, { + // response: { + // error: { + // description: "Transaction page 0 could not be retrieved.", + // }, + // }, + // }); + + // const hookBehavior = await tests.hookBehaveLikeThis( + // useComponentState, + // props, + // [ + // ({ status, error }) => { + // expect(status).equals("loading"); + // expect(error).undefined; + // }, + // ({ status, error }) => { + // expect(status).equals("loading-error"); + // if (error === undefined || error.type !== ErrorType.CLIENT) { + // throw Error("not the expected error"); + // } + // expect(error.payload).deep.equal({ + // error: { + // description: "Transaction page 0 could not be retrieved.", + // }, + // }); + // }, + // ], + // env.buildTestingContext(), + // ); + + // expect(hookBehavior).deep.eq({ result: "ok" }); + // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + // }); it("should show error message on server error", async () => { const env = new SwrMockEnvironment(); @@ -168,7 +168,7 @@ describe("Transaction states", () => { env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, { response: { error: { - description: "Transaction page 0 could not be retrieved.", + code: TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED, }, }, }); @@ -183,14 +183,10 @@ describe("Transaction states", () => { }, ({ status, error }) => { expect(status).equals("loading-error"); - if (error === undefined || error.type !== ErrorType.SERVER) { + if (error === undefined || !error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) { throw Error("not the expected error"); } - expect(error.payload).deep.equal({ - error: { - description: "Transaction page 0 could not be retrieved.", - }, - }); + expect(error.errorDetail.code).deep.equal(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED); }, ], env.buildTestingContext(), diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx index 7cf658681..beb24da57 100644 --- a/packages/demobank-ui/src/components/app.tsx +++ b/packages/demobank-ui/src/components/app.tsx @@ -15,47 +15,26 @@ */ import { - LibtoolVersion, + canonicalizeBaseUrl, getGlobalLogLevel, - setGlobalLogLevelFromString, + setGlobalLogLevelFromString } from "@gnu-taler/taler-util"; -import { TranslationProvider, useApiContext } from "@gnu-taler/web-util/browser"; -import { ComponentChildren, Fragment, FunctionalComponent, VNode, h } from "preact"; +import { TranslationProvider } from "@gnu-taler/web-util/browser"; +import { Fragment, FunctionalComponent, h } from "preact"; import { SWRConfig } from "swr"; -import { BackendStateProvider, useBackendContext } from "../context/backend.js"; +import { BackendStateProvider } from "../context/backend.js"; +import { BankCoreApiProvider } from "../context/config.js"; import { strings } from "../i18n/strings.js"; +import { bankUiSettings } from "../settings.js"; import { Routing } from "./Routing.js"; -import { useEffect, useState } from "preact/hooks"; -import { Loading } from "./Loading.js"; -import { getInitialBackendBaseURL } from "../hooks/backend.js"; -import { BANK_INTEGRATION_PROTOCOL_VERSION, useConfigState } from "../hooks/config.js"; -import { ErrorLoading } from "./ErrorLoading.js"; -import { BankFrame } from "../pages/BankFrame.js"; -import { ConfigStateProvider } from "../context/config.js"; const WITH_LOCAL_STORAGE_CACHE = false; -/** - * FIXME: - * - * - INPUT elements have their 'required' attribute ignored. - * - * - the page needs a "home" button that either redirects to - * the profile page (when the user is logged in), or to - * the very initial home page. - * - * - histories 'pages' are grouped in UL elements that cause - * the rendering to visually separate each UL. History elements - * should instead line up without any separation caused by - * a implementation detail. - * - * - Many strings need to be i18n-wrapped. - */ - const App: FunctionalComponent = () => { + const baseUrl = getInitialBackendBaseURL(); return ( - + { > - + ); }; + (window as any).setGlobalLogLevelFromString = setGlobalLogLevelFromString; (window as any).getGlobalLevel = getGlobalLogLevel; -function VersionCheck({ children }: { children: ComponentChildren }): VNode { - const checked = useConfigState() - - if (checked === undefined) { - return - } - if (checked.type === "wrong") { - return - the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}" - - } - if (checked.type === "ok") { - return {children} - } - - return - - -} - function localStorageProvider(): Map { const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]")); @@ -104,3 +64,31 @@ function localStorageProvider(): Map { } export default App; + +function getInitialBackendBaseURL(): string { + const overrideUrl = + typeof localStorage !== "undefined" + ? localStorage.getItem("bank-base-url") + : undefined; + let result: string; + if (!overrideUrl) { + //normal path + if (!bankUiSettings.backendBaseURL) { + console.error( + "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", + ); + result = window.origin + } else { + result = bankUiSettings.backendBaseURL; + } + } else { + // testing/development path + result = overrideUrl + } + try { + return canonicalizeBaseUrl(result) + } catch (e) { + //fall back + return canonicalizeBaseUrl(window.origin) + } +} \ No newline at end of file diff --git a/packages/demobank-ui/src/context/config.ts b/packages/demobank-ui/src/context/config.ts index a2cde18eb..013d8922e 100644 --- a/packages/demobank-ui/src/context/config.ts +++ b/packages/demobank-ui/src/context/config.ts @@ -14,36 +14,71 @@ GNU Taler; see the file COPYING. If not, see */ +import { TalerCorebankApi, TalerCoreBankHttpClient, TalerError } from "@gnu-taler/taler-util"; +import { BrowserHttpLib, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, createContext, h, VNode } from "preact"; -import { useContext } from "preact/hooks"; +import { useContext, useEffect, useState } from "preact/hooks"; +import { ErrorLoading } from "../components/ErrorLoading.js"; /** * * @author Sebastian Javier Marchano (sebasjm) */ -export type Type = Required; - -const initial: Type = { - name: "", - version: "0:0:0", - currency_fraction_digits: 2, - currency_fraction_limit: 2, - fiat_currency: "", - have_cashout: false, +export type Type = { + url: URL, + config: TalerCorebankApi.Config, + api: TalerCoreBankHttpClient, }; -const Context = createContext(initial); -export const useConfigContext = (): Type => useContext(Context); +const Context = createContext(undefined as any); + +export const useBankCoreApiContext = (): Type => useContext(Context); + +export type ConfigResult = undefined + | { type: "ok", config: TalerCorebankApi.Config } + | { type: "incompatible", result: TalerCorebankApi.Config, supported: string } + | { type: "error", error: TalerError } -export const ConfigStateProvider = ({ - value, +export const BankCoreApiProvider = ({ + baseUrl, children, }: { - value: Type, + baseUrl: string, children: ComponentChildren; }): VNode => { + const [checked, setChecked] = useState() + const { i18n } = useTranslationContext(); + const url = new URL(baseUrl) + const api = new TalerCoreBankHttpClient(url.href, new BrowserHttpLib()) + useEffect(() => { + api.getConfig() + .then((resp) => { + if (api.isCompatible(resp.body.version)) { + setChecked({ type: "ok", config: resp.body }); + } else { + setChecked({ type: "incompatible", result: resp.body, supported: api.PROTOCOL_VERSION }) + } + }) + .catch((error: unknown) => { + if (error instanceof TalerError) { + setChecked({ type: "error", error }); + } + }); + }, []); + if (checked === undefined) { + return h("div", {}, "loading...") + } + if (checked.type === "error") { + return h(ErrorLoading, { error: checked.error, showDetail: true }) + } + if (checked.type === "incompatible") { + return 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 + } return h(Context.Provider, { value, children, diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts index 5c55cfade..c8ba3d576 100644 --- a/packages/demobank-ui/src/declaration.d.ts +++ b/packages/demobank-ui/src/declaration.d.ts @@ -31,525 +31,5 @@ declare module "*.png" { export default content; } -/********************************************** - * Type definitions for states and API calls. * - *********************************************/ - -/** - * Request body of POST /transactions. - * - * If the amount appears twice: both as a Payto parameter and - * in the JSON dedicate field, the one on the Payto URI takes - * precedence. - */ -interface TransactionRequestType { - paytoUri: string; - amount?: string; // with currency. -} - -/** - * Request body of /register. - */ -interface CredentialsRequestType { - username?: string; - password?: string; - repeatPassword?: string; -} - -/** - * Request body of /register. - */ -// interface LoginRequestType { -// username: string; -// password: string; -// } - -interface WireTransferRequestType { - iban?: string; - subject?: string; - amount?: string; -} - -type HashCode = string; -type EddsaPublicKey = string; -type EddsaSignature = string; -type WireTransferIdentifierRawP = string; -type RelativeTime = { - d_us: number | "forever" -}; -type ImageDataUrl = string; - -interface WithId { - id: string; -} - -interface Timestamp { - // Milliseconds since epoch, or the special - // value "forever" to represent an event that will - // never happen. - t_s: number | "never"; -} -interface Duration { - d_us: number | "forever"; -} - -interface WithId { - id: string; -} - -type Amount = string; -type UUID = string; -type Integer = number; - -namespace SandboxBackend { - export interface Config { - // Name of this API, always "circuit". - name: string; - // API version in the form $n:$n:$n - version: string; - // If 'true', the server provides local currency - // conversion support. - // If missing or false, some parts of the API - // are not supported and return 404. - have_cashout?: boolean; - - // Fiat currency. That is the currency in which - // cash-out operations ultimately wire money. - // Only applicable if have_cashout=true. - fiat_currency?: string; - - // How many digits should the amounts be rendered - // with by default. Small capitals should - // be used to render fractions beyond the number - // given here (like on gas stations). - currency_fraction_digits?: number; - - // How many decimal digits an operation can - // have. Wire transfers with more decimal - // digits will not be accepted. - currency_fraction_limit?: number; - } - interface RatiosAndFees { - // Exchange rate to buy the circuit currency from fiat. - buy_at_ratio: number; - // Exchange rate to sell the circuit currency for fiat. - sell_at_ratio: number; - // Fee to subtract after applying the buy ratio. - buy_in_fee: number; - // Fee to subtract after applying the sell ratio. - sell_out_fee: number; - } - - export interface SandboxError { - error?: SandboxErrorDetail; - } - interface SandboxErrorDetail { - // String enum classifying the error. - type: ErrorType; - - // Human-readable error description. - description: string; - } - enum ErrorType { - /** - * This error can be related to a business operation, - * a non-existent object requested by the client, or - * even when the bank itself fails. - */ - SandboxError = "sandbox-error", - - /** - * It is the error type thrown by helper functions - * from the Util library. Those are used by both - * Sandbox and Nexus, therefore the actual meaning - * must be carried by the error 'message' field. - */ - UtilError = "util-error", - } - - - type EmailAddress = string; - type PhoneNumber = string; - - namespace CoreBank { - - interface BankAccountCreateWithdrawalRequest { - // Amount to withdraw. - amount: Amount; - } - interface BankAccountCreateWithdrawalResponse { - // ID of the withdrawal, can be used to view/modify the withdrawal operation. - withdrawal_id: string; - - // URI that can be passed to the wallet to initiate the withdrawal. - taler_withdraw_uri: string; - } - interface BankAccountGetWithdrawalResponse { - // Amount that will be withdrawn with this withdrawal operation. - amount: Amount; - - // Was the withdrawal aborted? - aborted: boolean; - - // Has the withdrawal been confirmed by the bank? - // The wire transfer for a withdrawal is only executed once - // both confirmation_done is true and selection_done is true. - confirmation_done: boolean; - - // Did the wallet select reserve details? - selection_done: boolean; - - // Reserve public key selected by the exchange, - // only non-null if selection_done is true. - selected_reserve_pub: string | null; - - // Exchange account selected by the wallet, or by the bank - // (with the default exchange) in case the wallet did not provide one - // through the Integration API. - selected_exchange_account: string | null; - } - - interface BankAccountTransactionsResponse { - transactions: BankAccountTransactionInfo[]; - } - - interface BankAccountTransactionInfo { - creditor_payto_uri: string; - debtor_payto_uri: string; - - amount: Amount; - direction: "debit" | "credit"; - - subject: string; - - // Transaction unique ID. Matches - // $transaction_id from the URI. - row_id: number; - date: Timestamp; - } - - interface CreateBankAccountTransactionCreate { - // Address in the Payto format of the wire transfer receiver. - // It needs at least the 'message' query string parameter. - payto_uri: string; - - // Transaction amount (in the $currency:x.y format), optional. - // However, when not given, its value must occupy the 'amount' - // query string parameter of the 'payto' field. In case it - // is given in both places, the paytoUri's takes the precedence. - amount?: string; - } - - interface RegisterAccountRequest { - // Username - username: string; - - // Password. - password: string; - - // Legal name of the account owner - name: string; - - // Defaults to false. - is_public?: boolean; - - // Is this a taler exchange account? - // If true: - // - incoming transactions to the account that do not - // have a valid reserve public key are automatically - // - the account provides the taler-wire-gateway-api endpoints - // Defaults to false. - is_taler_exchange?: boolean; - - // Addresses where to send the TAN for transactions. - // Currently only used for cashouts. - // If missing, cashouts will fail. - // In the future, might be used for other transactions - // as well. - challenge_contact_data?: ChallengeContactData; - - // 'payto' address pointing a bank account - // external to the libeufin-bank. - // Payments will be sent to this bank account - // when the user wants to convert the local currency - // back to fiat currency outside libeufin-bank. - cashout_payto_uri?: string; - - // Internal payto URI of this bank account. - // Used mostly for testing. - internal_payto_uri?: string; - } - interface ChallengeContactData { - - // E-Mail address - email?: EmailAddress; - - // Phone number. - phone?: PhoneNumber; - } - - interface AccountReconfiguration { - - // Addresses where to send the TAN for transactions. - // Currently only used for cashouts. - // If missing, cashouts will fail. - // In the future, might be used for other transactions - // as well. - challenge_contact_data?: ChallengeContactData; - - // 'payto' address pointing a bank account - // external to the libeufin-bank. - // Payments will be sent to this bank account - // when the user wants to convert the local currency - // back to fiat currency outside libeufin-bank. - cashout_address?: string; - - // Legal name associated with $username. - // When missing, the old name is kept. - name?: string; - - // If present, change the is_exchange configuration. - // See RegisterAccountRequest - is_exchange?: boolean; - } - - - interface AccountPasswordChange { - - // New password. - new_password: string; - } - interface PublicAccountsResponse { - public_accounts: PublicAccount[]; - } - interface PublicAccount { - payto_uri: string; - - balance: Balance; - - // The account name (=username) of the - // libeufin-bank account. - account_name: string; - } - - interface ListBankAccountsResponse { - accounts: AccountMinimalData[]; - } - interface Balance { - amount: Amount; - credit_debit_indicator: "credit" | "debit"; - } - interface AccountMinimalData { - // Username - username: string; - - // Legal name of the account owner. - name: string; - - // current balance of the account - balance: Balance; - - // Number indicating the max debit allowed for the requesting user. - debit_threshold: Amount; - } - - interface AccountData { - // Legal name of the account owner. - name: string; - - // Available balance on the account. - balance: Balance; - - // payto://-URI of the account. - payto_uri: string; - - // Number indicating the max debit allowed for the requesting user. - debit_threshold: Amount; - - contact_data?: ChallengeContactData; - - // 'payto' address pointing the bank account - // where to send cashouts. This field is optional - // because not all the accounts are required to participate - // in the merchants' circuit. One example is the exchange: - // that never cashouts. Registering these accounts can - // be done via the access API. - cashout_payto_uri?: string; - } - - } - - namespace Circuit { - interface CircuitAccountRequest { - // Username - username: string; - - // Password. - password: string; - - // Addresses where to send the TAN. If - // this field is missing, then the cashout - // won't succeed. - contact_data: CircuitContactData; - - // Legal subject owning the account. - name: string; - - // 'payto' address pointing the bank account - // where to send payments, in case the user - // wants to convert the local currency back - // to fiat. - cashout_address: string; - - // IBAN of this bank account, which is therefore - // internal to the circuit. Randomly generated, - // when it is not given. - internal_iban?: string; - } - interface CircuitContactData { - // E-Mail address - email?: string; - - // Phone number. - phone?: string; - } - interface CircuitAccountReconfiguration { - // Addresses where to send the TAN. - contact_data: CircuitContactData; - - // 'payto' address pointing the bank account - // where to send payments, in case the user - // wants to convert the local currency back - // to fiat. - cashout_address: string; - } - interface AccountPasswordChange { - // New password. - new_password: string; - } - - interface CircuitAccounts { - customers: CircuitAccountMinimalData[]; - } - interface CircuitAccountMinimalData { - // Username - username: string; - - // Legal subject owning the account. - name: string; - - // current balance of the account - balance: Balance; - } - - interface CircuitAccountData { - // Username - username: string; - - // IBAN hosted at Libeufin Sandbox - iban: string; - - contact_data: CircuitContactData; - - // Legal subject owning the account. - name: string; - - // 'payto' address pointing the bank account - // where to send cashouts. - cashout_address: string; - } - interface CashoutEstimate { - // Amount that the user will get deducted from their regional - // bank account, according to the 'amount_credit' value. - amount_debit: Amount; - // Amount that the user will receive in their fiat - // bank account, according to 'amount_debit'. - amount_credit: Amount; - } - interface CashoutRequest { - // Optional subject to associate to the - // cashout operation. This data will appear - // as the incoming wire transfer subject in - // the user's external bank account. - subject?: string; - - // That is the plain amount that the user specified - // to cashout. Its $currency is the circuit currency. - amount_debit: Amount; - - // That is the amount that will effectively be - // transferred by the bank to the user's bank - // account, that is external to the circuit. - // It is expressed in the fiat currency and - // is calculated after the cashout fee and the - // exchange rate. See the /cashout-rates call. - amount_credit: Amount; - - // Which channel the TAN should be sent to. If - // this field is missing, it defaults to SMS. - // The default choice prefers to change the communication - // channel respect to the one used to issue this request. - tan_channel?: TanChannel; - } - interface CashoutPending { - // UUID identifying the operation being created - // and now waiting for the TAN confirmation. - uuid: string; - } - interface CashoutConfirm { - // the TAN that confirms $cashoutId. - tan: string; - } - interface Config { - // Name of this API, always "circuit". - name: string; - // API version in the form $n:$n:$n - version: string; - // Contains ratios and fees related to buying - // and selling the circuit currency. - ratios_and_fees: RatiosAndFees; - // Fiat currency. That is the currency in which - // cash-out operations ultimately wire money. - fiat_currency: string; - } - interface RatiosAndFees { - // Exchange rate to buy the circuit currency from fiat. - buy_at_ratio: float; - // Exchange rate to sell the circuit currency for fiat. - sell_at_ratio: float; - // Fee to subtract after applying the buy ratio. - buy_in_fee: float; - // Fee to subtract after applying the sell ratio. - sell_out_fee: float; - } - interface Cashouts { - // Every string represents a cash-out operation UUID. - cashouts: string[]; - } - interface CashoutStatusResponse { - status: CashoutStatus; - // Amount debited to the circuit bank account. - amount_debit: Amount; - // Amount credited to the external bank account. - amount_credit: Amount; - // Transaction subject. - subject: string; - // Circuit bank account that created the cash-out. - account: string; - // Fiat bank account that will receive the cashed out amount. - cashout_address: string; - // Ratios and fees related to this cash-out at the time - // when the operation was created. - ratios_and_fees: RatiosAndFees; - // Time when the cash-out was created. - creation_time: number; // milliseconds since the Unix epoch - // Time when the cash-out was confirmed via its TAN. - // Missing or null, when the operation wasn't confirmed yet. - confirmation_time?: number | null; // milliseconds since the Unix epoch - } - type CashoutStatusResponseWithId = CashoutStatusResponse & { id: string }; - } -} - declare const __VERSION__: string; declare const __GIT_HASH__: string; diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts index 154c43ae6..2533d32fe 100644 --- a/packages/demobank-ui/src/hooks/access.ts +++ b/packages/demobank-ui/src/hooks/access.ts @@ -14,168 +14,31 @@ GNU Taler; see the file COPYING. If not, see */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { useBackendContext } from "../context/backend.js"; +import { AccessToken, TalerCoreBankResultByMethod, TalerHttpError } from "@gnu-taler/taler-util"; +import { useState } from "preact/hooks"; import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; -import { - useAuthenticatedBackend, - useMatchMutate, - usePublicBackend, -} from "./backend.js"; +import { useBackendState } from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import _useSWR, { SWRHook } from "swr"; -import { Amounts } from "@gnu-taler/taler-util"; +import { useBankCoreApiContext } from "../context/config.js"; const useSWR = _useSWR as unknown as SWRHook; -export function useAccessAPI(): AccessAPI { - const mutateAll = useMatchMutate(); - const { request } = useAuthenticatedBackend(); - const { state } = useBackendContext(); - if (state.status === "loggedOut") { - throw Error("access-api can't be used when the user is not logged In"); - } - const account = state.username; - - const createWithdrawal = async ( - data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest, - ): Promise< - HttpResponseOk - > => { - const res = - await request( - `accounts/${account}/withdrawals`, - { - method: "POST", - data, - contentType: "json", - }, - ); - return res; - }; - const createTransaction = async ( - data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate, - ): Promise> => { - const res = await request( - `accounts/${account}/transactions`, - { - method: "POST", - data, - contentType: "json", - }, - ); - await mutateAll(/.*accounts\/.*/); - return res; - }; - const deleteAccount = async (): Promise> => { - const res = await request(`accounts/${account}`, { - method: "DELETE", - contentType: "json", - }); - await mutateAll(/.*accounts\/.*/); - return res; - }; - - return { - createWithdrawal, - createTransaction, - deleteAccount, - }; -} - -export function useAccessAnonAPI(): AccessAnonAPI { - const mutateAll = useMatchMutate(); - const { request } = useAuthenticatedBackend(); - - const abortWithdrawal = async (id: string): Promise> => { - const res = await request(`withdrawals/${id}/abort`, { - method: "POST", - contentType: "json", - }); - await mutateAll(/.*withdrawals\/.*/); - return res; - }; - const confirmWithdrawal = async ( - id: string, - ): Promise> => { - const res = await request(`withdrawals/${id}/confirm`, { - method: "POST", - contentType: "json", - }); - await mutateAll(/.*withdrawals\/.*/); - return res; - }; - - return { - abortWithdrawal, - confirmWithdrawal, - }; -} - -export function useTestingAPI(): TestingAPI { - const mutateAll = useMatchMutate(); - const { request: noAuthRequest } = usePublicBackend(); - const register = async ( - data: SandboxBackend.CoreBank.RegisterAccountRequest, - ): Promise> => { - // FIXME: This API is deprecated. The normal account registration API should be used instead. - const res = await noAuthRequest(`accounts`, { - method: "POST", - data, - contentType: "json", - }); - await mutateAll(/.*accounts\/.*/); - return res; - }; - - return { register }; -} - -export interface TestingAPI { - register: ( - data: SandboxBackend.CoreBank.RegisterAccountRequest, - ) => Promise>; -} - -export interface AccessAPI { - createWithdrawal: ( - data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest, - ) => Promise< - HttpResponseOk - >; - createTransaction: ( - data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate, - ) => Promise>; - deleteAccount: () => Promise>; -} -export interface AccessAnonAPI { - abortWithdrawal: (wid: string) => Promise>; - confirmWithdrawal: (wid: string) => Promise>; -} export interface InstanceTemplateFilter { //FIXME: add filter to the template list position?: string; } -export function useAccountDetails( - account: string, -): HttpResponse< - SandboxBackend.CoreBank.AccountData, - SandboxBackend.SandboxError -> { - const { fetcher } = useAuthenticatedBackend(); - - const { data, error } = useSWR< - HttpResponseOk, - RequestError - >([`accounts/${account}`], fetcher, { +export function useAccountDetails(account: string) { + const { state: credentials } = useBackendState(); + const { api } = useBankCoreApiContext(); + + async function fetcher([username, token]: [string, AccessToken]) { + return await api.getAccount({ username, token }) + } + const token = credentials.status !== "loggedIn" ? undefined : credentials.token + const { data, error } = useSWR, TalerHttpError>([account, token], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -187,26 +50,22 @@ export function useAccountDetails( keepPreviousData: true, }); - if (data) { - return data; - } - if (error) return error.cause; - return { loading: true }; + if (data) return data + if (error) return error; + return undefined; } // FIXME: should poll -export function useWithdrawalDetails( - wid: string, -): HttpResponse< - SandboxBackend.CoreBank.BankAccountGetWithdrawalResponse, - SandboxBackend.SandboxError -> { - const { fetcher } = useAuthenticatedBackend(); - - const { data, error } = useSWR< - HttpResponseOk, - RequestError - >([`withdrawals/${wid}`], fetcher, { +export function useWithdrawalDetails(wid: string) { + // const { state: credentials } = useBackendState(); + const { api } = useBankCoreApiContext(); + + async function fetcher(wid: string) { + return await api.getWithdrawalById(wid) + } + + const { data, error } = useSWR, TalerHttpError>( + [wid], fetcher, { refreshInterval: 1000, refreshWhenHidden: false, revalidateOnFocus: false, @@ -218,25 +77,22 @@ export function useWithdrawalDetails( keepPreviousData: true, }); - // if (isValidating) return { loading: true, data: data?.data }; if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } -export function useTransactionDetails( - account: string, - tid: string, -): HttpResponse< - SandboxBackend.CoreBank.BankAccountTransactionInfo, - SandboxBackend.SandboxError -> { - const { paginatedFetcher } = useAuthenticatedBackend(); - - const { data, error } = useSWR< - HttpResponseOk, - RequestError - >([`accounts/${account}/transactions/${tid}`], paginatedFetcher, { +export function useTransactionDetails(account: string, tid: number) { + const { state: credentials } = useBackendState(); + const token = credentials.status !== "loggedIn" ? undefined : credentials.token + const { api } = useBankCoreApiContext(); + + async function fetcher([username, token, txid]: [string, AccessToken, number]) { + return await api.getTransactionById({ username, token }, txid) + } + + const { data, error } = useSWR, TalerHttpError>( + [account, token, tid], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -248,60 +104,37 @@ export function useTransactionDetails( keepPreviousData: true, }); - // if (isValidating) return { loading: true, data: data?.data }; if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } -interface PaginationFilter { - // page: number; -} +export function usePublicAccounts(initial?: number) { + const [offset, setOffset] = useState(initial); + const { api } = useBankCoreApiContext(); + + async function fetcher(txid: number | undefined) { + return await api.getPublicAccounts({ + limit: MAX_RESULT_SIZE, + offset: txid ? String(txid) : undefined, + order: "asc" + }) + } + + const { data, error } = useSWR, TalerHttpError>([offset], fetcher); -export function usePublicAccounts( - args?: PaginationFilter, -): HttpResponsePaginated< - SandboxBackend.CoreBank.PublicAccountsResponse, - SandboxBackend.SandboxError -> { - const { paginatedFetcher } = usePublicBackend(); - - const [page, setPage] = useState(1); - - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk, - RequestError - >([`public-accounts`, page, PAGE_SIZE], paginatedFetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - SandboxBackend.CoreBank.PublicAccountsResponse, - SandboxBackend.SandboxError - > - >({ loading: true }); - - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData]); - - if (afterError) return afterError.cause; - - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.public_accounts.length < PAGE_SIZE; - const isReachingStart = false; + const isLastPage = + data && data.body.public_accounts.length < PAGE_SIZE; + const isFirstPage = !initial; const pagination = { - isReachingEnd, - isReachingStart, + isLastPage, + isFirstPage, loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.public_accounts.length < MAX_RESULT_SIZE) { - setPage(page + 1); + if (isLastPage || data?.type !== "ok") return; + const list = data.body.public_accounts + if (list.length < MAX_RESULT_SIZE) { + // setOffset(list[list.length-1].account_name); } }, loadMorePrev: () => { @@ -309,43 +142,39 @@ export function usePublicAccounts( }, }; - const public_accounts = !afterData - ? [] - : (afterData || lastAfter).data.public_accounts; - if (loadingAfter) return { loading: true, data: { public_accounts } }; - if (afterData) { - return { ok: true, data: { public_accounts }, ...pagination }; + // const public_accountslist = data?.type !== "ok" ? [] : data.body.public_accounts; + if (data) { + return { ok: true, data: data.body, ...pagination } } - return { loading: true }; + if (error) { + return error; + } + return undefined; } /** - * FIXME: mutate result when balance change (transaction ) + * @param account * @param args * @returns */ -export function useTransactions( - account: string, - args?: PaginationFilter, -): HttpResponsePaginated< - SandboxBackend.CoreBank.BankAccountTransactionsResponse, - SandboxBackend.SandboxError -> { - const { paginatedFetcher } = useAuthenticatedBackend(); - - const [start, setStart] = useState(); - - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk, - RequestError - >( - [`accounts/${account}/transactions`, start, PAGE_SIZE], - paginatedFetcher, { +export function useTransactions(account: string, initial?: number) { + const { state: credentials } = useBackendState(); + const token = credentials.status !== "loggedIn" ? undefined : credentials.token + + const [offset, setOffset] = useState(initial); + const { api } = useBankCoreApiContext(); + + async function fetcher([username, token, txid]: [string, AccessToken, number | undefined]) { + return await api.getTransactions({ username, token }, { + limit: MAX_RESULT_SIZE, + offset: txid ? String(txid) : undefined, + order: "dec" + }) + } + + const { data, error } = useSWR, TalerHttpError>( + [account, token, offset], fetcher, { refreshInterval: 0, refreshWhenHidden: false, refreshWhenOffline: false, @@ -356,50 +185,30 @@ export function useTransactions( } ); - const [lastAfter, setLastAfter] = useState< - HttpResponse< - SandboxBackend.CoreBank.BankAccountTransactionsResponse, - SandboxBackend.SandboxError - > - >({ loading: true }); - - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData]); - - if (afterError) { - return afterError.cause; - } - - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.transactions.length < PAGE_SIZE; - const isReachingStart = start == undefined; + const isLastPage = + data && data.type === "ok" && data.body.transactions.length < PAGE_SIZE; + const isFirstPage = true; const pagination = { - isReachingEnd, - isReachingStart, + isLastPage, + isFirstPage, loadMore: () => { - if (!afterData || isReachingEnd) return; - // if (afterData.data.transactions.length < MAX_RESULT_SIZE) { - const l = afterData.data.transactions[afterData.data.transactions.length-1] - setStart(String(l.row_id)); - // } + if (isLastPage || data?.type !== "ok") return; + const list = data.body.transactions + if (list.length < MAX_RESULT_SIZE) { + setOffset(list[list.length - 1].row_id); + } }, loadMorePrev: () => { - if (!afterData || isReachingStart) return; - // if (afterData.data.transactions.length < MAX_RESULT_SIZE) { - setStart(undefined) - // } + null; }, }; - const transactions = !afterData - ? [] - : (afterData || lastAfter).data.transactions; - if (loadingAfter) return { loading: true, data: { transactions } }; - if (afterData) { - return { ok: true, data: { transactions }, ...pagination }; + if (data) { + return { ok: true, data, ...pagination } + } + if (error) { + return error; } - return { loading: true }; + return undefined; } diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index 889618646..589d7fab0 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -15,6 +15,7 @@ */ import { + AccessToken, Codec, buildCodecForObject, buildCodecForUnion, @@ -24,23 +25,11 @@ import { codecForString, } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - RequestError, buildStorageKey, - useLocalStorage, + useLocalStorage } from "@gnu-taler/web-util/browser"; -import { - HttpResponse, - HttpResponseOk, - RequestOptions, -} from "@gnu-taler/web-util/browser"; -import { useApiContext } from "@gnu-taler/web-util/browser"; -import { useCallback, useEffect, useState } from "preact/hooks"; import { useSWRConfig } from "swr"; -import { useBackendContext } from "../context/backend.js"; import { bankUiSettings } from "../settings.js"; -import { AccessToken } from "./useCredentialsChecker.js"; /** * Has the information to reach and @@ -91,34 +80,6 @@ export const codecForBackendState = (): Codec => .alternative("expired", codecForBackendStateExpired()) .build("BackendState"); -export function getInitialBackendBaseURL(): string { - const overrideUrl = - typeof localStorage !== "undefined" - ? localStorage.getItem("bank-base-url") - : undefined; - let result: string; - if (!overrideUrl) { - //normal path - if (!bankUiSettings.backendBaseURL) { - console.error( - "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", - ); - result = window.origin - } else { - result = bankUiSettings.backendBaseURL; - } - } else { - // testing/development path - result = overrideUrl - } - try { - return canonicalizeBaseUrl(result) - } catch (e) { - //fall back - return canonicalizeBaseUrl(window.origin) - } -} - export const defaultState: BackendState = { status: "loggedOut", }; @@ -127,7 +88,7 @@ export interface BackendStateHandler { state: BackendState; logOut(): void; expired(): void; - logIn(info: {username: string, token: AccessToken}): void; + logIn(info: { username: string, token: AccessToken }): void; } const BACKEND_STATE_KEY = buildStorageKey( @@ -174,226 +135,6 @@ export function useBackendState(): BackendStateHandler { }; } -interface useBackendType { - request: ( - path: string, - options?: RequestOptions, - ) => Promise>; - fetcher: (endpoint: string) => Promise>; - multiFetcher: (endpoint: string[][]) => Promise[]>; - paginatedFetcher: ( - args: [string, string | undefined, number], - ) => Promise>; - sandboxAccountsFetcher: ( - args: [string, number, number, string], - ) => Promise>; - sandboxCashoutFetcher: (endpoint: string[]) => Promise>; -} -export function usePublicBackend(): useBackendType { - const { request: requestHandler } = useApiContext(); - - const baseUrl = getInitialBackendBaseURL(); - - const request = useCallback( - function requestImpl( - path: string, - options: RequestOptions = {}, - ): Promise> { - return requestHandler(baseUrl, path, options); - }, - [baseUrl], - ); - - const fetcher = useCallback( - function fetcherImpl(endpoint: string): Promise> { - return requestHandler(baseUrl, endpoint); - }, - [baseUrl], - ); - const paginatedFetcher = useCallback( - function fetcherImpl([endpoint, start, size]: [ - string, - string | undefined, - number, - ]): Promise> { - const delta = -1 * size //descending order - const params = start ? { delta, start } : { delta } - return requestHandler(baseUrl, endpoint, { - params, - }); - }, - [baseUrl], - ); - const multiFetcher = useCallback( - function multiFetcherImpl([endpoints]: string[][]): Promise< - HttpResponseOk[] - > { - return Promise.all( - endpoints.map((endpoint) => requestHandler(baseUrl, endpoint)), - ); - }, - [baseUrl], - ); - const sandboxAccountsFetcher = useCallback( - function fetcherImpl([endpoint, page, size, account]: [ - string, - number, - number, - string, - ]): Promise> { - return requestHandler(baseUrl, endpoint, { - params: { page: page || 1, size }, - }); - }, - [baseUrl], - ); - const sandboxCashoutFetcher = useCallback( - function fetcherImpl([endpoint, account]: string[]): Promise< - HttpResponseOk - > { - return requestHandler(baseUrl, endpoint); - }, - [baseUrl], - ); - return { - request, - fetcher, - paginatedFetcher, - multiFetcher, - sandboxAccountsFetcher, - sandboxCashoutFetcher, - }; -} - -type CheckResult = ValidResult | RequestInvalidResult | InvalidationResult; - -interface ValidResult { - valid: true; -} -interface RequestInvalidResult { - valid: false; - requestError: true; - cause: RequestError["cause"]; -} -interface InvalidationResult { - valid: false; - requestError: false; - error: unknown; -} - -export function useAuthenticatedBackend(): useBackendType { - const { state } = useBackendContext(); - const { request: requestHandler } = useApiContext(); - - // FIXME: libeufin returns 400 insteand of 401 if there is no auth token - const creds = state.status === "loggedIn" ? state.token : "secret-token:a"; - const baseUrl = getInitialBackendBaseURL(); - - const request = useCallback( - function requestImpl( - path: string, - options: RequestOptions = {}, - ): Promise> { - return requestHandler(baseUrl, path, { token: creds, ...options }); - }, - [baseUrl, creds], - ); - - const fetcher = useCallback( - function fetcherImpl(endpoint: string): Promise> { - return requestHandler(baseUrl, endpoint, { token: creds }); - }, - [baseUrl, creds], - ); - const paginatedFetcher = useCallback( - function fetcherImpl([endpoint, start, size]: [ - string, - string | undefined, - number, - ]): Promise> { - const delta = -1 * size //descending order - const params = start ? { delta, start } : { delta } - return requestHandler(baseUrl, endpoint, { - token: creds, - params, - }); - }, - [baseUrl, creds], - ); - const multiFetcher = useCallback( - function multiFetcherImpl([endpoints]: string[][]): Promise< - HttpResponseOk[] - > { - return Promise.all( - endpoints.map((endpoint) => - requestHandler(baseUrl, endpoint, { token: creds }), - ), - ); - }, - [baseUrl, creds], - ); - const sandboxAccountsFetcher = useCallback( - function fetcherImpl([endpoint, page, size, account]: [ - string, - number, - number, - string, - ]): Promise> { - return requestHandler(baseUrl, endpoint, { - token: creds, - params: { page: page || 1, size }, - }); - }, - [baseUrl], - ); - - const sandboxCashoutFetcher = useCallback( - function fetcherImpl([endpoint, account]: string[]): Promise< - HttpResponseOk - > { - return requestHandler(baseUrl, endpoint, { - token: creds, - params: { account }, - }); - }, - [baseUrl, creds], - ); - return { - request, - fetcher, - paginatedFetcher, - multiFetcher, - sandboxAccountsFetcher, - sandboxCashoutFetcher, - }; -} -/** - * - * @deprecated - */ -export function useBackendConfig(): HttpResponse< - SandboxBackend.Config, - SandboxBackend.SandboxError -> { - const { request } = usePublicBackend(); - - type Type = SandboxBackend.Config; - - const [result, setResult] = useState< - HttpResponse - >({ loading: true }); - - useEffect(() => { - request(`/config`) - .then((data) => setResult(data)) - .catch((error: RequestError) => - setResult(error.cause), - ); - }, [request]); - - return result; -} - export function useMatchMutate(): ( re: RegExp, value?: unknown, diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 5dba60951..208663f8b 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -14,239 +14,18 @@ GNU Taler; see the file COPYING. If not, see */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, - useApiContext, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useMemo, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; import { useBackendContext } from "../context/backend.js"; import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; -import { - getInitialBackendBaseURL, - useAuthenticatedBackend, - useMatchMutate, -} from "./backend.js"; +import { useBackendState } from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import { AccessToken, AmountJson, Amounts, OperationOk, TalerCoreBankResultByMethod, TalerCorebankApi, TalerError, TalerHttpError } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook } from "swr"; -import { AmountJson, Amounts } from "@gnu-taler/taler-util"; -import { AccessToken } from "./useCredentialsChecker.js"; -const useSWR = _useSWR as unknown as SWRHook; - -export function useAdminAccountAPI(): AdminAccountAPI { - const { request } = useAuthenticatedBackend(); - const mutateAll = useMatchMutate(); - const { state, logIn } = useBackendContext(); - if (state.status === "loggedOut") { - throw Error("access-api can't be used when the user is not logged In"); - } - - const createAccount = async ( - data: SandboxBackend.Circuit.CircuitAccountRequest, - ): Promise> => { - const res = await request(`circuit-api/accounts`, { - method: "POST", - data, - contentType: "json", - }); - await mutateAll(/.*circuit-api\/accounts.*/); - return res; - }; - - const updateAccount = async ( - account: string, - data: SandboxBackend.Circuit.CircuitAccountReconfiguration, - ): Promise> => { - const res = await request(`circuit-api/accounts/${account}`, { - method: "PATCH", - data, - contentType: "json", - }); - await mutateAll(/.*circuit-api\/accounts.*/); - return res; - }; - const deleteAccount = async ( - account: string, - ): Promise> => { - const res = await request(`circuit-api/accounts/${account}`, { - method: "DELETE", - contentType: "json", - }); - await mutateAll(/.*circuit-api\/accounts.*/); - return res; - }; - const changePassword = async ( - account: string, - data: SandboxBackend.Circuit.AccountPasswordChange, - ): Promise> => { - const res = await request(`circuit-api/accounts/${account}/auth`, { - method: "PATCH", - data, - contentType: "json", - }); - if (account === state.username) { - await mutateAll(/.*/); - logIn({ - username: account, - //FIXME: change password api - token: data.new_password as AccessToken, - }); - } - return res; - }; - - return { createAccount, deleteAccount, updateAccount, changePassword }; -} - -export function useCircuitAccountAPI(): CircuitAccountAPI { - const { request } = useAuthenticatedBackend(); - const mutateAll = useMatchMutate(); - const { state } = useBackendContext(); - if (state.status === "loggedOut") { - throw Error("access-api can't be used when the user is not logged In"); - } - const account = state.username; - - const updateAccount = async ( - data: SandboxBackend.Circuit.CircuitAccountReconfiguration, - ): Promise> => { - const res = await request(`circuit-api/accounts/${account}`, { - method: "PATCH", - data, - contentType: "json", - }); - await mutateAll(/.*circuit-api\/accounts.*/); - return res; - }; - const changePassword = async ( - data: SandboxBackend.Circuit.AccountPasswordChange, - ): Promise> => { - const res = await request(`circuit-api/accounts/${account}/auth`, { - method: "PATCH", - data, - contentType: "json", - }); - return res; - }; - - const createCashout = async ( - data: SandboxBackend.Circuit.CashoutRequest, - ): Promise> => { - const res = await request( - `circuit-api/cashouts`, - { - method: "POST", - data, - contentType: "json", - }, - ); - return res; - }; - - const confirmCashout = async ( - cashoutId: string, - data: SandboxBackend.Circuit.CashoutConfirm, - ): Promise> => { - const res = await request( - `circuit-api/cashouts/${cashoutId}/confirm`, - { - method: "POST", - data, - contentType: "json", - }, - ); - await mutateAll(/.*circuit-api\/cashout.*/); - return res; - }; - - const abortCashout = async ( - cashoutId: string, - ): Promise> => { - const res = await request(`circuit-api/cashouts/${cashoutId}/abort`, { - method: "POST", - contentType: "json", - }); - await mutateAll(/.*circuit-api\/cashout.*/); - return res; - }; - - return { - updateAccount, - changePassword, - createCashout, - confirmCashout, - abortCashout, - }; -} +import { useBankCoreApiContext } from "../context/config.js"; +import { assertUnreachable } from "../pages/HomePage.js"; -export interface AdminAccountAPI { - createAccount: ( - data: SandboxBackend.Circuit.CircuitAccountRequest, - ) => Promise>; - deleteAccount: (account: string) => Promise>; - - updateAccount: ( - account: string, - data: SandboxBackend.Circuit.CircuitAccountReconfiguration, - ) => Promise>; - changePassword: ( - account: string, - data: SandboxBackend.Circuit.AccountPasswordChange, - ) => Promise>; -} - -export interface CircuitAccountAPI { - updateAccount: ( - data: SandboxBackend.Circuit.CircuitAccountReconfiguration, - ) => Promise>; - changePassword: ( - data: SandboxBackend.Circuit.AccountPasswordChange, - ) => Promise>; - createCashout: ( - data: SandboxBackend.Circuit.CashoutRequest, - ) => Promise>; - confirmCashout: ( - id: string, - data: SandboxBackend.Circuit.CashoutConfirm, - ) => Promise>; - abortCashout: (id: string) => Promise>; -} - -async function getBusinessStatus( - request: ReturnType["request"], - username: string, - token: AccessToken, -): Promise { - try { - const url = getInitialBackendBaseURL(); - const result = await request( - url, - `circuit-api/accounts/${username}`, - { token }, - ); - return result.ok; - } catch (error) { - return false; - } -} - -async function getEstimationByCredit( - request: ReturnType["request"], - basicAuth: { username: string; password: string }, -): Promise { - try { - const url = getInitialBackendBaseURL(); - const result = await request< - HttpResponseOk - >(url, `circuit-api/accounts/${basicAuth.username}`, { basicAuth }); - return result.ok; - } catch (error) { - return false; - } -} +const useSWR = _useSWR as unknown as SWRHook; export type TransferCalculation = { debit: AmountJson; @@ -266,37 +45,27 @@ type CashoutEstimators = { export function useEstimator(): CashoutEstimators { const { state } = useBackendContext(); - const { request } = useApiContext(); + const { api } = useBankCoreApiContext(); const creds = state.status !== "loggedIn" ? undefined : state.token; return { estimateByCredit: async (amount, fee, rate) => { - const zeroBalance = Amounts.zeroOfCurrency(fee.currency); - const zeroFiat = Amounts.zeroOfCurrency(fee.currency); - const zeroCalc = { - debit: zeroBalance, - credit: zeroFiat, - beforeFee: zeroBalance, - }; - const url = getInitialBackendBaseURL(); - const result = await request( - url, - `circuit-api/cashouts/estimates`, - { - token: creds, - params: { - amount_credit: Amounts.stringify(amount), - }, - }, - ); - // const credit = Amounts.parseOrThrow(result.data.data.amount_credit); + const resp = await api.getCashoutRate({ + credit: amount + }); + if (resp.type === "fail") { + // can't happen + // not-supported: it should not be able to call this function + // wrong-calculation: we are using just one parameter + throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint) + } const credit = amount; const _credit = { ...credit, currency: fee.currency }; const beforeFee = Amounts.sub(_credit, fee).amount; - const debit = Amounts.parseOrThrow(result.data.amount_debit); + const debit = Amounts.parseOrThrow(resp.body.amount_debit); return { debit, beforeFee, @@ -311,18 +80,14 @@ export function useEstimator(): CashoutEstimators { credit: zeroFiat, beforeFee: zeroBalance, }; - const url = getInitialBackendBaseURL(); - const result = await request( - url, - `circuit-api/cashouts/estimates`, - { - token: creds, - params: { - amount_debit: Amounts.stringify(amount), - }, - }, - ); - const credit = Amounts.parseOrThrow(result.data.amount_credit); + const resp = await api.getCashoutRate({ debit: amount }); + if (resp.type === "fail") { + // can't happen + // not-supported: it should not be able to call this function + // wrong-calculation: we are using just one parameter + throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint) + } + const credit = Amounts.parseOrThrow(resp.body.amount_credit); const _credit = { ...credit, currency: fee.currency }; const debit = amount; const beforeFee = Amounts.sub(_credit, fee).amount; @@ -335,67 +100,15 @@ export function useEstimator(): CashoutEstimators { }; } -export function useBusinessAccountFlag(): boolean | undefined { - const [isBusiness, setIsBusiness] = useState(); - const { state } = useBackendContext(); - const { request } = useApiContext(); - const creds = - state.status !== "loggedIn" - ? undefined - : {user: state.username, token: state.token}; - - useEffect(() => { - if (!creds) return; - getBusinessStatus(request, creds.user, creds.token) - .then((result) => { - setIsBusiness(result); - }) - .catch((error) => { - setIsBusiness(false); - }); - }); - - return isBusiness; -} - -export function useBusinessAccountDetails( - account: string, -): HttpResponse< - SandboxBackend.Circuit.CircuitAccountData, - SandboxBackend.SandboxError -> { - const { fetcher } = useAuthenticatedBackend(); - - const { data, error } = useSWR< - HttpResponseOk, - RequestError - >([`circuit-api/accounts/${account}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - keepPreviousData: true, - }); - - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -export function useRatiosAndFeeConfig(): HttpResponse< - SandboxBackend.Circuit.Config, - SandboxBackend.SandboxError -> { - const { fetcher } = useAuthenticatedBackend(); +export function useRatiosAndFeeConfig() { + const { api } = useBankCoreApiContext(); + function fetcher() { + return api.getConversionRates() + } const { data, error } = useSWR< - HttpResponseOk, - RequestError - >([`circuit-api/config`], fetcher, { + TalerCoreBankResultByMethod<"getConversionRates">, TalerHttpError + >([], fetcher, { refreshInterval: 60 * 1000, refreshWhenHidden: false, revalidateOnFocus: false, @@ -409,8 +122,8 @@ export function useRatiosAndFeeConfig(): HttpResponse< }); if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } interface PaginationFilter { @@ -418,58 +131,49 @@ interface PaginationFilter { page?: number; } -export function useBusinessAccounts( - args?: PaginationFilter, -): HttpResponsePaginated< - SandboxBackend.Circuit.CircuitAccounts, - SandboxBackend.SandboxError -> { - const { sandboxAccountsFetcher } = useAuthenticatedBackend(); - const [page, setPage] = useState(0); +export function useBusinessAccounts() { + const { state: credentials } = useBackendState(); + const token = credentials.status !== "loggedIn" ? undefined : credentials.token + const { api } = useBankCoreApiContext(); - const { - data: afterData, - error: afterError, - // isValidating: loadingAfter, - } = useSWR< - HttpResponseOk, - RequestError - >( - [`accounts`, args?.page, PAGE_SIZE, args?.account], - sandboxAccountsFetcher, - { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - keepPreviousData: true, - }, - ); + const [offset, setOffset] = useState(); - // const [lastAfter, setLastAfter] = useState< - // HttpResponse - // >({ loading: true }); + function fetcher(token: AccessToken, offset?: string) { + return api.getAccounts(token, { + limit: MAX_RESULT_SIZE, + offset, + order: "asc" + }) + } - // useEffect(() => { - // if (afterData) setLastAfter(afterData); - // }, [afterData]); + const { data, error } = useSWR, TalerHttpError>( + [token, offset], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + errorRetryCount: 0, + errorRetryInterval: 1, + shouldRetryOnError: false, + keepPreviousData: true, + }, + ); - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data?.customers?.length < PAGE_SIZE; - const isReachingStart = false; + const isLastPage = + data && data.type === "ok" && data.body.accounts.length < PAGE_SIZE; + const isFirstPage = false; const pagination = { - isReachingEnd, - isReachingStart, + isLastPage, + isFirstPage, loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data?.customers?.length < MAX_RESULT_SIZE) { - setPage(page + 1); + if (isLastPage || data?.type !== "ok") return; + const list = data.body.accounts + if (list.length < MAX_RESULT_SIZE) { + //FIXME: define pagination + + // setOffset(list[list.length - 1].row_id); } }, loadMorePrev: () => { @@ -477,85 +181,65 @@ export function useBusinessAccounts( }, }; - const result = useMemo(() => { - const customers = !afterData ? [] : afterData?.data?.customers ?? []; - return { ok: true as const, data: { customers }, ...pagination }; - }, [afterData?.data]); - - if (afterError) return afterError.cause; - if (afterData) { - return result; - } + if (data) return { ok: true, data, ...pagination }; + if (error) return error; + return undefined; +} - // if (loadingAfter) - // return { loading: true, data: { customers } }; - // if (afterData) { - // return { ok: true, data: { customers }, ...pagination }; - // } - return { loading: true }; +type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: string } +function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId { + return c !== undefined } +export function useCashouts(account: string) { + const { state: credentials } = useBackendState(); + const token = credentials.status !== "loggedIn" ? undefined : credentials.token + const { api } = useBankCoreApiContext(); + + async function fetcher([username, token]: [string, AccessToken]) { + const list = await api.getAccountCashouts({ username, token }) + if (list.type !== "ok") { + assertUnreachable(list.type) + } + const all: Array = await Promise.all(list.body.cashouts.map(c => { + return api.getCashoutById({ username, token }, c.cashout_id).then(r => { + if (r.type === "fail") return undefined + return { ...r.body, id: c.cashout_id } + }) + })) -export function useCashouts( - account: string, -): HttpResponse< - SandboxBackend.Circuit.CashoutStatusResponseWithId[], - SandboxBackend.SandboxError -> { - const { sandboxCashoutFetcher, multiFetcher } = useAuthenticatedBackend(); + const cashouts = all.filter(notUndefined) + return { type: "ok" as const, body: { cashouts } } + } - const { data: list, error: listError } = useSWR< - HttpResponseOk, - RequestError - >([`circuit-api/cashouts`, account], sandboxCashoutFetcher, { + const { data, error } = useSWR, TalerHttpError>( + [account, token], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, revalidateOnReconnect: false, refreshWhenOffline: false, + errorRetryCount: 0, + errorRetryInterval: 1, + shouldRetryOnError: false, + keepPreviousData: true, }); - const paths = ((list?.data && list?.data.cashouts) || []).map( - (cashoutId) => `circuit-api/cashouts/${cashoutId}`, - ); - const { data: cashouts, error: productError } = useSWR< - HttpResponseOk[], - RequestError - >([paths], multiFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); + if (data) return data; + if (error) return error; + return undefined; +} - if (listError) return listError.cause; - if (productError) return productError.cause; +export function useCashoutDetails(cashoutId: string) { + const { state: credentials } = useBackendState(); + const creds = credentials.status !== "loggedIn" ? undefined : credentials + const { api } = useBankCoreApiContext(); - if (cashouts) { - const dataWithId = cashouts.map((d) => { - //take the id from the queried url - return { - ...d.data, - id: d.info?.url.replace(/.*\/cashouts\//, "") || "", - }; - }); - return { ok: true, data: dataWithId }; + async function fetcher([username, token, id]: [string, AccessToken, string]) { + return api.getCashoutById({ username, token }, id) } - return { loading: true }; -} - -export function useCashoutDetails( - id: string, -): HttpResponse< - SandboxBackend.Circuit.CashoutStatusResponse, - SandboxBackend.SandboxError -> { - const { fetcher } = useAuthenticatedBackend(); - const { data, error } = useSWR< - HttpResponseOk, - RequestError - >([`circuit-api/cashouts/${id}`], fetcher, { + const { data, error } = useSWR, TalerHttpError>( + [creds?.username, creds?.token, cashoutId], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -568,6 +252,6 @@ export function useCashoutDetails( }); if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } diff --git a/packages/demobank-ui/src/hooks/config.ts b/packages/demobank-ui/src/hooks/config.ts deleted file mode 100644 index a3bd294db..000000000 --- a/packages/demobank-ui/src/hooks/config.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { LibtoolVersion } from "@gnu-taler/taler-util"; -import { ErrorType, HttpError, HttpResponseServerError, RequestError, useApiContext } from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { getInitialBackendBaseURL } from "./backend.js"; - -/** - * Protocol version spoken with the bank. - * - * Uses libtool's current:revision:age versioning. - */ -export const BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0"; - -async function getConfigState( - request: ReturnType["request"], -): Promise { - const url = getInitialBackendBaseURL(); - const result = await request(url, `config`); - return result.data; -} - -export type ConfigResult = undefined - | { type: "ok", result: Required } - | { type: "wrong", result: SandboxBackend.Config } - | { type: "error", result: HttpError } - -export function useConfigState(): ConfigResult { - const [checked, setChecked] = useState() - const { request } = useApiContext(); - - useEffect(() => { - getConfigState(request) - .then((result) => { - const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, result.version) - if (r?.compatible) { - const complete: Required = { - currency_fraction_digits: result.currency_fraction_digits ?? 2, - currency_fraction_limit: result.currency_fraction_limit ?? 2, - fiat_currency: "", - have_cashout: result.have_cashout ?? false, - name: result.name, - version: result.version, - } - setChecked({ type: "ok", result: complete }); - } else { - setChecked({ type: "wrong", result }) - } - }) - .catch((error: unknown) => { - if (error instanceof RequestError) { - const result = error.cause - setChecked({ type: "error", result }); - } - }); - }, []); - - return checked; -} - - diff --git a/packages/demobank-ui/src/hooks/useCredentialsChecker.ts b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts deleted file mode 100644 index b3dedb654..000000000 --- a/packages/demobank-ui/src/hooks/useCredentialsChecker.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; -import { ErrorType, HttpError, RequestError, useApiContext } from "@gnu-taler/web-util/browser"; -import { getInitialBackendBaseURL } from "./backend.js"; - -export function useCredentialsChecker() { - const { request } = useApiContext(); - const baseUrl = getInitialBackendBaseURL(); - //check against instance details endpoint - //while merchant backend doesn't have a login endpoint - async function requestNewLoginToken( - username: string, - password: string, - ): Promise { - const data: LoginTokenRequest = { - scope: "readwrite" as "write", //FIX: different than merchant - duration: { - // d_us: "forever" //FIX: should return shortest - d_us: 60 * 60 * 24 * 7 * 1000 * 1000 - }, - refreshable: true, - } - try { - const response = await request(baseUrl, `accounts/${username}/token`, { - method: "POST", - basicAuth: { - username, - password, - }, - data, - contentType: "json" - }); - return { valid: true, token: `secret-token:${response.data.access_token}` as AccessToken, expiration: response.data.expiration }; - } catch (error) { - if (error instanceof RequestError) { - return { valid: false, cause: error.cause }; - } - - return { - valid: false, cause: { - type: ErrorType.UNEXPECTED, - loading: false, - info: { - hasToken: true, - status: 0, - options: {}, - url: `/private/token`, - payload: {} - }, - exception: error, - message: (error instanceof Error ? error.message : "unpexepected error") - } - }; - } - }; - - async function refreshLoginToken( - baseUrl: string, - token: LoginToken - ): Promise { - - if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) { - return { - valid: false, cause: { - type: ErrorType.CLIENT, - status: HttpStatusCode.Unauthorized, - message: "login token expired, login again.", - info: { - hasToken: true, - status: 401, - options: {}, - url: `/private/token`, - payload: {} - }, - payload: {} - }, - } - } - - return requestNewLoginToken(baseUrl, token.token) - } - return { requestNewLoginToken, refreshLoginToken } -} - -export interface LoginToken { - token: AccessToken, - expiration: Timestamp, -} -// token used to get loginToken -// must forget after used -declare const __ac_token: unique symbol; -export type AccessToken = string & { - [__ac_token]: true; -}; - -type YesOrNo = "yes" | "no"; -export type LoginResult = { - valid: true; - token: AccessToken; - expiration: Timestamp; -} | { - valid: false; - cause: HttpError<{}>; -} - - -// DELETE /private/instances/$INSTANCE -export interface LoginTokenRequest { - // Scope of the token (which kinds of operations it will allow) - scope: "readonly" | "write"; - - // Server may impose its own upper bound - // on the token validity duration - duration?: RelativeTime; - - // Can this token be refreshed? - // Defaults to false. - refreshable?: boolean; -} -export interface LoginTokenSuccessResponse { - // The login token that can be used to access resources - // that are in scope for some time. Must be prefixed - // with "Bearer " when used in the "Authorization" HTTP header. - // Will already begin with the RFC 8959 prefix. - access_token: AccessToken; - - // Scope of the token (which kinds of operations it will allow) - scope: "readonly" | "write"; - - // Server may impose its own upper bound - // on the token validity duration - expiration: Timestamp; - - // Can this token be refreshed? - refreshable: boolean; -} diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts index 9230fb6b1..ef6b4fede 100644 --- a/packages/demobank-ui/src/pages/AccountPage/index.ts +++ b/packages/demobank-ui/src/pages/AccountPage/index.ts @@ -14,20 +14,17 @@ GNU Taler; see the file COPYING. If not, see */ -import { HttpError, HttpResponseOk, HttpResponsePaginated, utils } from "@gnu-taler/web-util/browser"; -import { AbsoluteTime, AmountJson, PaytoUriIBAN, PaytoUriTalerBank } from "@gnu-taler/taler-util"; -import { Loading } from "../../components/Loading.js"; -import { useComponentState } from "./state.js"; -import { ReadyView, InvalidIbanView } from "./views.js"; +import { AbsoluteTime, AmountJson, TalerCorebankApi, TalerError, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { HttpResponsePaginated, utils } from "@gnu-taler/web-util/browser"; import { VNode } from "preact"; -import { LoginForm } from "../LoginForm.js"; import { ErrorLoading } from "../../components/ErrorLoading.js"; +import { Loading } from "../../components/Loading.js"; +import { LoginForm } from "../LoginForm.js"; +import { useComponentState } from "./state.js"; +import { InvalidIbanView, ReadyView } from "./views.js"; export interface Props { account: string; - onLoadNotOk: ( - error: HttpResponsePaginated, - ) => VNode; goToBusinessAccount: () => void; goToConfirmOperation: (id: string) => void; } @@ -42,7 +39,7 @@ export namespace State { export interface LoadingError { status: "loading-error"; - error: HttpError; + error: TalerError; } export interface BaseInfo { @@ -60,12 +57,12 @@ export namespace State { export interface InvalidIban { status: "invalid-iban", - error: HttpResponseOk; + error: TalerCorebankApi.AccountData; } export interface UserNotFound { - status: "error-user-not-found", - error: HttpError; + status: "login", + reason: "not-found" | "forbidden"; onRegister?: () => void; } } @@ -80,7 +77,7 @@ export interface Transaction { const viewMapping: utils.StateViewMap = { loading: Loading, - "error-user-not-found": LoginForm, + "login": LoginForm, "invalid-iban": InvalidIbanView, "loading-error": ErrorLoading, ready: ReadyView, diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts index ca7e1d447..96d45b7bd 100644 --- a/packages/demobank-ui/src/pages/AccountPage/state.ts +++ b/packages/demobank-ui/src/pages/AccountPage/state.ts @@ -14,54 +14,47 @@ GNU Taler; see the file COPYING. If not, see */ -import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, HttpStatusCode, TalerError, TalerErrorCode, parsePaytoUri } from "@gnu-taler/taler-util"; import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { useBackendContext } from "../../context/backend.js"; import { useAccountDetails } from "../../hooks/access.js"; import { Props, State } from "./index.js"; +import { assertUnreachable } from "../HomePage.js"; export function useComponentState({ account, goToBusinessAccount, goToConfirmOperation }: Props): State { const result = useAccountDetails(account); - const backend = useBackendContext(); const { i18n } = useTranslationContext(); - if (result.loading) { + if (!result) { return { status: "loading", error: undefined, }; } - if (!result.ok) { - if (result.loading || result.type === ErrorType.TIMEOUT) { - return { - status: "loading-error", - error: result, - }; - } - //logout if there is any error, not if loading - // backend.logOut(); - if (result.status === HttpStatusCode.NotFound) { - notifyError(i18n.str`Username or account label "${account}" not found`, undefined); - return { - status: "error-user-not-found", - error: result, - }; - } - if (result.status === HttpStatusCode.Unauthorized) { - notifyError(i18n.str`Authorization denied`, i18n.str`Maybe the session has expired, login again.`); - return { - status: "error-user-not-found", - error: result, - }; - } + if (result instanceof TalerError) { return { status: "loading-error", error: result, }; } - const { data } = result; + if (result.type === "fail") { + switch (result.case) { + case "unauthorized": return { + status: "login", + reason: "forbidden" + } + case "not-found": return { + status: "login", + reason: "not-found", + } + default: { + assertUnreachable(result) + } + } + } + + const { body: data } = result; const balance = Amounts.parseOrThrow(data.balance.amount); @@ -71,7 +64,7 @@ export function useComponentState({ account, goToBusinessAccount, goToConfirmOpe if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) { return { status: "invalid-iban", - error: result + error: data }; } diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx index 483cb579a..0604001e3 100644 --- a/packages/demobank-ui/src/pages/AccountPage/views.tsx +++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx @@ -18,14 +18,13 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { Attention } from "../../components/Attention.js"; import { Transactions } from "../../components/Transactions/index.js"; -import { useBusinessAccountDetails } from "../../hooks/circuit.js"; import { useSettings } from "../../hooks/settings.js"; import { PaymentOptions } from "../PaymentOptions.js"; import { State } from "./index.js"; export function InvalidIbanView({ error }: State.InvalidIban) { return ( -
Payto from server is not valid "{error.data.payto_uri}"
+
Payto from server is not valid "{error.payto_uri}"
); } @@ -75,19 +74,20 @@ function MaybeBusinessButton({ onClick: () => void; }): VNode { const { i18n } = useTranslationContext(); - const result = useBusinessAccountDetails(account); - if (!result.ok) return ; - return ( -
- -
- ); + return + // const result = useBusinessAccountDetails(account); + // if (!result.ok) return ; + // return ( + //
+ // + //
+ // ); } diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 6ab6ba3e4..c75964f8e 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see */ -import { Amounts, Logger, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, Logger, TalerError, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util"; import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, VNode, h } from "preact"; import { useEffect, useErrorBoundary, useState } from "preact/hooks"; @@ -27,6 +27,7 @@ import { useAccountDetails } from "../hooks/access.js"; import { useSettings } from "../hooks/settings.js"; import { bankUiSettings } from "../settings.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; +import { Loading } from "../components/Loading.js"; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -237,7 +238,6 @@ export function BankFrame({ + diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx index 9ac93bb34..fda2d904d 100644 --- a/packages/demobank-ui/src/pages/RegistrationPage.tsx +++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ -import { HttpStatusCode, Logger, TranslatedString } from "@gnu-taler/taler-util"; +import { AccessToken, HttpStatusCode, Logger, TalerError, TranslatedString } from "@gnu-taler/taler-util"; import { RequestError, notify, @@ -23,12 +23,11 @@ import { import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { useBackendContext } from "../context/backend.js"; -import { useTestingAPI } from "../hooks/access.js"; import { bankUiSettings } from "../settings.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { getRandomPassword, getRandomUsername } from "./rnd.js"; -import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js"; +import { useBankCoreApiContext } from "../context/config.js"; const logger = new Logger("RegistrationPage"); @@ -63,9 +62,9 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on const [phone, setPhone] = useState(); const [email, setEmail] = useState(); const [repeatPassword, setRepeatPassword] = useState(); - const { requestNewLoginToken } = useCredentialsChecker() - const { register } = useTestingAPI(); + const { api } = useBankCoreApiContext() + // const { register } = useTestingAPI(); const { i18n } = useTranslationContext(); const errors = undefinedIfEmpty({ @@ -95,26 +94,77 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on : undefined, }); + async function doRegistrationAndLogin(name: string | undefined, username: string, password: string) { + const creationResponse = await api.createAccount("" as AccessToken, { name: name ?? "", username, password }); + if (creationResponse.type === "fail") { + switch (creationResponse.case) { + case "invalid-input": return notify({ + type: "error", + title: i18n.str`Some of the input fields are invalid.`, + description: creationResponse.detail.hint as TranslatedString, + debug: creationResponse.detail, + }) + case "unable-to-create": return notify({ + type: "error", + title: i18n.str`Unable to create that account.`, + description: creationResponse.detail.hint as TranslatedString, + debug: creationResponse.detail, + }) + case "unauthorized": return notify({ + type: "error", + title: i18n.str`No enough permission to create that account.`, + description: creationResponse.detail.hint as TranslatedString, + debug: creationResponse.detail, + }) + case "already-exist": return notify({ + type: "error", + title: i18n.str`That username is already taken`, + description: creationResponse.detail.hint as TranslatedString, + debug: creationResponse.detail, + }) + default: assertUnreachable(creationResponse) + } + } + const resp = await api.getAuthenticationAPI(username).createAccessToken(password, { + // scope: "readwrite" as "write", //FIX: different than merchant + scope: "readwrite", + duration: { + d_us: "forever" //FIX: should return shortest + // d_us: 60 * 60 * 24 * 7 * 1000 * 1000 + }, + refreshable: true, + }) + + if (resp.type === "ok") { + backend.logIn({ username, token: resp.body.access_token }); + } else { + switch (resp.case) { + case "wrong-credentials": return notify({ + type: "error", + title: i18n.str`Wrong credentials for "${username}"`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "not-found": return notify({ + type: "error", + title: i18n.str`Account not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: assertUnreachable(resp) + } + } + } + async function doRegistrationStep() { if (!username || !password) return; try { - await register({ name: name ?? "", username, password }); - const resp = await requestNewLoginToken(username, password) + await doRegistrationAndLogin(name, username, password) setUsername(undefined); - if (resp.valid) { - backend.logIn({ username, token: resp.token }); - } onComplete(); } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Conflict - ? i18n.str`That username is already taken` - : undefined, - }), - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError( i18n.str`Operation failed, please report`, @@ -143,27 +193,11 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on setPassword(undefined); setRepeatPassword(undefined); const username = `_${user.first}-${user.second}_` - await register({ username, name: `${user.first} ${user.second}`, password: pass }); - const resp = await requestNewLoginToken(username, pass) - if (resp.valid) { - backend.logIn({ username, token: resp.token }); - } + await doRegistrationAndLogin(name, username, pass) onComplete(); } catch (error) { - if (error instanceof RequestError) { - if (tries > 0) { - await delay(200) - await doRandomRegistration(tries - 1) - } else { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Conflict - ? i18n.str`Could not create a random user` - : undefined, - }), - ); - } + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError( i18n.str`Operation failed, please report`, diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx index 6acf0361e..3534f9733 100644 --- a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx +++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx @@ -1,67 +1,88 @@ -import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { HttpStatusCode, TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util"; +import { HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; -import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js"; import { useState } from "preact/hooks"; -import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; -import { buildRequestErrorMessage } from "../utils.js"; +import { ErrorLoading } from "../components/ErrorLoading.js"; +import { Loading } from "../components/Loading.js"; +import { useBankCoreApiContext } from "../context/config.js"; +import { useAccountDetails } from "../hooks/access.js"; +import { useBackendState } from "../hooks/backend.js"; +import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; +import { assertUnreachable } from "./HomePage.js"; +import { LoginForm } from "./LoginForm.js"; import { AccountForm } from "./admin/AccountForm.js"; export function ShowAccountDetails({ account, onClear, onUpdateSuccess, - onLoadNotOk, onChangePassword, }: { - onLoadNotOk: ( - error: HttpResponsePaginated, - ) => VNode; onClear?: () => void; onChangePassword: () => void; onUpdateSuccess: () => void; account: string; }): VNode { const { i18n } = useTranslationContext(); - const result = useBusinessAccountDetails(account); - const { updateAccount } = useAdminAccountAPI(); + const { state: credentials } = useBackendState(); + const creds = credentials.status !== "loggedIn" ? undefined : credentials + const { api } = useBankCoreApiContext() + const [update, setUpdate] = useState(false); - const [submitAccount, setSubmitAccount] = useState< - SandboxBackend.Circuit.CircuitAccountData | undefined - >(); + const [submitAccount, setSubmitAccount] = useState(); - if (!result.ok) { - if (result.loading || result.type === ErrorType.TIMEOUT) { - return onLoadNotOk(result); - } - if (result.status === HttpStatusCode.NotFound) { - return
account not found
; + const result = useAccountDetails(account); + if (!result) { + return + } + if (result instanceof TalerError) { + return + } + if (result.type === "fail") { + switch (result.case) { + case "not-found": return + case "unauthorized": return + default: assertUnreachable(result) } - return onLoadNotOk(result); } async function doUpdate() { if (!update) { setUpdate(true); } else { - if (!submitAccount) return; + if (!submitAccount || !creds) return; try { - await updateAccount(account, { - cashout_address: submitAccount.cashout_address, - contact_data: submitAccount.contact_data, + const resp = await api.updateAccount(creds, { + cashout_address: submitAccount.cashout_payto_uri, + challenge_contact_data: undefinedIfEmpty({ + email: submitAccount.contact_data?.email, + phone: submitAccount.contact_data?.phone, + }), + is_exchange: false, + name: submitAccount.name, }); - onUpdateSuccess(); + if (resp.type === "ok") { + onUpdateSuccess(); + } else { + switch (resp.case) { + case "unauthorized": return notify({ + type: "error", + title: i18n.str`The rights to change the account are not sufficient`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "not-found": return notify({ + type: "error", + title: i18n.str`The username was not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: assertUnreachable(resp) + } + } } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Forbidden - ? i18n.str`The rights to change the account are not sufficient` - : status === HttpStatusCode.NotFound - ? i18n.str`The username was not found` - : undefined, - }), - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError( i18n.str`Operation failed, please report`, @@ -86,24 +107,24 @@ export function ShowAccountDetails({ }
-
- - - change the account details - - - -
-
+
+ + + change the account details + + + +
+ setSubmitAccount(a)} > diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx index 46f4fe0ef..ac6e9fa9b 100644 --- a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx +++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx @@ -1,43 +1,33 @@ -import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; -import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { TalerError, TranslatedString } from "@gnu-taler/taler-util"; +import { HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useEffect, useRef, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; -import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { doAutoFocus } from "./PaytoWireTransferForm.js"; +import { useBankCoreApiContext } from "../context/config.js"; +import { assertUnreachable } from "./HomePage.js"; +import { useBackendState } from "../hooks/backend.js"; export function UpdateAccountPassword({ account, onCancel, onUpdateSuccess, - onLoadNotOk, focus, }: { - onLoadNotOk: ( - error: HttpResponsePaginated, - ) => VNode; onCancel: () => void; focus?: boolean, onUpdateSuccess: () => void; account: string; }): VNode { const { i18n } = useTranslationContext(); - const result = useBusinessAccountDetails(account); - const { changePassword } = useAdminAccountAPI(); + const { state: credentials } = useBackendState(); + const creds = credentials.status !== "loggedIn" ? undefined : credentials + const { api } = useBankCoreApiContext(); + const [password, setPassword] = useState(); const [repeat, setRepeat] = useState(); - if (!result.ok) { - if (result.loading || result.type === ErrorType.TIMEOUT) { - return onLoadNotOk(result); - } - if (result.status === HttpStatusCode.NotFound) { - return
account not found
; - } - return onLoadNotOk(result); - } - const errors = undefinedIfEmpty({ password: !password ? i18n.str`required` : undefined, repeat: !repeat @@ -48,15 +38,35 @@ export function UpdateAccountPassword({ }); async function doChangePassword() { - if (!!errors || !password) return; + if (!!errors || !password || !creds) return; try { - const r = await changePassword(account, { + const resp = await api.updatePassword(creds, { new_password: password, }); - onUpdateSuccess(); + if (resp.type === "ok") { + onUpdateSuccess(); + } else { + switch (resp.case) { + case "unauthorized": { + notify({ + type: "error", + title: i18n.str`Not authorized to change the password, maybe the session is invalid.` + }) + break; + } + case "not-found": { + notify({ + type: "error", + title: i18n.str`Account not found` + }) + break; + } + default: assertUnreachable(resp) + } + } } catch (error) { - if (error instanceof RequestError) { - notify(buildRequestErrorMessage(i18n, error.cause)); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError(i18n.str`Operation failed, please report`, (error instanceof Error ? error.message diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index da299b1c8..2d80bad1f 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -19,9 +19,9 @@ import { Amounts, HttpStatusCode, Logger, + TalerError, TranslatedString, - WithdrawUriResult, - parseWithdrawUri, + parseWithdrawUri } from "@gnu-taler/taler-util"; import { RequestError, @@ -31,13 +31,15 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { forwardRef } from "preact/compat"; -import { useEffect, useRef, useState } from "preact/hooks"; -import { useAccessAPI } from "../hooks/access.js"; -import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; -import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js"; +import { useState } from "preact/hooks"; +import { Attention } from "../components/Attention.js"; +import { useBankCoreApiContext } from "../context/config.js"; +import { useBackendState } from "../hooks/backend.js"; import { useSettings } from "../hooks/settings.js"; +import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; +import { assertUnreachable } from "./HomePage.js"; import { OperationState } from "./OperationState/index.js"; -import { Attention } from "../components/Attention.js"; +import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js"; const logger = new Logger("WalletWithdrawForm"); const RefAmount = forwardRef(InputAmount); @@ -52,7 +54,10 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { const { i18n } = useTranslationContext(); const [settings, updateSettings] = useSettings() - const { createWithdrawal } = useAccessAPI(); + const { state: credentials } = useBackendState(); + const creds = credentials.status !== "loggedIn" ? undefined : credentials + + const { api } = useBankCoreApiContext() const [amountStr, setAmountStr] = useState(`${settings.maxWithdrawalAmount}`); if (!!settings.currentWithdrawalOperationId) { @@ -81,30 +86,33 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { }); async function doStart() { - if (!parsedAmount) return; + if (!parsedAmount || !creds) return; try { - const result = await createWithdrawal({ + const result = await api.createWithdrawal(creds, { amount: Amounts.stringify(parsedAmount), }); - const uri = parseWithdrawUri(result.data.taler_withdraw_uri); - if (!uri) { - return notifyError( - i18n.str`Server responded with an invalid withdraw URI`, - i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`); + if (result.type === "ok") { + const uri = parseWithdrawUri(result.body.taler_withdraw_uri); + if (!uri) { + return notifyError( + i18n.str`Server responded with an invalid withdraw URI`, + i18n.str`Withdraw URI: ${result.body.taler_withdraw_uri}`); + } else { + updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId) + goToConfirmOperation(uri.withdrawalOperationId); + } } else { - updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId) - goToConfirmOperation(uri.withdrawalOperationId); + switch (result.case) { + case "insufficient-funds": { + notify({ type: "error", title: i18n.str`The operation was rejected due to insufficient funds` }) + break; + } + default: assertUnreachable(result.case) + } } } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Forbidden - ? i18n.str`The operation was rejected due to insufficient funds` - : undefined, - }), - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError( i18n.str`Operation failed, please report`, diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index ddcd2492d..602ec9bd8 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -22,6 +22,7 @@ import { PaytoUri, PaytoUriIBAN, PaytoUriTalerBank, + TalerError, TranslatedString, WithdrawUriResult } from "@gnu-taler/taler-util"; @@ -35,10 +36,12 @@ import { import { Fragment, VNode, h } from "preact"; import { useMemo, useState } from "preact/hooks"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; -import { useAccessAnonAPI } from "../hooks/access.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { useSettings } from "../hooks/settings.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; +import { useBankCoreApiContext } from "../context/config.js"; +import { assertUnreachable } from "./HomePage.js"; +import { mutate } from "swr"; const logger = new Logger("WithdrawalConfirmationQuestion"); @@ -70,7 +73,7 @@ export function WithdrawalConfirmationQuestion({ }; }, []); - const { confirmWithdrawal, abortWithdrawal } = useAccessAnonAPI(); + const { api } = useBankCoreApiContext() const [captchaAnswer, setCaptchaAnswer] = useState(); const answer = parseInt(captchaAnswer ?? "", 10); const [busy, setBusy] = useState>() @@ -87,24 +90,32 @@ export function WithdrawalConfirmationQuestion({ async function doTransfer() { try { setBusy({}) - await confirmWithdrawal( - withdrawUri.withdrawalOperationId, - ); - if (!settings.showWithdrawalSuccess) { - notifyInfo(i18n.str`Wire transfer completed!`) + const resp = await api.confirmWithdrawalById(withdrawUri.withdrawalOperationId); + if (resp.type === "ok") { + mutate(() => true)// clean any info that we have + if (!settings.showWithdrawalSuccess) { + notifyInfo(i18n.str`Wire transfer completed!`) + } + } else { + switch (resp.case) { + case "previously-aborted": return notify({ + type: "error", + title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "no-exchange-or-reserve-selected": return notify({ + type: "error", + title: i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + default: assertUnreachable(resp) + } } } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Conflict - ? i18n.str`The withdrawal has been aborted previously and can't be confirmed` - : status === HttpStatusCode.UnprocessableEntity - ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before` - : undefined, - }), - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError( i18n.str`Operation failed, please report`, @@ -120,18 +131,26 @@ export function WithdrawalConfirmationQuestion({ async function doCancel() { try { setBusy({}) - await abortWithdrawal(withdrawUri.withdrawalOperationId); - onAborted(); + const resp = await api.abortWithdrawalById(withdrawUri.withdrawalOperationId); + if (resp.type === "ok") { + onAborted(); + } else { + switch (resp.case) { + case "previously-confirmed": { + notify({ + type: "error", + title: i18n.str`The reserve operation has been confirmed previously and can't be aborted` + }); + break; + } + default: { + assertUnreachable(resp.case) + } + } + } } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Conflict - ? i18n.str`The reserve operation has been confirmed previously and can't be aborted` - : undefined, - }), - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError( i18n.str`Operation failed, please report`, diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 35fb94a6c..15910201e 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -16,17 +16,17 @@ import { Amounts, - HttpStatusCode, Logger, + TalerError, WithdrawUriResult, parsePaytoUri } from "@gnu-taler/taler-util"; -import { ErrorType, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; +import { ErrorLoading } from "../components/ErrorLoading.js"; import { Loading } from "../components/Loading.js"; import { useWithdrawalDetails } from "../hooks/access.js"; -import { useSettings } from "../hooks/settings.js"; -import { handleNotOkResult } from "./HomePage.js"; +import { assertUnreachable } from "./HomePage.js"; import { QrCodeSection } from "./QrCodeSection.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; @@ -48,48 +48,20 @@ export function WithdrawalQRCode({ const { i18n } = useTranslationContext(); const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId); - if (!result.ok) { - if (result.loading) { - return ; - } - if (result.type === ErrorType.CLIENT && result.status === HttpStatusCode.NotFound) { - return
-
-
- -
- -
- -
-

- - This operation is not known by the server. The operation id is wrong or the - server deleted the operation information before reaching here. - -

-
-
-
-
- -
-
+ if (!result) { + return + } + if (result instanceof TalerError) { + return + } + if (result.type === "fail") { + switch (result.case) { + case "not-found": return + default: assertUnreachable(result.case) } - return handleNotOkResult(i18n)(result); } - const { data } = result; + + const { body: data } = result; if (data.aborted) { return
@@ -194,3 +166,41 @@ export function WithdrawalQRCode({ /> ); } + + +function OperationNotFound({ onClose }: { onClose: () => void }): VNode { + const { i18n } = useTranslationContext(); + return
+
+
+ +
+ +
+ +
+

+ + This operation is not known by the server. The operation id is wrong or the + server deleted the operation information before reaching here. + +

+
+
+
+
+ +
+
+} \ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx b/packages/demobank-ui/src/pages/admin/Account.tsx index 676fc43d0..bf2fa86f0 100644 --- a/packages/demobank-ui/src/pages/admin/Account.tsx +++ b/packages/demobank-ui/src/pages/admin/Account.tsx @@ -1,10 +1,13 @@ -import { Amounts } from "@gnu-taler/taler-util"; -import { PaytoWireTransferForm } from "../PaytoWireTransferForm.js"; -import { handleNotOkResult } from "../HomePage.js"; -import { useAccountDetails } from "../../hooks/access.js"; -import { useBackendContext } from "../../context/backend.js"; +import { Amounts, TalerError } from "@gnu-taler/taler-util"; import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; +import { ErrorLoading } from "../../components/ErrorLoading.js"; +import { Loading } from "../../components/Loading.js"; +import { useBackendContext } from "../../context/backend.js"; +import { useAccountDetails } from "../../hooks/access.js"; +import { assertUnreachable } from "../HomePage.js"; +import { LoginForm } from "../LoginForm.js"; +import { PaytoWireTransferForm } from "../PaytoWireTransferForm.js"; export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode { const { i18n } = useTranslationContext(); @@ -12,15 +15,25 @@ export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode const account = r.state.status !== "loggedOut" ? r.state.username : "admin"; const result = useAccountDetails(account); - if (!result.ok) { - return handleNotOkResult(i18n)(result); + if (!result) { + return + } + if (result instanceof TalerError) { + return } - const { data } = result; + if (result.type === "fail") { + switch (result.case) { + case "unauthorized": return + case "not-found": return + default: assertUnreachable(result) + } + } + const { body: data } = result; const balance = Amounts.parseOrThrow(data.balance.amount); - const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit"; - - const debitThreshold = Amounts.parseOrThrow(result.data.debit_threshold); + const balanceIsDebit = data.balance.credit_debit_indicator == "debit"; + + const debitThreshold = Amounts.parseOrThrow(data.debit_threshold); const limit = balanceIsDebit ? Amounts.sub(debitThreshold, balance).amount : Amounts.add(balance, debitThreshold).amount; diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx index ed8bf610d..8470930bf 100644 --- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -3,7 +3,7 @@ import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; import { useEffect, useRef, useState } from "preact/hooks"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { buildPayto, parsePaytoUri } from "@gnu-taler/taler-util"; +import { TalerCorebankApi, buildPayto, parsePaytoUri } from "@gnu-taler/taler-util"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; @@ -28,8 +28,8 @@ export function AccountForm({ }: { focus?: boolean, children: ComponentChildren, - template: SandboxBackend.Circuit.CircuitAccountData | undefined; - onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void; + template: TalerCorebankApi.AccountData | undefined; + onChange: (a: TalerCorebankApi.AccountData | undefined) => void; purpose: "create" | "update" | "show"; }): VNode { const initial = initializeFromTemplate(template); @@ -41,12 +41,12 @@ export function AccountForm({ function updateForm(newForm: typeof initial): void { - const parsed = !newForm.cashout_address + const parsed = !newForm.cashout_payto_uri ? undefined - : buildPayto("iban", newForm.cashout_address, undefined);; + : buildPayto("iban", newForm.cashout_payto_uri, undefined);; const errors = undefinedIfEmpty>({ - cashout_address: !newForm.cashout_address + cashout_payto_uri: !newForm.cashout_payto_uri ? i18n.str`required` : !parsed ? i18n.str`does not follow the pattern` @@ -75,7 +75,8 @@ export function AccountForm({ // ? i18n.str`IBAN should have just uppercased letters and numbers` // : validateIBAN(newForm.iban, i18n), name: !newForm.name ? i18n.str`required` : undefined, - username: !newForm.username ? i18n.str`required` : undefined, + + // username: !newForm.username ? i18n.str`required` : undefined, }); setErrors(errors); setForm(newForm); @@ -94,7 +95,7 @@ export function AccountForm({
-
+ {/*
+
*/}

@@ -200,18 +201,20 @@ export function AccountForm({ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" name="email" id="email" - data-error={!!errors?.contact_data?.email && form.contact_data.email !== undefined} + data-error={!!errors?.contact_data?.email && form.contact_data?.email !== undefined} disabled={purpose !== "create"} - value={form.contact_data.email ?? ""} + value={form.contact_data?.email ?? ""} onChange={(e) => { - form.contact_data.email = e.currentTarget.value; - updateForm(structuredClone(form)); + if (form.contact_data) { + form.contact_data.email = e.currentTarget.value; + updateForm(structuredClone(form)); + } }} autocomplete="off" />

@@ -231,18 +234,20 @@ export function AccountForm({ name="phone" id="phone" disabled={purpose !== "create"} - value={form.contact_data.phone ?? ""} - data-error={!!errors?.contact_data?.phone && form.contact_data.phone !== undefined} + value={form.contact_data?.phone ?? ""} + data-error={!!errors?.contact_data?.phone && form.contact_data?.phone !== undefined} onChange={(e) => { - form.contact_data.phone = e.currentTarget.value; - updateForm(structuredClone(form)); + if (form.contact_data) { + form.contact_data.phone = e.currentTarget.value; + updateForm(structuredClone(form)); + } }} // placeholder="" autocomplete="off" /> @@ -259,21 +264,21 @@ export function AccountForm({
{ - form.cashout_address = e.currentTarget.value; + form.cashout_payto_uri = e.currentTarget.value; updateForm(structuredClone(form)); }} autocomplete="off" />

@@ -289,26 +294,27 @@ export function AccountForm({ } function initializeFromTemplate( - account: SandboxBackend.Circuit.CircuitAccountData | undefined, -): WithIntermediate { + account: TalerCorebankApi.AccountData | undefined, +): WithIntermediate { const emptyAccount = { - cashout_address: undefined, - iban: undefined, - name: undefined, - username: undefined, + cashout_payto_uri: undefined, contact_data: undefined, + payto_uri: undefined, + balance: undefined, + debit_threshold: undefined, + name: undefined, }; const emptyContact = { email: undefined, phone: undefined, }; - const initial: PartialButDefined = + const initial: PartialButDefined = structuredClone(account) ?? emptyAccount; if (typeof initial.contact_data === "undefined") { initial.contact_data = emptyContact; } - initial.contact_data.email; + // initial.contact_data.email; return initial as any; } diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx index a6899e679..8a1e8294a 100644 --- a/packages/demobank-ui/src/pages/admin/AccountList.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx @@ -1,10 +1,12 @@ -import { h, VNode } from "preact"; -import { useBusinessAccounts } from "../../hooks/circuit.js"; -import { handleNotOkResult } from "../HomePage.js"; -import { AccountAction } from "./Home.js"; -import { Amounts } from "@gnu-taler/taler-util"; +import { Amounts, TalerError } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { ErrorLoading } from "../../components/ErrorLoading.js"; +import { Loading } from "../../components/Loading.js"; +import { useBusinessAccounts } from "../../hooks/circuit.js"; +import { assertUnreachable } from "../HomePage.js"; import { RenderAmount } from "../PaytoWireTransferForm.js"; +import { AccountAction } from "./Home.js"; interface Props { onAction: (type: AccountAction, account: string) => void; @@ -13,15 +15,23 @@ interface Props { } export function AccountList({ account, onAction, onCreateAccount }: Props): VNode { - const result = useBusinessAccounts({ account }); + const result = useBusinessAccounts(); const { i18n } = useTranslationContext(); - if (result.loading) return

; - if (!result.ok) { - return handleNotOkResult(i18n)(result); + if (!result) { + return + } + if (result instanceof TalerError) { + return + } + if (result.data.type === "fail") { + switch (result.data.case) { + case "unauthorized": return
un auth
+ default: assertUnreachable(result.data.case) + } } - const { customers } = result.data; + const { accounts } = result.data.body; return
@@ -45,7 +55,7 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod
- {!customers.length ? ( + {!accounts.length ? (
) : ( @@ -60,7 +70,7 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod - {customers.map((item, idx) => { + {accounts.map((item, idx) => { const balance = !item.balance ? undefined : Amounts.parse(item.balance.amount); diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx index 2146fc6f0..f6176e772 100644 --- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -1,11 +1,14 @@ +import { HttpStatusCode, TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util"; import { RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { VNode, h, Fragment } from "preact"; -import { useAdminAccountAPI } from "../../hooks/circuit.js"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { buildRequestErrorMessage } from "../../utils.js"; -import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; import { getRandomPassword } from "../rnd.js"; import { AccountForm } from "./AccountForm.js"; +import { useBackendState } from "../../hooks/backend.js"; +import { useBankCoreApiContext } from "../../context/config.js"; +import { assertUnreachable } from "../HomePage.js"; +import { mutate } from "swr"; export function CreateNewAccount({ onCancel, @@ -15,40 +18,63 @@ export function CreateNewAccount({ onCreateSuccess: (password: string) => void; }): VNode { const { i18n } = useTranslationContext(); - const { createAccount } = useAdminAccountAPI(); + // const { createAccount } = useAdminAccountAPI(); + const { state: credentials } = useBackendState() + const token = credentials.status !== "loggedIn" ? undefined : credentials.token + const { api } = useBankCoreApiContext(); + const [submitAccount, setSubmitAccount] = useState< - SandboxBackend.Circuit.CircuitAccountData | undefined + TalerCorebankApi.AccountData | undefined >(); async function doCreate() { - if (!submitAccount) return; + if (!submitAccount || !token) return; try { - const account: SandboxBackend.Circuit.CircuitAccountRequest = - { - cashout_address: submitAccount.cashout_address, - contact_data: submitAccount.contact_data, - internal_iban: submitAccount.iban, + const account: TalerCorebankApi.RegisterAccountRequest = { + cashout_payto_uri: submitAccount.cashout_payto_uri, + challenge_contact_data: submitAccount.contact_data, + internal_payto_uri: submitAccount.payto_uri, name: submitAccount.name, - username: submitAccount.username, + username: "",//FIXME: not in account data password: getRandomPassword(), }; - await createAccount(account); - onCreateSuccess(account.password); + const resp = await api.createAccount(token, account); + if (resp.type === "ok") { + mutate(() => true)// clean account list + onCreateSuccess(account.password); + } else { + switch (resp.case) { + case "invalid-input": return notify({ + type: "error", + title: i18n.str`Server replied that input data was invalid`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "unable-to-create": return notify({ + type: "error", + title: i18n.str`The account name is registered.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "unauthorized": return notify({ + type: "error", + title: i18n.str`The rights to perform the operation are not sufficient`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "already-exist": return notify({ + type: "error", + title: i18n.str`Account name is already taken`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: assertUnreachable(resp) + } + } } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Forbidden - ? i18n.str`The rights to perform the operation are not sufficient` - : status === HttpStatusCode.BadRequest - ? i18n.str`Server replied that input data was invalid` - : status === HttpStatusCode.Conflict - ? i18n.str`At least one registration detail was not available` - : undefined, - }), - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError( i18n.str`Operation failed, please report`, diff --git a/packages/demobank-ui/src/pages/admin/Home.tsx b/packages/demobank-ui/src/pages/admin/Home.tsx index d50ff14b4..71ea8ce1b 100644 --- a/packages/demobank-ui/src/pages/admin/Home.tsx +++ b/packages/demobank-ui/src/pages/admin/Home.tsx @@ -2,15 +2,14 @@ import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { Cashouts } from "../../components/Cashouts/index.js"; -import { ShowCashoutDetails } from "../business/Home.js"; -import { handleNotOkResult } from "../HomePage.js"; +import { Transactions } from "../../components/Transactions/index.js"; import { ShowAccountDetails } from "../ShowAccountDetails.js"; import { UpdateAccountPassword } from "../UpdateAccountPassword.js"; +import { ShowCashoutDetails } from "../business/Home.js"; import { AdminAccount } from "./Account.js"; import { AccountList } from "./AccountList.js"; import { CreateNewAccount } from "./CreateNewAccount.js"; import { RemoveAccount } from "./RemoveAccount.js"; -import { Transactions } from "../../components/Transactions/index.js"; /** * Query account information and show QR code if there is pending withdrawal @@ -38,7 +37,6 @@ export function AdminHome({ onRegister }: Props): VNode { switch (action.type) { case "show-cashouts-details": return { setAction(undefined); }} @@ -74,7 +72,6 @@ export function AdminHome({ onRegister }: Props): VNode { ) case "update-password": return { notifyInfo(i18n.str`Password changed`); setAction(undefined); @@ -85,7 +82,6 @@ export function AdminHome({ onRegister }: Props): VNode { /> case "remove-account": return { notifyInfo(i18n.str`Account removed`); setAction(undefined); @@ -96,7 +92,6 @@ export function AdminHome({ onRegister }: Props): VNode { /> case "show-details": return { setAction({ type: "update-password", @@ -137,12 +132,12 @@ export function AdminHome({ onRegister }: Props): VNode { }} account={undefined} onAction={(type, account) => setAction({ account, type })} - + /> - + ); } \ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx index b323b0d01..ce8a53ca1 100644 --- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -1,24 +1,25 @@ -import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { VNode, h, Fragment } from "preact"; +import { Amounts, HttpStatusCode, TalerError, TranslatedString } from "@gnu-taler/taler-util"; +import { HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { Attention } from "../../components/Attention.js"; +import { ErrorLoading } from "../../components/ErrorLoading.js"; +import { Loading } from "../../components/Loading.js"; +import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; import { useAccountDetails } from "../../hooks/access.js"; -import { useAdminAccountAPI } from "../../hooks/circuit.js"; -import { Amounts, HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js"; -import { useEffect, useRef, useState } from "preact/hooks"; -import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; -import { Attention } from "../../components/Attention.js"; +import { assertUnreachable } from "../HomePage.js"; +import { LoginForm } from "../LoginForm.js"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; +import { useBankCoreApiContext } from "../../context/config.js"; +import { useBackendState } from "../../hooks/backend.js"; export function RemoveAccount({ account, onCancel, onUpdateSuccess, - onLoadNotOk, focus, }: { - onLoadNotOk: ( - error: HttpResponsePaginated, - ) => VNode; focus?: boolean; onCancel: () => void; onUpdateSuccess: () => void; @@ -27,18 +28,26 @@ export function RemoveAccount({ const { i18n } = useTranslationContext(); const result = useAccountDetails(account); const [accountName, setAccountName] = useState() - const { deleteAccount } = useAdminAccountAPI(); - if (!result.ok) { - if (result.loading || result.type === ErrorType.TIMEOUT) { - return onLoadNotOk(result); - } - if (result.status === HttpStatusCode.NotFound) { - return
account not found
; + const { state } = useBackendState(); + const token = state.status !== "loggedIn" ? undefined : state.token + const { api } = useBankCoreApiContext() + + if (!result) { + return + } + if (result instanceof TalerError) { + return + } + if (result.type === "fail") { + switch (result.case) { + case "unauthorized": return + case "not-found": return + default: assertUnreachable(result) } - return onLoadNotOk(result); } - const balance = Amounts.parse(result.data.balance.amount); + + const balance = Amounts.parse(result.body.balance.amount); if (!balance) { return
there was an error reading the balance
; } @@ -50,23 +59,45 @@ export function RemoveAccount({ } async function doRemove() { + if (!token) return; try { - const r = await deleteAccount(account); - onUpdateSuccess(); + const resp = await api.deleteAccount({ username: account, token }); + if (resp.type === "ok") { + onUpdateSuccess(); + } else { + switch (resp.case) { + case "unauthorized": return notify({ + type: "error", + title: i18n.str`No enough permission to delete the account.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "not-found": return notify({ + type: "error", + title: i18n.str`The username was not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "unable-to-delete": return notify({ + type: "error", + title: i18n.str`The administrator specified a institutional username.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "balance-not-zero": return notify({ + type: "error", + title: i18n.str`Can't delete an account with balance different than zero.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: { + assertUnreachable(resp) + } + } + } } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Forbidden - ? i18n.str`The administrator specified a institutional username` - : status === HttpStatusCode.NotFound - ? i18n.str`The username was not found` - : status === HttpStatusCode.PreconditionFailed - ? i18n.str`Balance was not zero` - : undefined, - }), - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError(i18n.str`Operation failed, please report`, (error instanceof Error diff --git a/packages/demobank-ui/src/pages/business/Home.tsx b/packages/demobank-ui/src/pages/business/Home.tsx index 1a84effcd..03d7895e3 100644 --- a/packages/demobank-ui/src/pages/business/Home.tsx +++ b/packages/demobank-ui/src/pages/business/Home.tsx @@ -16,26 +16,28 @@ import { AmountJson, Amounts, - HttpStatusCode, + TalerError, TranslatedString } from "@gnu-taler/taler-util"; import { - HttpResponse, - HttpResponsePaginated, - RequestError, notify, notifyError, notifyInfo, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; +import { mutate } from "swr"; import { Cashouts } from "../../components/Cashouts/index.js"; +import { ErrorLoading } from "../../components/ErrorLoading.js"; +import { Loading } from "../../components/Loading.js"; import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { useBankCoreApiContext } from "../../context/config.js"; import { useAccountDetails } from "../../hooks/access.js"; +import { useBackendState } from "../../hooks/backend.js"; import { useCashoutDetails, - useCircuitAccountAPI, useEstimator, useRatiosAndFeeConfig, } from "../../hooks/circuit.js"; @@ -44,7 +46,7 @@ import { buildRequestErrorMessage, undefinedIfEmpty, } from "../../utils.js"; -import { handleNotOkResult } from "../HomePage.js"; +import { LoginForm } from "../LoginForm.js"; import { InputAmount } from "../PaytoWireTransferForm.js"; import { ShowAccountDetails } from "../ShowAccountDetails.js"; import { UpdateAccountPassword } from "../UpdateAccountPassword.js"; @@ -53,12 +55,10 @@ interface Props { account: string, onClose: () => void; onRegister: () => void; - onLoadNotOk: () => void; } export function BusinessAccount({ onClose, account, - onLoadNotOk, onRegister, }: Props): VNode { const { i18n } = useTranslationContext(); @@ -68,12 +68,10 @@ export function BusinessAccount({ string | undefined >(); - if (newCashout) { return ( { setNewcashout(false); }} @@ -91,7 +89,6 @@ export function BusinessAccount({ return ( { setShowCashoutDetails(undefined); }} @@ -102,7 +99,6 @@ export function BusinessAccount({ return ( { notifyInfo(i18n.str`Password changed`); setUpdatePassword(false); @@ -117,7 +113,6 @@ export function BusinessAccount({
{ notifyInfo(i18n.str`Account updated`); }} @@ -158,11 +153,6 @@ interface PropsCashout { account: string; onComplete: (id: string) => void; onCancel: () => void; - onLoadNotOk: ( - error: - | HttpResponsePaginated - | HttpResponse, - ) => VNode; } type FormType = { @@ -175,88 +165,78 @@ type ErrorFrom = { [P in keyof T]+?: string; }; -// check #7719 -function useRatiosAndFeeConfigWithChangeDetection(): HttpResponse< - SandboxBackend.Circuit.Config & { hasChanged?: boolean }, - SandboxBackend.SandboxError -> { - const result = useRatiosAndFeeConfig(); - const [oldResult, setOldResult] = useState< - SandboxBackend.Circuit.Config | undefined - >(undefined); - const dataFromBackend = result.ok ? result.data : undefined; - useEffect(() => { - // save only the first result of /config to the backend - if (!dataFromBackend || oldResult !== undefined) return; - setOldResult(dataFromBackend); - }, [dataFromBackend]); - - if (!result.ok) return result; - - const data = !oldResult ? result.data : oldResult; - const hasChanged = - oldResult && - (result.data.name !== oldResult.name || - result.data.version !== oldResult.version || - result.data.ratios_and_fees.buy_at_ratio !== - oldResult.ratios_and_fees.buy_at_ratio || - result.data.ratios_and_fees.buy_in_fee !== - oldResult.ratios_and_fees.buy_in_fee || - result.data.ratios_and_fees.sell_at_ratio !== - oldResult.ratios_and_fees.sell_at_ratio || - result.data.ratios_and_fees.sell_out_fee !== - oldResult.ratios_and_fees.sell_out_fee || - result.data.fiat_currency !== oldResult.fiat_currency); - - return { - ...result, - data: { ...data, hasChanged }, - }; -} function CreateCashout({ - account, + account: accountName, onComplete, onCancel, - onLoadNotOk, }: PropsCashout): VNode { const { i18n } = useTranslationContext(); - const ratiosResult = useRatiosAndFeeConfig(); - const result = useAccountDetails(account); + const resultRatios = useRatiosAndFeeConfig(); + const resultAccount = useAccountDetails(accountName); const { estimateByCredit: calculateFromCredit, estimateByDebit: calculateFromDebit, } = useEstimator(); + const { state } = useBackendState() + const creds = state.status !== "loggedIn" ? undefined : state + const { api, config } = useBankCoreApiContext() const [form, setForm] = useState>({ isDebit: true }); - const { createCashout } = useCircuitAccountAPI(); - if (!result.ok) return onLoadNotOk(result); - if (!ratiosResult.ok) return onLoadNotOk(ratiosResult); - const config = ratiosResult.data; + if (!resultAccount || !resultRatios) { + return + } + if (resultAccount instanceof TalerError) { + return + } + if (resultRatios instanceof TalerError) { + return + } + if (resultAccount.type === "fail") { + switch (resultAccount.case) { + case "unauthorized": return + case "not-found": return + default: assertUnreachable(resultAccount) + } + } + + if (resultRatios.type === "fail") { + switch (resultRatios.case) { + case "not-supported": return
cashout operations are not supported
+ default: assertUnreachable(resultRatios.case) + } + } + if (!config.fiat_currency) { + return
cashout operations are not supported
+ } - const balance = Amounts.parseOrThrow(result.data.balance.amount); - const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit"; + const ratio = resultRatios.body - const debitThreshold = Amounts.parseOrThrow(result.data.debit_threshold); - const zero = Amounts.zeroOfCurrency(balance.currency); - const limit = balanceIsDebit - ? Amounts.sub(debitThreshold, balance).amount - : Amounts.add(balance, debitThreshold).amount; + const account = { + balance: Amounts.parseOrThrow(resultAccount.body.balance.amount), + balanceIsDebit: resultAccount.body.balance.credit_debit_indicator == "debit", + debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold) + } + + const zero = Amounts.zeroOfCurrency(account.balance.currency); + const limit = account.balanceIsDebit + ? Amounts.sub(account.debitThreshold, account.balance).amount + : Amounts.add(account.balance, account.debitThreshold).amount; const zeroCalc = { debit: zero, credit: zero, beforeFee: zero }; const [calc, setCalc] = useState(zeroCalc); - const sellRate = config.ratios_and_fees.sell_at_ratio; - const sellFee = !config.ratios_and_fees.sell_out_fee + + const sellRate = ratio.sell_at_ratio; + const sellFee = !ratio.sell_out_fee ? zero : Amounts.parseOrThrow( - `${balance.currency}:${config.ratios_and_fees.sell_out_fee}`, + `${account.balance.currency}:${ratio.sell_out_fee}`, ); - const fiatCurrency = config.fiat_currency; if (!sellRate || sellRate < 0) return
error rate
; const amount = Amounts.parseOrThrow( - `${!form.isDebit ? fiatCurrency : balance.currency}:${!form.amount ? "0" : form.amount + `${!form.isDebit ? config.fiat_currency.name : account.balance.currency}:${!form.amount ? "0" : form.amount }`, ); @@ -267,15 +247,16 @@ function CreateCashout({ setCalc(r); }) .catch((error) => { - notify( - error instanceof RequestError - ? buildRequestErrorMessage(i18n, error.cause) - : { - type: "error", - title: i18n.str`Could not estimate the cashout`, - description: error.message as TranslatedString - }, - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } }); } else { calculateFromCredit(amount, sellFee, sellRate) @@ -283,20 +264,21 @@ function CreateCashout({ setCalc(r); }) .catch((error) => { - notify( - error instanceof RequestError - ? buildRequestErrorMessage(i18n, error.cause) - : { - type: "error", - title: i18n.str`Could not estimate the cashout`, - description: error.message, - }, - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } }); } }, [form.amount, form.isDebit]); - const balanceAfter = Amounts.sub(balance, calc.debit).amount; + const balanceAfter = Amounts.sub(account.balance, calc.debit).amount; function updateForm(newForm: typeof form): void { setForm(newForm); @@ -374,8 +356,8 @@ function CreateCashout({
@@ -384,7 +366,7 @@ function CreateCashout({ >{i18n.str`Total cost`}
@@ -392,7 +374,7 @@ function CreateCashout({ {" "} @@ -402,7 +384,7 @@ function CreateCashout({ @@ -411,7 +393,7 @@ function CreateCashout({ @@ -423,7 +405,7 @@ function CreateCashout({ >{i18n.str`Total cashout transfer`} @@ -501,35 +483,55 @@ function CreateCashout({ onClick={async (e) => { e.preventDefault(); - if (errors) return; + if (errors || !creds) return; try { - const res = await createCashout({ + const resp = await api.createCashout(creds, { amount_credit: Amounts.stringify(calc.credit), amount_debit: Amounts.stringify(calc.debit), subject: form.subject, tan_channel: form.channel, }); - onComplete(res.data.uuid); + if (resp.type === "ok") { + mutate(() => true)// clean cashout list + onComplete(resp.body.cashout_id); + } else { + switch (resp.case) { + case "incorrect-exchange-rate": return notify({ + type: "error", + title: i18n.str`The exchange rate was incorrectly applied`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "no-allowed": return notify({ + type: "error", + title: i18n.str`This user is not allowed to make a cashout`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "no-contact-info": return notify({ + type: "error", + title: i18n.str`Need a contact data where to send the TAN`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "no-enough-balance": return notify({ + type: "error", + title: i18n.str`The account does not have sufficient funds`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "tan-not-supported": return notify({ + type: "error", + title: i18n.str`The bank does not support the TAN channel for this operation`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + default: assertUnreachable(resp) + } + } } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.BadRequest - ? i18n.str`The exchange rate was incorrectly applied` - : status === HttpStatusCode.Forbidden - ? i18n.str`A institutional user tried the operation` - : status === HttpStatusCode.Conflict - ? i18n.str`Need a contact data where to send the TAN` - : status === HttpStatusCode.PreconditionFailed - ? i18n.str`The account does not have sufficient funds` - : undefined, - onServerError: (status) => - status === HttpStatusCode.ServiceUnavailable - ? i18n.str`The bank does not support the TAN channel for this operation` - : undefined, - }), - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError( i18n.str`Operation failed, please report`, @@ -552,24 +554,34 @@ function CreateCashout({ interface ShowCashoutProps { id: string; onCancel: () => void; - onLoadNotOk: ( - error: HttpResponsePaginated, - ) => VNode; } export function ShowCashoutDetails({ id, onCancel, - onLoadNotOk, }: ShowCashoutProps): VNode { const { i18n } = useTranslationContext(); + const { state } = useBackendState(); + const creds = state.status !== "loggedIn" ? undefined : state + const { api } = useBankCoreApiContext() const result = useCashoutDetails(id); - const { abortCashout, confirmCashout } = useCircuitAccountAPI(); const [code, setCode] = useState(undefined); - if (!result.ok) return onLoadNotOk(result); + + if (!result) { + return + } + if (result instanceof TalerError) { + return + } + if (result.type === "fail") { + switch (result.case) { + case "already-aborted": return
this cashout is already aborted
+ default: assertUnreachable(result.case) + } + } const errors = undefinedIfEmpty({ code: !code ? i18n.str`required` : undefined, }); - const isPending = String(result.data.status).toUpperCase() === "PENDING"; + const isPending = String(result.body.status).toUpperCase() === "PENDING"; return (

Cashout details {id}

@@ -578,43 +590,47 @@ export function ShowCashoutDetails({ - +
- +
- +
- +
- +
- +
- +
{isPending ? (
@@ -652,21 +668,33 @@ export function ShowCashoutDetails({ class="pure-button pure-button-primary button-error" onClick={async (e) => { e.preventDefault(); + if (!creds) return; try { - await abortCashout(id); - onCancel(); + const resp = await api.abortCashoutById(creds, id); + if (resp.type === "ok") { + onCancel(); + } else { + switch (resp.case) { + case "not-found": 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 "already-confirmed": return notify({ + type: "error", + title: i18n.str`Cashout was already confimed.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: { + assertUnreachable(resp) + } + } + } } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.NotFound - ? i18n.str`Cashout not found. It may be also mean that it was already aborted.` - : status === HttpStatusCode.PreconditionFailed - ? i18n.str`Cashout was already confimed` - : undefined, - }), - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError( i18n.str`Operation failed, please report`, @@ -687,27 +715,40 @@ export function ShowCashoutDetails({ class="pure-button pure-button-primary " onClick={async (e) => { e.preventDefault(); + if (!creds) return; try { if (!code) return; - const rest = await confirmCashout(id, { + const resp = await api.confirmCashoutById(creds, id, { tan: code, }); + if (resp.type === "ok") { + mutate(() => true)//clean cashout state + } else { + switch (resp.case) { + case "not-found": 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 "wrong-tan-or-credential": return notify({ + type: "error", + title: i18n.str`Invalid code or credentials.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "cashout-address-changed": return notify({ + type: "error", + title: i18n.str`The cash-out address between the creation and the confirmation changed.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: assertUnreachable(resp) + } + } } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.NotFound - ? i18n.str`Cashout not found. It may be also mean that it was already aborted.` - : status === HttpStatusCode.PreconditionFailed - ? i18n.str`Cashout was already confimed` - : status === HttpStatusCode.Conflict - ? i18n.str`Confirmation failed. Maybe the user changed their cash-out address between the creation and the confirmation` - : status === HttpStatusCode.Forbidden - ? i18n.str`Invalid code` - : undefined, - }), - ); + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) } else { notifyError( i18n.str`Operation failed, please report`, diff --git a/packages/demobank-ui/src/stories.test.ts b/packages/demobank-ui/src/stories.test.ts index 07db7d8cf..265304b25 100644 --- a/packages/demobank-ui/src/stories.test.ts +++ b/packages/demobank-ui/src/stories.test.ts @@ -18,7 +18,7 @@ * * @author Sebastian Javier Marchano (sebasjm) */ -import { setupI18n } from "@gnu-taler/taler-util"; +import { AccessToken, setupI18n } from "@gnu-taler/taler-util"; import { parseGroupImport } from "@gnu-taler/web-util/browser"; import * as tests from "@gnu-taler/web-util/testing"; import * as components from "./components/index.examples.js"; @@ -26,7 +26,6 @@ import * as pages from "./pages/index.stories.js"; import { ComponentChildren, VNode, h as create } from "preact"; import { BackendStateProviderTesting } from "./context/backend.js"; -import { AccessToken } from "./hooks/useCredentialsChecker.js"; setupI18n("en", { en: {} }); diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts index e7673f078..310e80cd6 100644 --- a/packages/demobank-ui/src/utils.ts +++ b/packages/demobank-ui/src/utils.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see */ -import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { HttpStatusCode, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util"; import { ErrorNotification, ErrorType, @@ -62,15 +62,15 @@ export type PartialButDefined = { export type WithIntermediate = { [prop in keyof Type]: Type[prop] extends object - ? WithIntermediate - : Type[prop] | undefined; + ? WithIntermediate + : Type[prop] | undefined; }; export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] - ? RecursivePartial[] - : T[P] extends object - ? RecursivePartial - : T[P]; + ? RecursivePartial[] + : T[P] extends object + ? RecursivePartial + : T[P]; }; export enum TanChannel { @@ -94,59 +94,61 @@ export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1; export function buildRequestErrorMessage( i18n: ReturnType["i18n"], - cause: HttpError, - specialCases: { - onClientError?: (status: HttpStatusCode) => TranslatedString | undefined; - onServerError?: (status: HttpStatusCode) => TranslatedString | undefined; - } = {}, + cause: TalerError<{}>, ): ErrorNotification { let result: ErrorNotification; - switch (cause.type) { - case ErrorType.TIMEOUT: { + switch (cause.errorDetail.code) { + case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { result = { type: "error", title: i18n.str`Request timeout`, + description: cause.message as TranslatedString, + debug: JSON.stringify(cause.errorDetail, undefined, 2), }; break; } - case ErrorType.CLIENT: { - const title = - specialCases.onClientError && specialCases.onClientError(cause.status); + case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: { result = { type: "error", - title: title ? title : i18n.str`The server didn't accept the request`, - description: cause?.payload?.error?.description as TranslatedString, - debug: JSON.stringify(cause), + title: i18n.str`Request throttled`, + description: cause.message as TranslatedString, + debug: JSON.stringify(cause.errorDetail, undefined, 2), }; break; } - case ErrorType.SERVER: { - const title = - specialCases.onServerError && specialCases.onServerError(cause.status); + case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: { result = { type: "error", - title: title - ? title - : i18n.str`The server had problems processing the request`, - description: cause?.payload?.error?.description as TranslatedString, - debug: JSON.stringify(cause), + title: i18n.str`Malformed response`, + description: cause.message as TranslatedString, + debug: JSON.stringify(cause.errorDetail, undefined, 2), }; break; } - case ErrorType.UNREADABLE: { + case TalerErrorCode.WALLET_NETWORK_ERROR: { result = { type: "error", - title: i18n.str`Unexpected error`, - description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}` as TranslatedString, - debug: JSON.stringify(cause), + title: i18n.str`Network error`, + description: cause.message as TranslatedString, + debug: JSON.stringify(cause.errorDetail, undefined, 2), + }; + break; + } + case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { + result = { + type: "error", + title: i18n.str`Unexpected request error`, + description: cause.message as TranslatedString, + debug: JSON.stringify(cause.errorDetail, undefined, 2), }; break; } - case ErrorType.UNEXPECTED: { + default: { result = { type: "error", title: i18n.str`Unexpected error`, - debug: JSON.stringify(cause), + description: cause.message as TranslatedString, + debug: JSON.stringify(cause.errorDetail, undefined, 2), }; break; } -- cgit v1.2.3