summaryrefslogtreecommitdiff
path: root/packages/demobank-ui
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-10-19 02:55:57 -0300
committerSebastian <sebasjm@gmail.com>2023-10-19 02:56:15 -0300
commit366cccb8fcae6a9971a1e8a9143d821e289339d1 (patch)
treefcaa481f7053ef11c92e988d3fb84bf3cedbaba3 /packages/demobank-ui
parenta67518ab1a865fc79374a19bce6513b0caa2eab6 (diff)
downloadwallet-core-366cccb8fcae6a9971a1e8a9143d821e289339d1.tar.gz
wallet-core-366cccb8fcae6a9971a1e8a9143d821e289339d1.tar.bz2
wallet-core-366cccb8fcae6a9971a1e8a9143d821e289339d1.zip
integrate bank into the new taler-util API
Diffstat (limited to 'packages/demobank-ui')
-rw-r--r--packages/demobank-ui/src/components/Cashouts/index.ts6
-rw-r--r--packages/demobank-ui/src/components/Cashouts/state.ts7
-rw-r--r--packages/demobank-ui/src/components/Cashouts/views.tsx4
-rw-r--r--packages/demobank-ui/src/components/ErrorLoading.tsx107
-rw-r--r--packages/demobank-ui/src/components/Routing.tsx7
-rw-r--r--packages/demobank-ui/src/components/Transactions/index.ts4
-rw-r--r--packages/demobank-ui/src/components/Transactions/state.ts18
-rw-r--r--packages/demobank-ui/src/components/Transactions/test.ts94
-rw-r--r--packages/demobank-ui/src/components/app.tsx90
-rw-r--r--packages/demobank-ui/src/context/config.ts65
-rw-r--r--packages/demobank-ui/src/declaration.d.ts520
-rw-r--r--packages/demobank-ui/src/hooks/access.ts401
-rw-r--r--packages/demobank-ui/src/hooks/backend.ts265
-rw-r--r--packages/demobank-ui/src/hooks/circuit.ts548
-rw-r--r--packages/demobank-ui/src/hooks/config.ts59
-rw-r--r--packages/demobank-ui/src/hooks/useCredentialsChecker.ts135
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/index.ts25
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/state.ts51
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/views.tsx34
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx36
-rw-r--r--packages/demobank-ui/src/pages/HomePage.tsx78
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx104
-rw-r--r--packages/demobank-ui/src/pages/OperationState/index.ts10
-rw-r--r--packages/demobank-ui/src/pages/OperationState/state.ts162
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx77
-rw-r--r--packages/demobank-ui/src/pages/PublicHistoriesPage.tsx25
-rw-r--r--packages/demobank-ui/src/pages/QrCodeSection.tsx50
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx110
-rw-r--r--packages/demobank-ui/src/pages/ShowAccountDetails.tsx123
-rw-r--r--packages/demobank-ui/src/pages/UpdateAccountPassword.tsx60
-rw-r--r--packages/demobank-ui/src/pages/WalletWithdrawForm.tsx60
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx77
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx98
-rw-r--r--packages/demobank-ui/src/pages/admin/Account.tsx37
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx72
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountList.tsx34
-rw-r--r--packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx80
-rw-r--r--packages/demobank-ui/src/pages/admin/Home.tsx13
-rw-r--r--packages/demobank-ui/src/pages/admin/RemoveAccount.tsx101
-rw-r--r--packages/demobank-ui/src/pages/business/Home.tsx377
-rw-r--r--packages/demobank-ui/src/stories.test.ts3
-rw-r--r--packages/demobank-ui/src/utils.ts70
42 files changed, 1559 insertions, 2738 deletions
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<SandboxBackend.SandboxError>;
+ 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 <http://www.gnu.org/licenses/>
*/
+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 (
<tr key={idx}>
- <td>{format(item.creation_time, "dd/MM/yyyy HH:mm:ss")}</td>
+ <td>{item.creation_time.t_s === "never" ? i18n.str`never` : format(item.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")}</td>
<td>
{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")
: "-"}
</td>
<td><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} /></td>
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 <http://www.gnu.org/licenses/>
*/
-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<SandboxBackend.SandboxError> }): VNode {
+export function ErrorLoading({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode {
const { i18n } = useTranslationContext()
- return (<Attention type="danger" title={error.message as TranslatedString}>
- <p class="text-sm font-medium text-red-800">Got status "{error.info.status}" on {error.info.url}</p>
- </Attention>
- );
+ 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 <Attention type="danger" title={i18n.str`The request reached a timeout, check your connection.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ 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 <Attention type="danger" title={i18n.str`A lot of request were made to the same server and this action was throttled`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ 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 <Attention type="danger" title={i18n.str`The response of the request is malformed.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_NETWORK_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) {
+ const { requestMethod, requestUrl } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`Could not complete the request due to a network problem.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ 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 <Attention type="danger" title={i18n.str`Unexpected request error`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ //////////////////
+ // Every other error
+ //////////////////
+ // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ // return <Attention type="danger" title={i18n.str``}>
+ // </Attention>
+ // }
+ //////////////////
+ // Default message for unhandled case
+ //////////////////
+ default: return <Attention type="danger" title={i18n.str`Unexpected error`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify(error.errorDetail, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
}
+
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 <BankFrame >
<Router history={history}>
@@ -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<SandboxBackend.SandboxError>;
+ 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 <http://www.gnu.org/licenses/>
*/
-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 (
<TranslationProvider source={strings}>
<BackendStateProvider>
- <VersionCheck>
+ <BankCoreApiProvider baseUrl={baseUrl}>
<SWRConfig
value={{
provider: WITH_LOCAL_STORAGE_CACHE
@@ -65,34 +44,15 @@ const App: FunctionalComponent = () => {
>
<Routing />
</SWRConfig>
- </VersionCheck>
+ </BankCoreApiProvider>
</BackendStateProvider>
</TranslationProvider >
);
};
+
(window as any).setGlobalLogLevelFromString = setGlobalLogLevelFromString;
(window as any).getGlobalLevel = getGlobalLogLevel;
-function VersionCheck({ children }: { children: ComponentChildren }): VNode {
- const checked = useConfigState()
-
- if (checked === undefined) {
- return <Loading />
- }
- if (checked.type === "wrong") {
- return <BankFrame>
- the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}"
- </BankFrame>
- }
- if (checked.type === "ok") {
- return <ConfigStateProvider value={checked.result}>{children}</ConfigStateProvider>
- }
-
- return <BankFrame>
- <ErrorLoading error={checked.result} />
- </BankFrame>
-}
-
function localStorageProvider(): Map<unknown, unknown> {
const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
@@ -104,3 +64,31 @@ function localStorageProvider(): Map<unknown, unknown> {
}
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 <http://www.gnu.org/licenses/>
*/
+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<SandboxBackend.Config>;
-
-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<Type>(initial);
-export const useConfigContext = (): Type => useContext(Context);
+const Context = createContext<Type>(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<ConfigResult>()
+ 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 <http://www.gnu.org/licenses/>
*/
-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<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>
- > => {
- const res =
- await request<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>(
- `accounts/${account}/withdrawals`,
- {
- method: "POST",
- data,
- contentType: "json",
- },
- );
- return res;
- };
- const createTransaction = async (
- data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(
- `accounts/${account}/transactions`,
- {
- method: "POST",
- data,
- contentType: "json",
- },
- );
- await mutateAll(/.*accounts\/.*/);
- return res;
- };
- const deleteAccount = async (): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`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<HttpResponseOk<void>> => {
- const res = await request<void>(`withdrawals/${id}/abort`, {
- method: "POST",
- contentType: "json",
- });
- await mutateAll(/.*withdrawals\/.*/);
- return res;
- };
- const confirmWithdrawal = async (
- id: string,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`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<HttpResponseOk<void>> => {
- // FIXME: This API is deprecated. The normal account registration API should be used instead.
- const res = await noAuthRequest<void>(`accounts`, {
- method: "POST",
- data,
- contentType: "json",
- });
- await mutateAll(/.*accounts\/.*/);
- return res;
- };
-
- return { register };
-}
-
-export interface TestingAPI {
- register: (
- data: SandboxBackend.CoreBank.RegisterAccountRequest,
- ) => Promise<HttpResponseOk<void>>;
-}
-
-export interface AccessAPI {
- createWithdrawal: (
- data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest,
- ) => Promise<
- HttpResponseOk<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>
- >;
- createTransaction: (
- data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate,
- ) => Promise<HttpResponseOk<void>>;
- deleteAccount: () => Promise<HttpResponseOk<void>>;
-}
-export interface AccessAnonAPI {
- abortWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>;
- confirmWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>;
-}
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<SandboxBackend.CoreBank.AccountData>,
- RequestError<SandboxBackend.SandboxError>
- >([`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<TalerCoreBankResultByMethod<"getAccount">, 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<SandboxBackend.CoreBank.BankAccountGetWithdrawalResponse>,
- RequestError<SandboxBackend.SandboxError>
- >([`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<TalerCoreBankResultByMethod<"getWithdrawalById">, 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<SandboxBackend.CoreBank.BankAccountTransactionInfo>,
- RequestError<SandboxBackend.SandboxError>
- >([`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<TalerCoreBankResultByMethod<"getTransactionById">, 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<number | undefined>(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<TalerCoreBankResultByMethod<"getPublicAccounts">, 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<SandboxBackend.CoreBank.PublicAccountsResponse>,
- RequestError<SandboxBackend.SandboxError>
- >([`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<string>();
-
- const {
- data: afterData,
- error: afterError,
- isValidating: loadingAfter,
- } = useSWR<
- HttpResponseOk<SandboxBackend.CoreBank.BankAccountTransactionsResponse>,
- RequestError<SandboxBackend.SandboxError>
- >(
- [`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<number | undefined>(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<TalerCoreBankResultByMethod<"getTransactions">, 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<BackendState> =>
.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: <T>(
- path: string,
- options?: RequestOptions,
- ) => Promise<HttpResponseOk<T>>;
- fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
- multiFetcher: <T>(endpoint: string[][]) => Promise<HttpResponseOk<T>[]>;
- paginatedFetcher: <T>(
- args: [string, string | undefined, number],
- ) => Promise<HttpResponseOk<T>>;
- sandboxAccountsFetcher: <T>(
- args: [string, number, number, string],
- ) => Promise<HttpResponseOk<T>>;
- sandboxCashoutFetcher: <T>(endpoint: string[]) => Promise<HttpResponseOk<T>>;
-}
-export function usePublicBackend(): useBackendType {
- const { request: requestHandler } = useApiContext();
-
- const baseUrl = getInitialBackendBaseURL();
-
- const request = useCallback(
- function requestImpl<T>(
- path: string,
- options: RequestOptions = {},
- ): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, path, options);
- },
- [baseUrl],
- );
-
- const fetcher = useCallback(
- function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint);
- },
- [baseUrl],
- );
- const paginatedFetcher = useCallback(
- function fetcherImpl<T>([endpoint, start, size]: [
- string,
- string | undefined,
- number,
- ]): Promise<HttpResponseOk<T>> {
- const delta = -1 * size //descending order
- const params = start ? { delta, start } : { delta }
- return requestHandler<T>(baseUrl, endpoint, {
- params,
- });
- },
- [baseUrl],
- );
- const multiFetcher = useCallback(
- function multiFetcherImpl<T>([endpoints]: string[][]): Promise<
- HttpResponseOk<T>[]
- > {
- return Promise.all(
- endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint)),
- );
- },
- [baseUrl],
- );
- const sandboxAccountsFetcher = useCallback(
- function fetcherImpl<T>([endpoint, page, size, account]: [
- string,
- number,
- number,
- string,
- ]): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, {
- params: { page: page || 1, size },
- });
- },
- [baseUrl],
- );
- const sandboxCashoutFetcher = useCallback(
- function fetcherImpl<T>([endpoint, account]: string[]): Promise<
- HttpResponseOk<T>
- > {
- return requestHandler<T>(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<any>["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<T>(
- path: string,
- options: RequestOptions = {},
- ): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, path, { token: creds, ...options });
- },
- [baseUrl, creds],
- );
-
- const fetcher = useCallback(
- function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, { token: creds });
- },
- [baseUrl, creds],
- );
- const paginatedFetcher = useCallback(
- function fetcherImpl<T>([endpoint, start, size]: [
- string,
- string | undefined,
- number,
- ]): Promise<HttpResponseOk<T>> {
- const delta = -1 * size //descending order
- const params = start ? { delta, start } : { delta }
- return requestHandler<T>(baseUrl, endpoint, {
- token: creds,
- params,
- });
- },
- [baseUrl, creds],
- );
- const multiFetcher = useCallback(
- function multiFetcherImpl<T>([endpoints]: string[][]): Promise<
- HttpResponseOk<T>[]
- > {
- return Promise.all(
- endpoints.map((endpoint) =>
- requestHandler<T>(baseUrl, endpoint, { token: creds }),
- ),
- );
- },
- [baseUrl, creds],
- );
- const sandboxAccountsFetcher = useCallback(
- function fetcherImpl<T>([endpoint, page, size, account]: [
- string,
- number,
- number,
- string,
- ]): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, {
- token: creds,
- params: { page: page || 1, size },
- });
- },
- [baseUrl],
- );
-
- const sandboxCashoutFetcher = useCallback(
- function fetcherImpl<T>([endpoint, account]: string[]): Promise<
- HttpResponseOk<T>
- > {
- return requestHandler<T>(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<Type, SandboxBackend.SandboxError>
- >({ loading: true });
-
- useEffect(() => {
- request<Type>(`/config`)
- .then((data) => setResult(data))
- .catch((error: RequestError<SandboxBackend.SandboxError>) =>
- 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 <http://www.gnu.org/licenses/>
*/
-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<HttpResponseOk<void>> => {
- const res = await request<void>(`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<HttpResponseOk<void>> => {
- const res = await request<void>(`circuit-api/accounts/${account}`, {
- method: "PATCH",
- data,
- contentType: "json",
- });
- await mutateAll(/.*circuit-api\/accounts.*/);
- return res;
- };
- const deleteAccount = async (
- account: string,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`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<HttpResponseOk<void>> => {
- const res = await request<void>(`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<HttpResponseOk<void>> => {
- const res = await request<void>(`circuit-api/accounts/${account}`, {
- method: "PATCH",
- data,
- contentType: "json",
- });
- await mutateAll(/.*circuit-api\/accounts.*/);
- return res;
- };
- const changePassword = async (
- data: SandboxBackend.Circuit.AccountPasswordChange,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`circuit-api/accounts/${account}/auth`, {
- method: "PATCH",
- data,
- contentType: "json",
- });
- return res;
- };
-
- const createCashout = async (
- data: SandboxBackend.Circuit.CashoutRequest,
- ): Promise<HttpResponseOk<SandboxBackend.Circuit.CashoutPending>> => {
- const res = await request<SandboxBackend.Circuit.CashoutPending>(
- `circuit-api/cashouts`,
- {
- method: "POST",
- data,
- contentType: "json",
- },
- );
- return res;
- };
-
- const confirmCashout = async (
- cashoutId: string,
- data: SandboxBackend.Circuit.CashoutConfirm,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(
- `circuit-api/cashouts/${cashoutId}/confirm`,
- {
- method: "POST",
- data,
- contentType: "json",
- },
- );
- await mutateAll(/.*circuit-api\/cashout.*/);
- return res;
- };
-
- const abortCashout = async (
- cashoutId: string,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`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<HttpResponseOk<void>>;
- deleteAccount: (account: string) => Promise<HttpResponseOk<void>>;
-
- updateAccount: (
- account: string,
- data: SandboxBackend.Circuit.CircuitAccountReconfiguration,
- ) => Promise<HttpResponseOk<void>>;
- changePassword: (
- account: string,
- data: SandboxBackend.Circuit.AccountPasswordChange,
- ) => Promise<HttpResponseOk<void>>;
-}
-
-export interface CircuitAccountAPI {
- updateAccount: (
- data: SandboxBackend.Circuit.CircuitAccountReconfiguration,
- ) => Promise<HttpResponseOk<void>>;
- changePassword: (
- data: SandboxBackend.Circuit.AccountPasswordChange,
- ) => Promise<HttpResponseOk<void>>;
- createCashout: (
- data: SandboxBackend.Circuit.CashoutRequest,
- ) => Promise<HttpResponseOk<SandboxBackend.Circuit.CashoutPending>>;
- confirmCashout: (
- id: string,
- data: SandboxBackend.Circuit.CashoutConfirm,
- ) => Promise<HttpResponseOk<void>>;
- abortCashout: (id: string) => Promise<HttpResponseOk<void>>;
-}
-
-async function getBusinessStatus(
- request: ReturnType<typeof useApiContext>["request"],
- username: string,
- token: AccessToken,
-): Promise<boolean> {
- try {
- const url = getInitialBackendBaseURL();
- const result = await request<SandboxBackend.Circuit.CircuitAccountData>(
- url,
- `circuit-api/accounts/${username}`,
- { token },
- );
- return result.ok;
- } catch (error) {
- return false;
- }
-}
-
-async function getEstimationByCredit(
- request: ReturnType<typeof useApiContext>["request"],
- basicAuth: { username: string; password: string },
-): Promise<boolean> {
- try {
- const url = getInitialBackendBaseURL();
- const result = await request<
- HttpResponseOk<SandboxBackend.Circuit.CircuitAccountData>
- >(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<SandboxBackend.Circuit.CashoutEstimate>(
- 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<SandboxBackend.Circuit.CashoutEstimate>(
- 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<boolean | undefined>();
- 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<SandboxBackend.Circuit.CircuitAccountData>,
- RequestError<SandboxBackend.SandboxError>
- >([`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<SandboxBackend.Circuit.Config>,
- RequestError<SandboxBackend.SandboxError>
- >([`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<SandboxBackend.Circuit.CircuitAccounts>,
- RequestError<SandboxBackend.SandboxError>
- >(
- [`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<string | undefined>();
- // const [lastAfter, setLastAfter] = useState<
- // HttpResponse<SandboxBackend.Circuit.CircuitAccounts, SandboxBackend.SandboxError>
- // >({ 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<TalerCoreBankResultByMethod<"getAccounts">, 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<CashoutWithId | undefined> = 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<SandboxBackend.Circuit.Cashouts>,
- RequestError<SandboxBackend.SandboxError>
- >([`circuit-api/cashouts`, account], sandboxCashoutFetcher, {
+ const { data, error } = useSWR<OperationOk<{ cashouts: CashoutWithId[] }>, 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<SandboxBackend.Circuit.CashoutStatusResponse>[],
- RequestError<SandboxBackend.SandboxError>
- >([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<SandboxBackend.Circuit.CashoutStatusResponse>,
- RequestError<SandboxBackend.SandboxError>
- >([`circuit-api/cashouts/${id}`], fetcher, {
+ const { data, error } = useSWR<TalerCoreBankResultByMethod<"getCashoutById">, 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<typeof useApiContext>["request"],
-): Promise<SandboxBackend.Config> {
- const url = getInitialBackendBaseURL();
- const result = await request<SandboxBackend.Config>(url, `config`);
- return result.data;
-}
-
-export type ConfigResult = undefined
- | { type: "ok", result: Required<SandboxBackend.Config> }
- | { type: "wrong", result: SandboxBackend.Config }
- | { type: "error", result: HttpError<SandboxBackend.SandboxError> }
-
-export function useConfigState(): ConfigResult {
- const [checked, setChecked] = useState<ConfigResult>()
- const { request } = useApiContext();
-
- useEffect(() => {
- getConfigState(request)
- .then((result) => {
- const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, result.version)
- if (r?.compatible) {
- const complete: Required<SandboxBackend.Config> = {
- 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<LoginResult> {
- 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<LoginTokenSuccessResponse>(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<LoginResult> {
-
- 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 <http://www.gnu.org/licenses/>
*/
-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: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => VNode;
goToBusinessAccount: () => void;
goToConfirmOperation: (id: string) => void;
}
@@ -42,7 +39,7 @@ export namespace State {
export interface LoadingError {
status: "loading-error";
- error: HttpError<SandboxBackend.SandboxError>;
+ error: TalerError;
}
export interface BaseInfo {
@@ -60,12 +57,12 @@ export namespace State {
export interface InvalidIban {
status: "invalid-iban",
- error: HttpResponseOk<SandboxBackend.CoreBank.AccountData>;
+ error: TalerCorebankApi.AccountData;
}
export interface UserNotFound {
- status: "error-user-not-found",
- error: HttpError<any>;
+ status: "login",
+ reason: "not-found" | "forbidden";
onRegister?: () => void;
}
}
@@ -80,7 +77,7 @@ export interface Transaction {
const viewMapping: utils.StateViewMap<State> = {
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 <http://www.gnu.org/licenses/>
*/
-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 (
- <div>Payto from server is not valid &quot;{error.data.payto_uri}&quot;</div>
+ <div>Payto from server is not valid &quot;{error.payto_uri}&quot;</div>
);
}
@@ -75,19 +74,20 @@ function MaybeBusinessButton({
onClick: () => void;
}): VNode {
const { i18n } = useTranslationContext();
- const result = useBusinessAccountDetails(account);
- if (!result.ok) return <Fragment />;
- return (
- <div class="w-full flex justify-end">
- <button
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={(e) => {
- e.preventDefault()
- onClick()
- }}
- >
- <i18n.Translate>Business Profile</i18n.Translate>
- </button>
- </div>
- );
+ return <Fragment />
+ // const result = useBusinessAccountDetails(account);
+ // if (!result.ok) return <Fragment />;
+ // return (
+ // <div class="w-full flex justify-end">
+ // <button
+ // class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ // onClick={(e) => {
+ // e.preventDefault()
+ // onClick()
+ // }}
+ // >
+ // <i18n.Translate>Business Profile</i18n.Translate>
+ // </button>
+ // </div>
+ // );
}
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 <http://www.gnu.org/licenses/>
*/
-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({
</span>
</span>
<button type="button" data-enabled={settings.showDebugInfo} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
onClick={() => {
updateSettings("showDebugInfo", !settings.showDebugInfo);
}}>
@@ -346,14 +346,6 @@ function StatusBanner(): VNode {
</div>
}
<MaybeShowDebugInfo info={n.message.debug} />
- {/* <a href="#" class="text-gray-500">
- show debug info
- </a>
- {n.message.debug &&
- <div class="mt-2 text-sm text-red-700 font-mono break-all">
- {n.message.debug}
- </div>
- } */}
</Attention>
case "info":
return <Attention type="success" title={n.message.title} onClose={() => {
@@ -411,16 +403,22 @@ function WelcomeAccount({ account }: { account: string }): VNode {
const { i18n } = useTranslationContext();
const result = useAccountDetails(account);
- if (!result.ok) return <div />
+ if (!result) {
+ return <Loading />
+ }
+ if (result instanceof TalerError) {
+ return <div />
+ }
+ if (result.type === "fail") return <div />
- const payto = parsePaytoUri(result.data.payto_uri)
+ const payto = parsePaytoUri(result.body.payto_uri)
if (!payto) return <div />
const accountNumber = !payto.isKnown ? undefined : payto.targetType === "iban" ? payto.iban : payto.targetType === "x-taler-bank" ? payto.account : undefined;
return <i18n.Translate>
Welcome, {account} {accountNumber !== undefined ?
<span>
- (<a href={result.data.payto_uri}>{accountNumber}</a> <CopyButton getContent={() => result.data.payto_uri} />)
+ (<a href={result.body.payto_uri}>{accountNumber}</a> <CopyButton getContent={() => result.body.payto_uri} />)
</span>
: <Fragment />}!
</i18n.Translate>
@@ -429,10 +427,16 @@ function WelcomeAccount({ account }: { account: string }): VNode {
function AccountBalance({ account }: { account: string }): VNode {
const result = useAccountDetails(account);
- if (!result.ok) return <div />
+ if (!result) {
+ return <Loading />
+ }
+ if (result instanceof TalerError) {
+ return <div />
+ }
+ if (result.type === "fail") return <div />
return <RenderAmount
- value={Amounts.parseOrThrow(result.data.balance.amount)}
- negative={result.data.balance.credit_debit_indicator === "debit"}
+ value={Amounts.parseOrThrow(result.body.balance.amount)}
+ negative={result.body.balance.credit_debit_indicator === "debit"}
/>
}
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx
index 95144f086..bd85cea1e 100644
--- a/packages/demobank-ui/src/pages/HomePage.tsx
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -31,12 +31,11 @@ import {
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { Loading } from "../components/Loading.js";
-import { getInitialBackendBaseURL } from "../hooks/backend.js";
+import { useBankCoreApiContext } from "../context/config.js";
import { useSettings } from "../hooks/settings.js";
import { AccountPage } from "./AccountPage/index.js";
import { LoginForm } from "./LoginForm.js";
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
-import { route } from "preact-router";
const logger = new Logger("AccountPage");
@@ -68,7 +67,6 @@ export function HomePage({
account={account}
goToConfirmOperation={goToConfirmOperation}
goToBusinessAccount={goToBusinessAccount}
- onLoadNotOk={handleNotOkResult(i18n)}
/>
);
}
@@ -82,9 +80,9 @@ export function WithdrawalOperationPage({
}): VNode {
//FIXME: libeufin sandbox should return show to create the integration api endpoint
//or return withdrawal uri from response
- const baseUrl = getInitialBackendBaseURL()
+ const { api } = useBankCoreApiContext()
const uri = stringifyWithdrawUri({
- bankIntegrationApiBaseUrl: `${baseUrl}/taler-integration`,
+ bankIntegrationApiBaseUrl: api.getIntegrationAPI().baseUrl,
withdrawalOperationId: operationId,
});
const parsedUri = parseWithdrawUri(uri);
@@ -110,76 +108,6 @@ export function WithdrawalOperationPage({
);
}
-export function handleNotOkResult(
- i18n: ReturnType<typeof useTranslationContext>["i18n"],
-): <T>(
- result:
- | HttpResponsePaginated<T, SandboxBackend.SandboxError>
- | HttpResponse<T, SandboxBackend.SandboxError>,
-) => VNode {
- return function handleNotOkResult2<T>(
- result:
- | HttpResponsePaginated<T, SandboxBackend.SandboxError | undefined>
- | HttpResponse<T, SandboxBackend.SandboxError | undefined>,
- ): VNode {
- if (result.loading) return <Loading />;
- if (!result.ok) {
- switch (result.type) {
- case ErrorType.TIMEOUT: {
- notifyError(i18n.str`Request timeout, try again later.`, undefined);
- break;
- }
- case ErrorType.CLIENT: {
- if (result.status === HttpStatusCode.Unauthorized) {
- notifyError(i18n.str`Wrong credentials`, undefined);
- return <LoginForm />;
- }
- const errorData = result.payload;
- notify({
- type: "error",
- title: i18n.str`Could not load due to a request error`,
- description: i18n.str`Request to url "${result.info.url}" returned ${result.info.status}`,
- debug: JSON.stringify(result),
- });
- break;
- }
- case ErrorType.SERVER: {
- notify({
- type: "error",
- title: i18n.str`Server returned with error`,
- description: result.payload?.error?.description as TranslatedString,
- debug: JSON.stringify(result.payload),
- });
- break;
- }
- case ErrorType.UNREADABLE: {
- notify({
- type: "error",
- title: i18n.str`Unexpected error.`,
- description: i18n.str`Response from ${result.info?.url} is unreadable, http status: ${result.status}`,
- debug: JSON.stringify(result),
- });
- break;
- }
- case ErrorType.UNEXPECTED: {
- notify({
- type: "error",
- title: i18n.str`Unexpected error.`,
- description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`,
- debug: JSON.stringify(result),
- });
- break;
- }
- default: {
- assertUnreachable(result);
- }
- }
- // route("/")
- return <div>error</div>;
- }
- return <div />;
- };
-}
export function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx
index 3ea94b899..a8167cca5 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -14,28 +14,29 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
-import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { HttpStatusCode, TalerAuthentication, TranslatedString } from "@gnu-taler/taler-util";
+import { ErrorType, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useBackendContext } from "../context/backend.js";
-import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js";
import { bankUiSettings } from "../settings.js";
import { undefinedIfEmpty } from "../utils.js";
import { doAutoFocus } from "./PaytoWireTransferForm.js";
+import { useBankCoreApiContext } from "../context/config.js";
+import { assertUnreachable } from "./HomePage.js";
/**
* Collect and submit login data.
*/
-export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
+export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forbidden", onRegister?: () => void }): VNode {
const backend = useBackendContext();
const currentUser = backend.state.status !== "loggedOut" ? backend.state.username : undefined
const [username, setUsername] = useState<string | undefined>(currentUser);
const [password, setPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext();
- const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker();
+ const { api } = useBankCoreApiContext();
/**
@@ -70,10 +71,6 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
password: !password ? i18n.str`Missing password` : undefined,
}) ?? busy;
- function saveError({ title, description, debug }: { title: TranslatedString, description?: TranslatedString, debug?: any }) {
- notifyError(title, description, debug)
- }
-
async function doLogout() {
backend.logOut()
}
@@ -81,63 +78,42 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
async function doLogin() {
if (!username || !password) return;
setBusy({})
- const result = await requestNewLoginToken(username, password);
- if (result.valid) {
- backend.logIn({ username, token: result.token });
+ const data: TalerAuthentication.TokenRequest = {
+ // 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,
+ }
+ 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 {
- const { cause } = result;
- switch (cause.type) {
- case ErrorType.CLIENT: {
- if (cause.status === HttpStatusCode.Unauthorized) {
- saveError({
- title: i18n.str`Wrong credentials for "${username}"`,
- });
- } else
- if (cause.status === HttpStatusCode.NotFound) {
- saveError({
- title: i18n.str`Account not found`,
- });
- } else {
- saveError({
- title: i18n.str`Could not load due to a request error`,
- description: i18n.str`Request to url "${cause.info.url}" returned ${cause.info.status}`,
- debug: JSON.stringify(cause.payload),
- });
- }
- break;
- }
- case ErrorType.SERVER: {
- saveError({
- title: i18n.str`Server had a problem, try again later or report.`,
- // description: cause.payload.error.description,
- debug: JSON.stringify(cause.payload),
- });
- break;
- }
- case ErrorType.TIMEOUT: {
- saveError({
- title: i18n.str`Request timeout, try again later.`,
- });
- break;
- }
- case ErrorType.UNREADABLE: {
- saveError({
- title: i18n.str`Unexpected error.`,
- description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}` as TranslatedString,
- debug: JSON.stringify(cause),
- });
- break;
- }
- default: {
- saveError({
- title: i18n.str`Unexpected error, please report.`,
- description: `Diagnostic from ${cause.info?.url} is "${cause.message}"` as TranslatedString,
- debug: JSON.stringify(cause),
- });
- break;
- }
+ 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)
}
- // backend.logOut();
}
setPassword(undefined);
setBusy(undefined)
diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts
index b347fd942..bc3555c48 100644
--- a/packages/demobank-ui/src/pages/OperationState/index.ts
+++ b/packages/demobank-ui/src/pages/OperationState/index.ts
@@ -14,8 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, AmountJson, WithdrawUriResult } from "@gnu-taler/taler-util";
-import { HttpError, utils } from "@gnu-taler/web-util/browser";
+import { AbsoluteTime, AmountJson, TalerCoreBankErrorsByMethod, TalerError, TalerErrorDetail, TranslatedString, WithdrawUriResult } from "@gnu-taler/taler-util";
+import { utils } from "@gnu-taler/web-util/browser";
import { ErrorLoading } from "../../components/ErrorLoading.js";
import { Loading } from "../../components/Loading.js";
import { useComponentState } from "./state.js";
@@ -44,7 +44,7 @@ export namespace State {
export interface LoadingError {
status: "loading-error";
- error: HttpError<SandboxBackend.SandboxError>;
+ error: TalerError;
}
/**
@@ -61,7 +61,7 @@ export namespace State {
export interface InvalidPayto {
status: "invalid-payto",
error: undefined;
- payto: string | null;
+ payto: string | undefined;
onClose: () => void;
}
export interface InvalidWithdrawal {
@@ -74,7 +74,7 @@ export namespace State {
status: "invalid-reserve",
error: undefined;
onClose: () => void;
- reserve: string | null;
+ reserve: string | undefined;
}
export interface NeedConfirmation {
status: "need-confirmation",
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts
index 4be680377..148571ec9 100644
--- a/packages/demobank-ui/src/pages/OperationState/state.ts
+++ b/packages/demobank-ui/src/pages/OperationState/state.ts
@@ -14,20 +14,26 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { Amounts, HttpStatusCode, TalerError, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
import { RequestError, notify, notifyError, notifyInfo, useTranslationContext, utils } from "@gnu-taler/web-util/browser";
import { useEffect, useState } from "preact/hooks";
-import { useAccessAPI, useAccessAnonAPI, useWithdrawalDetails } from "../../hooks/access.js";
-import { getInitialBackendBaseURL } from "../../hooks/backend.js";
+import { useBankCoreApiContext } from "../../context/config.js";
+import { useWithdrawalDetails } from "../../hooks/access.js";
+import { useBackendState } from "../../hooks/backend.js";
import { useSettings } from "../../hooks/settings.js";
import { buildRequestErrorMessage } from "../../utils.js";
import { Props, State } from "./index.js";
+import { assertUnreachable } from "../HomePage.js";
+import { mutate } from "swr";
export function useComponentState({ currency, onClose }: Props): utils.RecursiveState<State> {
const { i18n } = useTranslationContext();
const [settings, updateSettings] = useSettings()
- const { createWithdrawal } = useAccessAPI();
- const { abortWithdrawal, confirmWithdrawal } = useAccessAnonAPI();
+ const { state: credentials } = useBackendState()
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials
+ const { api } = useBankCoreApiContext()
+ // const { createWithdrawal } = useAccessAPI();
+ // const { abortWithdrawal, confirmWithdrawal } = useAccessAnonAPI();
const [busy, setBusy] = useState<Record<string, undefined>>()
const amount = settings.maxWithdrawalAmount
@@ -37,27 +43,33 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`)
try {
- const result = await createWithdrawal({
+ if (!creds) return;
+ const resp = await api.createWithdrawal(creds, {
amount: Amounts.stringify(parsedAmount),
});
- const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case "insufficient-funds": return notify({
+ type: "error",
+ title: i18n.str`The operation was rejected due to insufficient funds`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ default: assertUnreachable(resp.case)
+ }
+ }
+
+ const uri = parseWithdrawUri(resp.body.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}`);
+ i18n.str`Withdraw URI: ${resp.body.taler_withdraw_uri}`);
} else {
updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId)
}
} 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`,
@@ -76,8 +88,6 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
}
}, [settings.fastWithdrawal, amount])
- const baseUrl = getInitialBackendBaseURL()
-
if (!withdrawalOperationId) {
return {
status: "loading",
@@ -90,18 +100,24 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
async function doAbort() {
try {
setBusy({})
- await abortWithdrawal(wid);
- onClose();
+ const resp = await api.abortWithdrawalById(wid);
+ setBusy(undefined)
+ if (resp.type === "ok") {
+ onClose();
+ } else {
+ switch (resp.case) {
+ case "previously-confirmed": return notify({
+ type: "error",
+ title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ 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`,
@@ -111,28 +127,38 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
)
}
}
- setBusy(undefined)
}
async function doConfirm() {
try {
setBusy({})
- await confirmWithdrawal(wid);
- if (!settings.showWithdrawalSuccess) {
- notifyInfo(i18n.str`Wire transfer completed!`)
+ const resp = await api.confirmWithdrawalById(wid);
+ if (resp.type === "ok") {
+ mutate(() => true)//clean withdrawal state
+ if (!settings.showWithdrawalSuccess) {
+ notifyInfo(i18n.str`Wire transfer completed!`)
+ }
+ setBusy(undefined)
+ } 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`,
@@ -142,11 +168,10 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
)
}
}
- setBusy(undefined)
}
- const bankIntegrationApiBaseUrl = `${baseUrl}/taler-integration`
+
const uri = stringifyWithdrawUri({
- bankIntegrationApiBaseUrl,
+ bankIntegrationApiBaseUrl: api.getIntegrationAPI().baseUrl,
withdrawalOperationId,
});
const parsedUri = parseWithdrawUri(uri);
@@ -161,32 +186,43 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
return (): utils.RecursiveState<State> => {
const result = useWithdrawalDetails(withdrawalOperationId);
- const shouldCreateNewOperation = !result.ok && !result.loading && result.info.status === HttpStatusCode.NotFound
+ const shouldCreateNewOperation = result && !(result instanceof TalerError)
useEffect(() => {
if (shouldCreateNewOperation) {
doSilentStart()
}
}, [])
- if (!result.ok) {
- if (result.loading) {
- return {
- status: "loading",
- error: undefined
- }
- }
- if (result.info.status === HttpStatusCode.NotFound) {
- return {
- status: "loading",
- error: undefined,
- }
+ if (!result) {
+ return {
+ status: "loading",
+ error: undefined
}
+ }
+ if (result instanceof TalerError) {
return {
status: "loading-error",
error: result
}
}
- const { data } = result;
+
+ if (result.type === "fail") {
+ switch (result.case) {
+ case "not-found": {
+ return {
+ status: "aborted",
+ error: undefined,
+ onClose: async () => {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ }
+ }
+ default: assertUnreachable(result.case)
+ }
+ }
+
+ const { body: data } = result;
if (data.aborted) {
return {
status: "aborted",
@@ -247,8 +283,6 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
}
}
-
- // goToConfirmOperation(withdrawalOperationId)
return {
status: "need-confirmation",
error: undefined,
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 52dbd4ff6..7861bb0b3 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -16,9 +16,11 @@
import {
AmountJson,
+ AmountString,
Amounts,
HttpStatusCode,
Logger,
+ TalerError,
TranslatedString,
buildPayto,
parsePaytoUri,
@@ -30,17 +32,18 @@ import {
notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { h, VNode, Fragment, Ref } from "preact";
+import { Fragment, Ref, VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
-import { useAccessAPI } from "../hooks/access.js";
import {
buildRequestErrorMessage,
undefinedIfEmpty,
validateIBAN,
} from "../utils.js";
-import { useConfigState } from "../hooks/config.js";
-import { useConfigContext } from "../context/config.js";
+import { useBankCoreApiContext } from "../context/config.js";
+import { useBackendState } from "../hooks/backend.js";
+import { assertUnreachable } from "./HomePage.js";
+import { mutate } from "swr";
const logger = new Logger("PaytoWireTransferForm");
@@ -59,6 +62,8 @@ export function PaytoWireTransferForm({
}): VNode {
const [isRawPayto, setIsRawPayto] = useState(false);
// FIXME: remove this
+ const { state: credentials } = useBackendState()
+ const { api } = useBankCoreApiContext();
const [iban, setIban] = useState<string | undefined>();
const [subject, setSubject] = useState<string | undefined>();
const [amount, setAmount] = useState<string | undefined>();
@@ -95,7 +100,7 @@ export function PaytoWireTransferForm({
: undefined,
});
- const { createTransaction } = useAccessAPI();
+ // const { createTransaction } = useAccessAPI();
const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
@@ -119,36 +124,54 @@ export function PaytoWireTransferForm({
async function doSend() {
let payto_uri: string | undefined;
-
+ let sendingAmount: AmountString | undefined;
if (rawPaytoInput) {
- payto_uri = rawPaytoInput
+ const p = parsePaytoUri(rawPaytoInput)
+ if (!p) return;
+ sendingAmount = p.params.amount
+ delete p.params.amount
+ //it should have message
+ payto_uri = stringifyPaytoUri(p)
} else {
if (!iban || !subject) return;
const ibanPayto = buildPayto("iban", iban, undefined);
ibanPayto.params.message = encodeURIComponent(subject);
payto_uri = stringifyPaytoUri(ibanPayto);
+ sendingAmount = `${limit.currency}:${trimmedAmountStr}`
}
try {
- await createTransaction({
+ if (credentials.status !== "loggedIn") return;
+ const res = await api.createTransaction(credentials, {
payto_uri,
- amount: `${limit.currency}:${amount}`,
+ amount: sendingAmount,
});
+ mutate(() => true)
+ if (res.type === "fail") {
+ switch (res.case) {
+ case "invalid-input": return notify({
+ type: "error",
+ title: i18n.str`The request was invalid or the payto://-URI used unacceptable features.`,
+ description: res.detail.hint as TranslatedString,
+ debug: res.detail,
+ })
+ case "unauthorized": return notify({
+ type: "error",
+ title: i18n.str`Not enough permission to complete the operation.`,
+ description: res.detail.hint as TranslatedString,
+ debug: res.detail,
+ })
+ default: assertUnreachable(res)
+ }
+ }
onSuccess();
setAmount(undefined);
setIban(undefined);
setSubject(undefined);
rawPaytoInputSetter(undefined)
} catch (error) {
- if (error instanceof RequestError) {
- notify(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.BadRequest
- ? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
- : undefined,
- }),
- );
+ if (error instanceof TalerError) {
+ notify(buildRequestErrorMessage(i18n, error))
} else {
notifyError(
i18n.str`Operation failed, please report`,
@@ -179,11 +202,13 @@ export function PaytoWireTransferForm({
if (amount) {
setAmount(Amounts.stringifyValue(amount))
}
- const subject = parsed.params["subject"]
+ const subject = parsed.params["message"]
if (subject) {
setSubject(subject)
}
}
+ //payto://iban/DE9714548806481?amount=LOCAL%3A2&message=011Y8V8KDCPFDEKPDZTHS7KZ14GHX7BVWKRDDPZ1N75TJ90166T0
+ //payto://iban/DE9714548806481?receiver-name=Exchanger&amount=LOCAL%3A2&message=011Y8V8KDCPFDEKPDZTHS7KZ14GHX7BVWKRDDPZ1N75TJ90166T0
setIsRawPayto(false)
}} />
<span class="flex flex-1">
@@ -298,7 +323,7 @@ export function PaytoWireTransferForm({
/>
<ShowInputErrorLabel
message={errorsWire?.amount}
- isDirty={subject !== undefined}
+ isDirty={trimmedAmountStr !== undefined}
/>
<p class="mt-2 text-sm text-gray-500" >amount to transfer</p>
</div>
@@ -394,7 +419,7 @@ export function InputAmount(
},
ref: Ref<HTMLInputElement>,
): VNode {
- const cfg = useConfigContext()
+ const { config } = useBankCoreApiContext()
return (
<div class="mt-2">
<div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
@@ -418,8 +443,8 @@ export function InputAmount(
if (!onChange) return;
const l = e.currentTarget.value.length
const sep_pos = e.currentTarget.value.indexOf(FRAC_SEPARATOR)
- if (sep_pos !== -1 && l - sep_pos - 1 > cfg.currency_fraction_limit) {
- e.currentTarget.value = e.currentTarget.value.substring(0, sep_pos + cfg.currency_fraction_limit + 1)
+ if (sep_pos !== -1 && l - sep_pos - 1 > config.currency.num_fractional_input_digits) {
+ e.currentTarget.value = e.currentTarget.value.substring(0, sep_pos + config.currency.num_fractional_input_digits + 1)
}
onChange(e.currentTarget.value);
}}
@@ -431,11 +456,11 @@ export function InputAmount(
}
export function RenderAmount({ value, negative }: { value: AmountJson, negative?: boolean }): VNode {
- const cfg = useConfigContext()
+ const { config } = useBankCoreApiContext()
const str = Amounts.stringifyValue(value)
const sep_pos = str.indexOf(FRAC_SEPARATOR)
- if (sep_pos !== -1 && str.length - sep_pos - 1 > cfg.currency_fraction_digits) {
- const limit = sep_pos + cfg.currency_fraction_digits + 1
+ if (sep_pos !== -1 && str.length - sep_pos - 1 > config.currency.num_fractional_normal_digits) {
+ const limit = sep_pos + config.currency.num_fractional_normal_digits + 1
const normal = str.substring(0, limit)
const small = str.substring(limit)
return <span class="whitespace-nowrap">
diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
index 680368919..d33353180 100644
--- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
+++ b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
@@ -14,35 +14,36 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Logger } from "@gnu-taler/taler-util";
+import { Logger, TalerError } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { Loading } from "../components/Loading.js";
import { Transactions } from "../components/Transactions/index.js";
import { usePublicAccounts } from "../hooks/access.js";
-import { handleNotOkResult } from "./HomePage.js";
-import { Loading } from "../components/Loading.js";
const logger = new Logger("PublicHistoriesPage");
-interface Props {}
+interface Props { }
/**
* Show histories of public accounts.
*/
-export function PublicHistoriesPage({}: Props): VNode {
+export function PublicHistoriesPage({ }: Props): VNode {
const { i18n } = useTranslationContext();
const result = usePublicAccounts();
+ const firstAccount = result && !(result instanceof TalerError) && result.data.public_accounts.length > 0
+ ? result.data.public_accounts[0].account_name
+ : undefined;
- const [showAccount, setShowAccount] = useState(
- result.ok && result.data.public_accounts.length > 0
- ? result.data.public_accounts[0].account_name
- : undefined,
- );
+ const [showAccount, setShowAccount] = useState(firstAccount);
- if (!result.ok) {
- return handleNotOkResult(i18n)(result);
+ if (!result) {
+ return <Loading />
+ }
+ if (result instanceof TalerError) {
+ return <Loading />
}
const { data } = result;
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index e07525ab4..109993aae 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -17,6 +17,7 @@
import {
HttpStatusCode,
stringifyWithdrawUri,
+ TalerError,
TranslatedString,
WithdrawUriResult,
} from "@gnu-taler/taler-util";
@@ -29,8 +30,9 @@ import {
import { Fragment, h, VNode } from "preact";
import { useEffect } from "preact/hooks";
import { QR } from "../components/QR.js";
-import { useAccessAnonAPI } from "../hooks/access.js";
import { buildRequestErrorMessage } from "../utils.js";
+import { useBankCoreApiContext } from "../context/config.js";
+import { assertUnreachable } from "./HomePage.js";
export function QrCodeSection({
withdrawUri,
@@ -50,22 +52,30 @@ export function QrCodeSection({
}, []);
const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
- const { abortWithdrawal } = useAccessAnonAPI();
+ const { api } = useBankCoreApiContext()
async function doAbort() {
try {
- await abortWithdrawal(withdrawUri.withdrawalOperationId);
- onAborted();
+ const result = await api.abortWithdrawalById(withdrawUri.withdrawalOperationId);
+ if (result.type === "ok") {
+ onAborted();
+ } else {
+ switch (result.case) {
+ case "previously-confirmed": {
+ notify({
+ type: "info",
+ title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`
+ })
+ break;
+ }
+ default: {
+ assertUnreachable(result.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`,
@@ -120,13 +130,13 @@ export function QrCodeSection({
</div>
</div>
<div class="flex items-center justify-center gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
- <button type="button"
- // class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md px-3 py-2 text-sm font-semibold text-black shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
- class="text-sm font-semibold leading-6 text-gray-900"
- onClick={doAbort}
- >
- Cancel
- </button>
+ <button type="button"
+ // class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md px-3 py-2 text-sm font-semibold text-black shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={doAbort}
+ >
+ Cancel
+ </button>
</div>
</div>
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 <http://www.gnu.org/licenses/>
*/
-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<string | undefined>();
const [email, setEmail] = useState<string | undefined>();
const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
- 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: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => 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<TalerCorebankApi.AccountData | undefined>();
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return <div>account not found</div>;
+ const result = useAccountDetails(account);
+ if (!result) {
+ return <Loading />
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoading error={result} />
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case "not-found": return <LoginForm reason="not-found" />
+ case "unauthorized": return <LoginForm reason="forbidden" />
+ 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({
}
</h2>
<div class="mt-4">
- <div class="flex items-center justify-between">
- <span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
- <i18n.Translate>change the account details</i18n.Translate>
- </span>
- </span>
- <button type="button" data-enabled={!update} class="bg-indigo-600 data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full ring-2 border-gray-600 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
- onClick={() => {
- setUpdate(!update)
- }}>
- <span aria-hidden="true" data-enabled={!update} class="translate-x-5 data-[enabled=true]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
- </button>
- </div>
- </div>
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <i18n.Translate>change the account details</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={!update} class="bg-indigo-600 data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full ring-2 border-gray-600 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
+ onClick={() => {
+ setUpdate(!update)
+ }}>
+ <span aria-hidden="true" data-enabled={!update} class="translate-x-5 data-[enabled=true]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ </button>
+ </div>
+ </div>
</div>
<AccountForm
- template={result.data}
+ template={result.body}
purpose={update ? "update" : "show"}
onChange={(a) => 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: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => 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<string | undefined>();
const [repeat, setRepeat] = useState<string | undefined>();
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return <div>account not found</div>;
- }
- 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<string | undefined>(`${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<string | undefined>();
const answer = parseInt(captchaAnswer ?? "", 10);
const [busy, setBusy] = useState<Record<string, undefined>>()
@@ -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 <Loading />;
- }
- if (result.type === ErrorType.CLIENT && result.status === HttpStatusCode.NotFound) {
- return <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
- <div>
- <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100 ">
- <svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
- </svg>
- </div>
-
- <div class="mt-3 text-center sm:mt-5">
- <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
- <i18n.Translate>Operation not found</i18n.Translate>
- </h3>
- <div class="mt-2">
- <p class="text-sm text-gray-500">
- <i18n.Translate>
- This operation is not known by the server. The operation id is wrong or the
- server deleted the operation information before reaching here.
- </i18n.Translate>
- </p>
- </div>
- </div>
- </div>
- <div class="mt-5 sm:mt-6">
- <button type="button"
- class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={async (e) => {
- e.preventDefault();
- onClose()
- }}>
- <i18n.Translate>Cotinue to dashboard</i18n.Translate>
- </button>
- </div>
- </div>
+ if (!result) {
+ return <Loading />
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoading error={result} />
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case "not-found": return <OperationNotFound onClose={onClose} />
+ default: assertUnreachable(result.case)
}
- return handleNotOkResult(i18n)(result);
}
- const { data } = result;
+
+ const { body: data } = result;
if (data.aborted) {
return <section id="main" class="content">
@@ -194,3 +166,41 @@ export function WithdrawalQRCode({
/>
);
}
+
+
+function OperationNotFound({ onClose }: { onClose: () => void }): VNode {
+ const { i18n } = useTranslationContext();
+ return <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
+ <div>
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100 ">
+ <svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
+ </svg>
+ </div>
+
+ <div class="mt-3 text-center sm:mt-5">
+ <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
+ <i18n.Translate>Operation not found</i18n.Translate>
+ </h3>
+ <div class="mt-2">
+ <p class="text-sm text-gray-500">
+ <i18n.Translate>
+ This operation is not known by the server. The operation id is wrong or the
+ server deleted the operation information before reaching here.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="mt-5 sm:mt-6">
+ <button type="button"
+ class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ onClick={async (e) => {
+ e.preventDefault();
+ onClose()
+ }}>
+ <i18n.Translate>Cotinue to dashboard</i18n.Translate>
+ </button>
+ </div>
+ </div>
+} \ 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 <Loading />
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoading error={result} />
}
- const { data } = result;
+ if (result.type === "fail") {
+ switch (result.case) {
+ case "unauthorized": return <LoginForm reason="forbidden" onRegister={onRegister} />
+ case "not-found": return <LoginForm reason="not-found" onRegister={onRegister} />
+ 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<RecursivePartial<typeof initial>>({
- 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({
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
+ {/* <div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="username"
@@ -127,7 +128,7 @@ export function AccountForm({
<p class="mt-2 text-sm text-gray-500" >
<i18n.Translate>account identification in the bank</i18n.Translate>
</p>
- </div>
+ </div> */}
<div class="sm:col-span-5">
<label
@@ -178,7 +179,7 @@ export function AccountForm({
name="internal-iban"
id="internal-iban"
disabled={true}
- value={form.iban ?? ""}
+ value={form.payto_uri ?? ""}
/>
</div>
<p class="mt-2 text-sm text-gray-500" >
@@ -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"
/>
<ShowInputErrorLabel
message={errors?.contact_data?.email}
- isDirty={form.contact_data.email !== undefined}
+ isDirty={form.contact_data?.email !== undefined}
/>
</div>
</div>
@@ -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"
/>
<ShowInputErrorLabel
message={errors?.contact_data?.phone}
- isDirty={form.contact_data.phone !== undefined}
+ isDirty={form.contact_data?.phone !== undefined}
/>
</div>
</div>
@@ -259,21 +264,21 @@ export function AccountForm({
<div class="mt-2">
<input
type="text"
- data-error={!!errors?.cashout_address && form.cashout_address !== undefined}
+ data-error={!!errors?.cashout_payto_uri && form.cashout_payto_uri !== undefined}
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="cashout"
id="cashout"
disabled={purpose === "show"}
- value={form.cashout_address ?? ""}
+ value={form.cashout_payto_uri ?? ""}
onChange={(e) => {
- form.cashout_address = e.currentTarget.value;
+ form.cashout_payto_uri = e.currentTarget.value;
updateForm(structuredClone(form));
}}
autocomplete="off"
/>
<ShowInputErrorLabel
- message={errors?.cashout_address}
- isDirty={form.cashout_address !== undefined}
+ message={errors?.cashout_payto_uri}
+ isDirty={form.cashout_payto_uri !== undefined}
/>
</div>
<p class="mt-2 text-sm text-gray-500" >
@@ -289,26 +294,27 @@ export function AccountForm({
}
function initializeFromTemplate(
- account: SandboxBackend.Circuit.CircuitAccountData | undefined,
-): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
+ account: TalerCorebankApi.AccountData | undefined,
+): WithIntermediate<TalerCorebankApi.AccountData> {
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<SandboxBackend.Circuit.CircuitAccountData> =
+ const initial: PartialButDefined<TalerCorebankApi.AccountData> =
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 <div />;
- if (!result.ok) {
- return handleNotOkResult(i18n)(result);
+ if (!result) {
+ return <Loading />
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoading error={result} />
+ }
+ if (result.data.type === "fail") {
+ switch (result.data.case) {
+ case "unauthorized": return <div>un auth</div>
+ default: assertUnreachable(result.data.case)
+ }
}
- const { customers } = result.data;
+ const { accounts } = result.data.body;
return <div class="px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
@@ -45,7 +55,7 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
- {!customers.length ? (
+ {!accounts.length ? (
<div></div>
) : (
<table class="min-w-full divide-y divide-gray-300">
@@ -60,7 +70,7 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
- {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 <ShowCashoutDetails
id={action.account}
- onLoadNotOk={handleNotOkResult(i18n)}
onCancel={() => {
setAction(undefined);
}}
@@ -74,7 +72,6 @@ export function AdminHome({ onRegister }: Props): VNode {
)
case "update-password": return <UpdateAccountPassword
account={action.account}
- onLoadNotOk={handleNotOkResult(i18n)}
onUpdateSuccess={() => {
notifyInfo(i18n.str`Password changed`);
setAction(undefined);
@@ -85,7 +82,6 @@ export function AdminHome({ onRegister }: Props): VNode {
/>
case "remove-account": return <RemoveAccount
account={action.account}
- onLoadNotOk={handleNotOkResult(i18n)}
onUpdateSuccess={() => {
notifyInfo(i18n.str`Account removed`);
setAction(undefined);
@@ -96,7 +92,6 @@ export function AdminHome({ onRegister }: Props): VNode {
/>
case "show-details": return <ShowAccountDetails
account={action.account}
- onLoadNotOk={handleNotOkResult(i18n)}
onChangePassword={() => {
setAction({
type: "update-password",
@@ -137,12 +132,12 @@ export function AdminHome({ onRegister }: Props): VNode {
}}
account={undefined}
onAction={(type, account) => setAction({ account, type })}
-
+
/>
<AdminAccount onRegister={onRegister} />
- <Transactions account="admin"/>
+ <Transactions account="admin" />
</Fragment>
);
} \ 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: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => 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<string | undefined>()
- const { deleteAccount } = useAdminAccountAPI();
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return <div>account not found</div>;
+ const { state } = useBackendState();
+ const token = state.status !== "loggedIn" ? undefined : state.token
+ const { api } = useBankCoreApiContext()
+
+ if (!result) {
+ return <Loading />
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoading error={result} />
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case "unauthorized": return <LoginForm reason="forbidden" />
+ case "not-found": return <LoginForm reason="not-found" />
+ 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 <div>there was an error reading the balance</div>;
}
@@ -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 (
<CreateCashout
account={account}
- onLoadNotOk={handleNotOkResult(i18n)}
onCancel={() => {
setNewcashout(false);
}}
@@ -91,7 +89,6 @@ export function BusinessAccount({
return (
<ShowCashoutDetails
id={showCashoutDetails}
- onLoadNotOk={handleNotOkResult(i18n)}
onCancel={() => {
setShowCashoutDetails(undefined);
}}
@@ -102,7 +99,6 @@ export function BusinessAccount({
return (
<UpdateAccountPassword
account={account}
- onLoadNotOk={handleNotOkResult(i18n)}
onUpdateSuccess={() => {
notifyInfo(i18n.str`Password changed`);
setUpdatePassword(false);
@@ -117,7 +113,6 @@ export function BusinessAccount({
<div>
<ShowAccountDetails
account={account}
- onLoadNotOk={handleNotOkResult(i18n)}
onUpdateSuccess={() => {
notifyInfo(i18n.str`Account updated`);
}}
@@ -158,11 +153,6 @@ interface PropsCashout {
account: string;
onComplete: (id: string) => void;
onCancel: () => void;
- onLoadNotOk: <T>(
- error:
- | HttpResponsePaginated<T, SandboxBackend.SandboxError>
- | HttpResponse<T, SandboxBackend.SandboxError>,
- ) => VNode;
}
type FormType = {
@@ -175,88 +165,78 @@ type ErrorFrom<T> = {
[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<Partial<FormType>>({ 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 <Loading />
+ }
+ if (resultAccount instanceof TalerError) {
+ return <ErrorLoading error={resultAccount} />
+ }
+ if (resultRatios instanceof TalerError) {
+ return <ErrorLoading error={resultRatios} />
+ }
+ if (resultAccount.type === "fail") {
+ switch (resultAccount.case) {
+ case "unauthorized": return <LoginForm reason="forbidden" />
+ case "not-found": return <LoginForm reason="not-found" />
+ default: assertUnreachable(resultAccount)
+ }
+ }
+
+ if (resultRatios.type === "fail") {
+ switch (resultRatios.case) {
+ case "not-supported": return <div>cashout operations are not supported</div>
+ default: assertUnreachable(resultRatios.case)
+ }
+ }
+ if (!config.fiat_currency) {
+ return <div>cashout operations are not supported</div>
+ }
- 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 <div>error rate</div>;
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({
<label for="balance-now">{i18n.str`Balance now`}</label>
<InputAmount
name="banace-now"
- currency={balance.currency}
- value={Amounts.stringifyValue(balance)}
+ currency={account.balance.currency}
+ value={Amounts.stringifyValue(account.balance)}
/>
</fieldset>
<fieldset>
@@ -384,7 +366,7 @@ function CreateCashout({
>{i18n.str`Total cost`}</label>
<InputAmount
name="total-cost"
- currency={balance.currency}
+ currency={account.balance.currency}
value={Amounts.stringifyValue(calc.debit)}
/>
</fieldset>
@@ -392,7 +374,7 @@ function CreateCashout({
<label for="balance-after">{i18n.str`Balance after`}</label>
<InputAmount
name="balance-after"
- currency={balance.currency}
+ currency={account.balance.currency}
value={balanceAfter ? Amounts.stringifyValue(balanceAfter) : ""}
/>
</fieldset>{" "}
@@ -402,7 +384,7 @@ function CreateCashout({
<label for="amount-conversiojn">{i18n.str`Amount after conversion`}</label>
<InputAmount
name="amount-conversion"
- currency={fiatCurrency}
+ currency={config.fiat_currency.name}
value={Amounts.stringifyValue(calc.beforeFee)}
/>
</fieldset>
@@ -411,7 +393,7 @@ function CreateCashout({
<label form="cashout-fee">{i18n.str`Cashout fee`}</label>
<InputAmount
name="cashout-fee"
- currency={fiatCurrency}
+ currency={config.fiat_currency.name}
value={Amounts.stringifyValue(sellFee)}
/>
</fieldset>
@@ -423,7 +405,7 @@ function CreateCashout({
>{i18n.str`Total cashout transfer`}</label>
<InputAmount
name="total"
- currency={fiatCurrency}
+ currency={config.fiat_currency.name}
value={Amounts.stringifyValue(calc.credit)}
/>
</fieldset>
@@ -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: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => 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<string | undefined>(undefined);
- if (!result.ok) return onLoadNotOk(result);
+
+ if (!result) {
+ return <Loading />
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoading error={result} />
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case "already-aborted": return <div>this cashout is already aborted</div>
+ 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 (
<div>
<h1>Cashout details {id}</h1>
@@ -578,43 +590,47 @@ export function ShowCashoutDetails({
<label>
<i18n.Translate>Subject</i18n.Translate>
</label>
- <input readOnly value={result.data.subject} />
+ <input readOnly value={result.body.subject} />
</fieldset>
<fieldset>
<label>
<i18n.Translate>Created</i18n.Translate>
</label>
- <input readOnly value={result.data.creation_time ?? ""} />
+ <input readOnly value={result.body.creation_time.t_s === "never" ? i18n.str`never` : format(result.body.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")} />
</fieldset>
<fieldset>
<label>
<i18n.Translate>Confirmed</i18n.Translate>
</label>
- <input readOnly value={result.data.confirmation_time ?? ""} />
+ <input readOnly value={result.body.confirmation_time === undefined ? "-" :
+ (result.body.confirmation_time.t_s === "never" ?
+ i18n.str`never` :
+ format(result.body.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss"))
+ } />
</fieldset>
<fieldset>
<label>
<i18n.Translate>Debited</i18n.Translate>
</label>
- <input readOnly value={result.data.amount_debit} />
+ <input readOnly value={result.body.amount_debit} />
</fieldset>
<fieldset>
<label>
<i18n.Translate>Credit</i18n.Translate>
</label>
- <input readOnly value={result.data.amount_credit} />
+ <input readOnly value={result.body.amount_credit} />
</fieldset>
<fieldset>
<label>
<i18n.Translate>Status</i18n.Translate>
</label>
- <input readOnly value={result.data.status} />
+ <input readOnly value={result.body.status} />
</fieldset>
<fieldset>
<label>
<i18n.Translate>Destination</i18n.Translate>
</label>
- <input readOnly value={result.data.cashout_address} />
+ <input readOnly value={result.body.credit_payto_uri} />
</fieldset>
{isPending ? (
<fieldset>
@@ -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 <http://www.gnu.org/licenses/>
*/
-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<T> = {
export type WithIntermediate<Type extends object> = {
[prop in keyof Type]: Type[prop] extends object
- ? WithIntermediate<Type[prop]>
- : Type[prop] | undefined;
+ ? WithIntermediate<Type[prop]>
+ : Type[prop] | undefined;
};
export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
- ? RecursivePartial<U>[]
- : T[P] extends object
- ? RecursivePartial<T[P]>
- : T[P];
+ ? RecursivePartial<U>[]
+ : T[P] extends object
+ ? RecursivePartial<T[P]>
+ : T[P];
};
export enum TanChannel {
@@ -94,59 +94,61 @@ export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
export function buildRequestErrorMessage(
i18n: ReturnType<typeof useTranslationContext>["i18n"],
- cause: HttpError<SandboxBackend.SandboxError>,
- 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;
}