diff options
Diffstat (limited to 'packages/demobank-ui/src/pages')
24 files changed, 1043 insertions, 851 deletions
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 "{error.data.payto_uri}"</div> + <div>Payto from server is not valid "{error.payto_uri}"</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`, |