diff options
author | Sebastian <sebasjm@gmail.com> | 2023-04-07 17:30:01 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-04-07 17:30:01 -0300 |
commit | a3aa7d95d09c83794067c47df4a455c0e3f21806 (patch) | |
tree | 00837196305227fe6f7cbc7289f96b256d5de089 | |
parent | 43ae414a55b84b1125c5e4377c6d485ca6c748e2 (diff) |
anon withdrawal confirmation, and fix error with infinity loop
19 files changed, 596 insertions, 565 deletions
diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx index e024be41b..ef535bb9f 100644 --- a/packages/demobank-ui/src/components/app.tsx +++ b/packages/demobank-ui/src/components/app.tsx @@ -48,19 +48,17 @@ const WITH_LOCAL_STORAGE_CACHE = false; const App: FunctionalComponent = () => { return ( <TranslationProvider source={strings}> - <PageStateProvider> - <BackendStateProvider> - <SWRConfig - value={{ - provider: WITH_LOCAL_STORAGE_CACHE - ? localStorageProvider - : undefined, - }} - > - <Routing /> - </SWRConfig> - </BackendStateProvider> - </PageStateProvider> + <BackendStateProvider> + <SWRConfig + value={{ + provider: WITH_LOCAL_STORAGE_CACHE + ? localStorageProvider + : undefined, + }} + > + <Routing /> + </SWRConfig> + </BackendStateProvider> </TranslationProvider> ); }; diff --git a/packages/demobank-ui/src/context/pageState.ts b/packages/demobank-ui/src/context/pageState.ts index 247297c7b..074fbcafc 100644 --- a/packages/demobank-ui/src/context/pageState.ts +++ b/packages/demobank-ui/src/context/pageState.ts @@ -29,9 +29,7 @@ export type Type = { pageStateSetter: StateUpdater<PageStateType>; }; const initial: Type = { - pageState: { - withdrawalInProgress: false, - }, + pageState: {}, pageStateSetter: () => { null; }, @@ -57,9 +55,7 @@ export const PageStateProvider = ({ * Wrapper providing defaults. */ function usePageState( - state: PageStateType = { - withdrawalInProgress: false, - }, + state: PageStateType = {}, ): [PageStateType, StateUpdater<PageStateType>] { const ret = useNotNullLocalStorage("page-state", JSON.stringify(state)); const retObj: PageStateType = JSON.parse(ret[0]); @@ -100,14 +96,18 @@ export type ErrorMessage = { * Track page state. */ export interface PageStateType { - error?: ErrorMessage; - info?: TranslatedString; - - withdrawalInProgress: boolean; - talerWithdrawUri?: string; - /** - * Not strictly a presentational value, could - * be moved in a future "withdrawal state" object. - */ - withdrawalId?: string; + currentWithdrawalOperationId?: string; +} + +export interface ObservedStateType { + error: ErrorMessage | undefined; + info: TranslatedString | undefined; +} +export const errorListeners: Array<(error: ErrorMessage) => void> = []; +export const infoListeners: Array<(info: TranslatedString) => void> = []; +export function notifyError(error: ErrorMessage) { + errorListeners.forEach((cb) => cb(error)); +} +export function notifyInfo(info: TranslatedString) { + infoListeners.forEach((cb) => cb(info)); } diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts index ee8566efe..546d59a84 100644 --- a/packages/demobank-ui/src/hooks/access.ts +++ b/packages/demobank-ui/src/hooks/access.ts @@ -59,30 +59,6 @@ export function useAccessAPI(): AccessAPI { ); return res; }; - const abortWithdrawal = async (id: string): Promise<HttpResponseOk<void>> => { - const res = await request<void>( - `access-api/accounts/${account}/withdrawals/${id}/abort`, - { - method: "POST", - contentType: "json", - }, - ); - await mutateAll(/.*accounts\/.*\/withdrawals\/.*/); - return res; - }; - const confirmWithdrawal = async ( - id: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>( - `access-api/accounts/${account}/withdrawals/${id}/confirm`, - { - method: "POST", - contentType: "json", - }, - ); - await mutateAll(/.*accounts\/.*\/withdrawals\/.*/); - return res; - }; const createTransaction = async ( data: SandboxBackend.Access.CreateBankAccountTransactionCreate, ): Promise<HttpResponseOk<void>> => { @@ -107,14 +83,41 @@ export function useAccessAPI(): AccessAPI { }; return { - abortWithdrawal, - confirmWithdrawal, createWithdrawal, createTransaction, deleteAccount, }; } +export function useAccessAnonAPI(): AccessAnonAPI { + const mutateAll = useMatchMutate(); + const { request } = useAuthenticatedBackend(); + + const abortWithdrawal = async (id: string): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`access-api/withdrawals/${id}/abort`, { + method: "POST", + contentType: "json", + }); + await mutateAll(/.*withdrawals\/.*/); + return res; + }; + const confirmWithdrawal = async ( + id: string, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`access-api/withdrawals/${id}/confirm`, { + method: "POST", + contentType: "json", + }); + await mutateAll(/.*withdrawals\/.*/); + return res; + }; + + return { + abortWithdrawal, + confirmWithdrawal, + }; +} + export function useTestingAPI(): TestingAPI { const mutateAll = useMatchMutate(); const { request: noAuthRequest } = usePublicBackend(); @@ -145,13 +148,15 @@ export interface AccessAPI { ) => Promise< HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse> >; - abortWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>; - confirmWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>; createTransaction: ( data: SandboxBackend.Access.CreateBankAccountTransactionCreate, ) => Promise<HttpResponseOk<void>>; deleteAccount: () => Promise<HttpResponseOk<void>>; } +export interface AccessAnonAPI { + abortWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>; + confirmWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>; +} export interface InstanceTemplateFilter { //FIXME: add filter to the template list @@ -210,7 +215,6 @@ export function useAccountDetails( // FIXME: should poll export function useWithdrawalDetails( - account: string, wid: string, ): HttpResponse< SandboxBackend.Access.BankAccountGetWithdrawalResponse, @@ -221,7 +225,7 @@ export function useWithdrawalDetails( const { data, error } = useSWR< HttpResponseOk<SandboxBackend.Access.BankAccountGetWithdrawalResponse>, RequestError<SandboxBackend.SandboxError> - >([`access-api/accounts/${account}/withdrawals/${wid}`], fetcher, { + >([`access-api/withdrawals/${wid}`], fetcher, { refreshInterval: 1000, refreshWhenHidden: false, revalidateOnFocus: false, diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index 3fe744874..e0b8d83ef 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -17,6 +17,7 @@ import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; import { ErrorType, + HttpError, RequestError, useLocalStorage, } from "@gnu-taler/web-util/lib/index.browser"; @@ -193,6 +194,22 @@ export function usePublicBackend(): useBackendType { }; } +type CheckResult = ValidResult | RequestInvalidResult | InvalidationResult; + +interface ValidResult { + valid: true; +} +interface RequestInvalidResult { + valid: false; + requestError: true; + cause: RequestError<any>["cause"]; +} +interface InvalidationResult { + valid: false; + requestError: false; + error: unknown; +} + export function useCredentialsChecker() { const { request } = useApiContext(); const baseUrl = getInitialBackendBaseURL(); @@ -201,10 +218,7 @@ export function useCredentialsChecker() { return async function testLogin( username: string, password: string, - ): Promise<{ - valid: boolean; - cause?: ErrorType; - }> { + ): Promise<CheckResult> { try { await request(baseUrl, `access-api/accounts/${username}/`, { basicAuth: { username, password }, @@ -213,9 +227,9 @@ export function useCredentialsChecker() { return { valid: true }; } catch (error) { if (error instanceof RequestError) { - return { valid: false, cause: error.cause.type }; + return { valid: false, requestError: true, cause: error.cause }; } - return { valid: false, cause: ErrorType.UNEXPECTED }; + return { valid: false, requestError: false, error }; } }; } diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx b/packages/demobank-ui/src/pages/AccountPage.tsx index c6ec7c88e..bab8cca16 100644 --- a/packages/demobank-ui/src/pages/AccountPage.tsx +++ b/packages/demobank-ui/src/pages/AccountPage.tsx @@ -14,15 +14,21 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util"; import { + ErrorType, HttpResponsePaginated, useTranslationContext, } from "@gnu-taler/web-util/lib/index.browser"; import { Fragment, h, VNode } from "preact"; +import { Loading } from "../components/Loading.js"; import { Transactions } from "../components/Transactions/index.js"; +import { PageStateType, notifyError } from "../context/pageState.js"; import { useAccountDetails } from "../hooks/access.js"; +import { LoginForm } from "./LoginForm.js"; import { PaymentOptions } from "./PaymentOptions.js"; +import { StateUpdater } from "preact/hooks"; +import { useBackendContext } from "../context/backend.js"; interface Props { account: string; @@ -35,9 +41,21 @@ interface Props { */ export function AccountPage({ account, onLoadNotOk }: Props): VNode { const result = useAccountDetails(account); + const backend = useBackendContext(); const { i18n } = useTranslationContext(); if (!result.ok) { + if (result.loading || result.type === ErrorType.TIMEOUT) { + return onLoadNotOk(result); + } + //logout if there is any error, not if loading + backend.logOut(); + if (result.status === HttpStatusCode.NotFound) { + notifyError({ + title: i18n.str`Username or account label "${account}" not found`, + }); + return <LoginForm />; + } return onLoadNotOk(result); } diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx index 92464a43e..b867d0103 100644 --- a/packages/demobank-ui/src/pages/AdminPage.tsx +++ b/packages/demobank-ui/src/pages/AdminPage.tsx @@ -14,13 +14,9 @@ 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, - parsePaytoUri, - TranslatedString, -} from "@gnu-taler/taler-util"; -import { + ErrorType, HttpResponsePaginated, RequestError, useTranslationContext, @@ -29,11 +25,7 @@ import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { Cashouts } from "../components/Cashouts/index.js"; import { useBackendContext } from "../context/backend.js"; -import { - ErrorMessage, - PageStateType, - usePageContext, -} from "../context/pageState.js"; +import { ErrorMessage, notifyInfo } from "../context/pageState.js"; import { useAccountDetails } from "../hooks/access.js"; import { useAdminAccountAPI, @@ -50,6 +42,7 @@ import { } from "../utils.js"; import { ErrorBannerFloat } from "./BankFrame.js"; import { ShowCashoutDetails } from "./BusinessAccount.js"; +import { handleNotOkResult } from "./HomePage.js"; import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; @@ -69,14 +62,12 @@ function randomPassword(): string { } interface Props { - onLoadNotOk: <T>( - error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, - ) => VNode; + onRegister: () => void; } /** * Query account information and show QR code if there is pending withdrawal */ -export function AdminPage({ onLoadNotOk }: Props): VNode { +export function AdminPage({ onRegister }: Props): VNode { const [account, setAccount] = useState<string | undefined>(); const [showDetails, setShowDetails] = useState<string | undefined>(); const [showCashouts, setShowCashouts] = useState<string | undefined>(); @@ -87,24 +78,13 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { >(); const [createAccount, setCreateAccount] = useState(false); - const { pageStateSetter } = usePageContext(); - - function showInfoMessage(info: TranslatedString): void { - pageStateSetter((prev) => ({ - ...prev, - info, - })); - } - function saveError(error: PageStateType["error"]): void { - pageStateSetter((prev) => ({ ...prev, error })); - } const result = useBusinessAccounts({ account }); const { i18n } = useTranslationContext(); if (result.loading) return <div />; if (!result.ok) { - return onLoadNotOk(result); + return handleNotOkResult(i18n, onRegister)(result); } const { customers } = result.data; @@ -113,7 +93,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { return ( <ShowCashoutDetails id={showCashoutDetails} - onLoadNotOk={onLoadNotOk} + onLoadNotOk={handleNotOkResult(i18n, onRegister)} onCancel={() => { setShowCashoutDetails(undefined); }} @@ -155,13 +135,13 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { return ( <ShowAccountDetails account={showDetails} - onLoadNotOk={onLoadNotOk} + onLoadNotOk={handleNotOkResult(i18n, onRegister)} onChangePassword={() => { setUpdatePassword(showDetails); setShowDetails(undefined); }} onUpdateSuccess={() => { - showInfoMessage(i18n.str`Account updated`); + notifyInfo(i18n.str`Account updated`); setShowDetails(undefined); }} onClear={() => { @@ -174,9 +154,9 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { return ( <RemoveAccount account={removeAccount} - onLoadNotOk={onLoadNotOk} + onLoadNotOk={handleNotOkResult(i18n, onRegister)} onUpdateSuccess={() => { - showInfoMessage(i18n.str`Account removed`); + notifyInfo(i18n.str`Account removed`); setRemoveAccount(undefined); }} onClear={() => { @@ -189,9 +169,9 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { return ( <UpdateAccountPassword account={updatePassword} - onLoadNotOk={onLoadNotOk} + onLoadNotOk={handleNotOkResult(i18n, onRegister)} onUpdateSuccess={() => { - showInfoMessage(i18n.str`Password changed`); + notifyInfo(i18n.str`Password changed`); setUpdatePassword(undefined); }} onClear={() => { @@ -205,7 +185,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { <CreateNewAccount onClose={() => setCreateAccount(false)} onCreateSuccess={(password) => { - showInfoMessage( + notifyInfo( i18n.str`Account created with password "${password}". The user must change the password on the next login.`, ); setCreateAccount(false); @@ -214,59 +194,6 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { ); } - function AdminAccount(): VNode { - const r = useBackendContext(); - const account = r.state.status === "loggedIn" ? r.state.username : "admin"; - const result = useAccountDetails(account); - - if (!result.ok) { - return onLoadNotOk(result); - } - const { data } = result; - const balance = Amounts.parseOrThrow(data.balance.amount); - const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold); - const balanceIsDebit = - result.data.balance.credit_debit_indicator == "debit"; - const limit = balanceIsDebit - ? Amounts.sub(debitThreshold, balance).amount - : Amounts.add(balance, debitThreshold).amount; - if (!balance) return <Fragment />; - return ( - <Fragment> - <section id="assets"> - <div class="asset-summary"> - <h2>{i18n.str`Bank account balance`}</h2> - {!balance ? ( - <div class="large-amount" style={{ color: "gray" }}> - Waiting server response... - </div> - ) : ( - <div class="large-amount amount"> - {balanceIsDebit ? <b>-</b> : null} - <span class="value">{`${Amounts.stringifyValue( - balance, - )}`}</span> - - <span class="currency">{`${balance.currency}`}</span> - </div> - )} - </div> - </section> - <PaytoWireTransferForm - focus - limit={limit} - onSuccess={() => { - pageStateSetter((prevState: PageStateType) => ({ - ...prevState, - info: i18n.str`Wire transfer created!`, - })); - }} - onError={saveError} - /> - </Fragment> - ); - } - return ( <Fragment> <div> @@ -293,7 +220,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { </div> </p> - <AdminAccount /> + <AdminAccount onRegister={onRegister} /> <section id="main" style={{ width: 600, marginLeft: "auto", marginRight: "auto" }} @@ -393,6 +320,53 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { ); } +function AdminAccount({ onRegister }: { onRegister: () => void }): VNode { + const { i18n } = useTranslationContext(); + const r = useBackendContext(); + const account = r.state.status === "loggedIn" ? r.state.username : "admin"; + const result = useAccountDetails(account); + + if (!result.ok) { + return handleNotOkResult(i18n, onRegister)(result); + } + const { data } = result; + const balance = Amounts.parseOrThrow(data.balance.amount); + const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold); + const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit"; + const limit = balanceIsDebit + ? Amounts.sub(debitThreshold, balance).amount + : Amounts.add(balance, debitThreshold).amount; + if (!balance) return <Fragment />; + return ( + <Fragment> + <section id="assets"> + <div class="asset-summary"> + <h2>{i18n.str`Bank account balance`}</h2> + {!balance ? ( + <div class="large-amount" style={{ color: "gray" }}> + Waiting server response... + </div> + ) : ( + <div class="large-amount amount"> + {balanceIsDebit ? <b>-</b> : null} + <span class="value">{`${Amounts.stringifyValue(balance)}`}</span> + + <span class="currency">{`${balance.currency}`}</span> + </div> + )} + </div> + </section> + <PaytoWireTransferForm + focus + limit={limit} + onSuccess={() => { + notifyInfo(i18n.str`Wire transfer created!`); + }} + /> + </Fragment> + ); +} + const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; @@ -442,10 +416,13 @@ export function UpdateAccountPassword({ const [repeat, setRepeat] = useState<string | undefined>(); const [error, saveError] = useState<ErrorMessage | undefined>(); - if (result.clientError) { - if (result.isNotfound) return <div>account not found</div>; - } 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); } @@ -679,10 +656,13 @@ export function ShowAccountDetails({ >(); const [error, saveError] = useState<ErrorMessage | undefined>(); - if (result.clientError) { - if (result.isNotfound) return <div>account not found</div>; - } 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); } @@ -804,10 +784,13 @@ function RemoveAccount({ const { deleteAccount } = useAdminAccountAPI(); const [error, saveError] = useState<ErrorMessage | undefined>(); - if (result.clientError) { - if (result.isNotfound) return <div>account not found</div>; - } 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); } diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index e75a5c1d0..d1f7250b9 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -14,15 +14,19 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Logger } from "@gnu-taler/taler-util"; +import { Logger, TranslatedString } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { StateUpdater, useEffect, useState } from "preact/hooks"; import talerLogo from "../assets/logo-white.svg"; import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js"; import { useBackendContext } from "../context/backend.js"; import { ErrorMessage, + PageStateProvider, PageStateType, + errorListeners, + infoListeners, usePageContext, } from "../context/pageState.js"; import { useBusinessAccountDetails } from "../hooks/circuit.js"; @@ -56,7 +60,20 @@ function MaybeBusinessButton({ ); } -export function BankFrame({ +export function BankFrame(props: { + children: ComponentChildren; + goToBusinessAccount?: () => void; +}): VNode { + return ( + <PageStateProvider> + <BankFrame2 goToBusinessAccount={props.goToBusinessAccount}> + {props.children} + </BankFrame2> + </PageStateProvider> + ); +} + +function BankFrame2({ children, goToBusinessAccount, }: { @@ -65,8 +82,8 @@ export function BankFrame({ }): VNode { const { i18n } = useTranslationContext(); const backend = useBackendContext(); - const { pageState, pageStateSetter } = usePageContext(); - logger.trace("state", pageState); + + const { pageStateSetter } = usePageContext(); const demo_sites = []; for (const i in bankUiSettings.demoSites) @@ -140,17 +157,9 @@ export function BankFrame({ href="#" class="pure-button logout-button" onClick={() => { - pageStateSetter((prevState: PageStateType) => { - const { talerWithdrawUri, withdrawalId, ...rest } = - prevState; - backend.logOut(); - return { - ...rest, - withdrawalInProgress: false, - error: undefined, - info: undefined, - isRawPayto: false, - }; + backend.logOut(); + pageStateSetter({ + currentWithdrawalOperationId: undefined, }); }} >{i18n.str`Logout`}</a> @@ -244,8 +253,33 @@ function ErrorBanner({ } function StatusBanner(): VNode | null { - const { pageState, pageStateSetter } = usePageContext(); - + const [info, setInfo] = useState<TranslatedString>(); + const [error, setError] = useState<ErrorMessage>(); + console.log("render", info, error); + function listenError(e: ErrorMessage) { + setError(e); + } + function listenInfo(m: TranslatedString) { + console.log("update info", m, info); + setInfo(m); + } + useEffect(() => { + console.log("sadasdsad", infoListeners.length); + errorListeners.push(listenError); + infoListeners.push(listenInfo); + console.log("sadasdsad", infoListeners.length); + return function unsuscribe() { + const idx = infoListeners.findIndex((d) => d === listenInfo); + if (idx !== -1) { + infoListeners.splice(idx, 1); + } + const idx2 = errorListeners.findIndex((d) => d === listenError); + if (idx2 !== -1) { + errorListeners.splice(idx2, 1); + } + console.log("unload", idx); + }; + }, []); return ( <div style={{ @@ -255,14 +289,14 @@ function StatusBanner(): VNode | null { width: "90%", }} > - {!pageState.info ? undefined : ( + {!info ? undefined : ( <div class="informational informational-ok" style={{ marginTop: 8, paddingLeft: 16, paddingRight: 16 }} > <div style={{ display: "flex", justifyContent: "space-between" }}> <p> - <b>{pageState.info}</b> + <b>{info}</b> </p> <div> <input @@ -270,18 +304,18 @@ function StatusBanner(): VNode | null { class="pure-button" value="Clear" onClick={async () => { - pageStateSetter((prev) => ({ ...prev, info: undefined })); + setInfo(undefined); }} /> </div> </div> </div> )} - {!pageState.error ? undefined : ( + {!error ? undefined : ( <ErrorBanner - error={pageState.error} + error={error} onClear={() => { - pageStateSetter((prev) => ({ ...prev, error: undefined })); + setError(undefined); }} /> )} diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/BusinessAccount.tsx index 262376fa2..02e64ac39 100644 --- a/packages/demobank-ui/src/pages/BusinessAccount.tsx +++ b/packages/demobank-ui/src/pages/BusinessAccount.tsx @@ -25,11 +25,17 @@ import { RequestError, useTranslationContext, } from "@gnu-taler/web-util/lib/index.browser"; -import { Fragment, h, VNode } from "preact"; -import { useEffect, useMemo, useState } from "preact/hooks"; +import { Fragment, VNode, h } from "preact"; +import { StateUpdater, useEffect, useState } from "preact/hooks"; import { Cashouts } from "../components/Cashouts/index.js"; import { useBackendContext } from "../context/backend.js"; -import { ErrorMessage, usePageContext } from "../context/pageState.js"; +import { + ErrorMessage, + ObservedStateType, + PageStateType, + notifyInfo, + usePageContext, +} from "../context/pageState.js"; import { useAccountDetails } from "../hooks/access.js"; import { useCashoutDetails, @@ -38,21 +44,20 @@ import { useRatiosAndFeeConfig, } from "../hooks/circuit.js"; import { - buildRequestErrorMessage, TanChannel, + buildRequestErrorMessage, undefinedIfEmpty, } from "../utils.js"; import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js"; import { ErrorBannerFloat } from "./BankFrame.js"; import { LoginForm } from "./LoginForm.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; +import { handleNotOkResult } from "./HomePage.js"; interface Props { onClose: () => void; onRegister: () => void; - onLoadNotOk: <T>( - error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, - ) => VNode; + onLoadNotOk: () => void; } export function BusinessAccount({ onClose, @@ -60,19 +65,12 @@ export function BusinessAccount({ onRegister, }: Props): VNode { const { i18n } = useTranslationContext(); - const { pageStateSetter } = usePageContext(); const backend = useBackendContext(); const [updatePassword, setUpdatePassword] = useState(false); const [newCashout, setNewcashout] = useState(false); const [showCashoutDetails, setShowCashoutDetails] = useState< string | undefined >(); - function showInfoMessage(info: TranslatedString): void { - pageStateSetter((prev) => ({ - ...prev, - info, - })); - } if (backend.state.status === "loggedOut") { return <LoginForm onRegister={onRegister} />; @@ -82,12 +80,12 @@ export function BusinessAccount({ return ( <CreateCashout account={backend.state.username} - onLoadNotOk={onLoadNotOk} + onLoadNotOk={handleNotOkResult(i18n, onRegister)} onCancel={() => { setNewcashout(false); }} onComplete={(id) => { - showInfoMessage( + notifyInfo( i18n.str`Cashout created. You need to confirm the operation to complete the transaction.`, ); setNewcashout(false); @@ -100,7 +98,7 @@ export function BusinessAccount({ return ( <ShowCashoutDetails id={showCashoutDetails} - onLoadNotOk={onLoadNotOk} + onLoadNotOk={handleNotOkResult(i18n, onRegister)} onCancel={() => { setShowCashoutDetails(undefined); }} @@ -111,9 +109,9 @@ export function BusinessAccount({ return ( <UpdateAccountPassword account={backend.state.username} - onLoadNotOk={onLoadNotOk} + onLoadNotOk={handleNotOkResult(i18n, onRegister)} onUpdateSuccess={() => { - showInfoMessage(i18n.str`Password changed`); + notifyInfo(i18n.str`Password changed`); setUpdatePassword(false); }} onClear={() => { @@ -126,9 +124,9 @@ export function BusinessAccount({ <div> <ShowAccountDetails account={backend.state.username} - onLoadNotOk={onLoadNotOk} + onLoadNotOk={handleNotOkResult(i18n, onRegister)} onUpdateSuccess={() => { - showInfoMessage(i18n.str`Account updated`); + notifyInfo(i18n.str`Account updated`); }} onChangePassword={() => { setUpdatePassword(true); @@ -168,7 +166,9 @@ interface PropsCashout { onComplete: (id: string) => void; onCancel: () => void; onLoadNotOk: <T>( - error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + error: + | HttpResponsePaginated<T, SandboxBackend.SandboxError> + | HttpResponse<T, SandboxBackend.SandboxError>, ) => VNode; } diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index 7ef4284bf..0a5a61396 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -14,16 +14,30 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Logger } from "@gnu-taler/taler-util"; +import { + HttpStatusCode, + Logger, + parseWithdrawUri, + stringifyWithdrawUri, +} from "@gnu-taler/taler-util"; import { ErrorType, + HttpResponse, HttpResponsePaginated, useTranslationContext, } from "@gnu-taler/web-util/lib/index.browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; +import { StateUpdater } from "preact/hooks"; import { Loading } from "../components/Loading.js"; import { useBackendContext } from "../context/backend.js"; -import { PageStateType, usePageContext } from "../context/pageState.js"; +import { + ObservedStateType, + PageStateType, + notifyError, + notifyInfo, + usePageContext, +} from "../context/pageState.js"; +import { getInitialBackendBaseURL } from "../hooks/backend.js"; import { AccountPage } from "./AccountPage.js"; import { AdminPage } from "./AdminPage.js"; import { LoginForm } from "./LoginForm.js"; @@ -41,133 +55,109 @@ const logger = new Logger("AccountPage"); * @param param0 * @returns */ -export function HomePage({ onRegister }: { onRegister: () => void }): VNode { +export function HomePage({ + onRegister, + onPendingOperationFound, +}: { + onPendingOperationFound: (id: string) => void; + onRegister: () => void; +}): VNode { const backend = useBackendContext(); const { pageState, pageStateSetter } = usePageContext(); const { i18n } = useTranslationContext(); - function saveError(error: PageStateType["error"]): void { - pageStateSetter((prev) => ({ ...prev, error })); - } - - function saveErrorAndLogout(error: PageStateType["error"]): void { - saveError(error); - backend.logOut(); - } - - function clearCurrentWithdrawal(): void { - pageStateSetter((prevState: PageStateType) => { - return { - ...prevState, - withdrawalId: undefined, - talerWithdrawUri: undefined, - withdrawalInProgress: false, - }; - }); - } - if (backend.state.status === "loggedOut") { return <LoginForm onRegister={onRegister} />; } - const { withdrawalId, talerWithdrawUri } = pageState; - - if (talerWithdrawUri && withdrawalId) { - return ( - <WithdrawalQRCode - account={backend.state.username} - withdrawalId={withdrawalId} - talerWithdrawUri={talerWithdrawUri} - onConfirmed={() => { - pageStateSetter((prevState) => { - const { talerWithdrawUri, ...rest } = prevState; - // remove talerWithdrawUri and add info - return { - ...rest, - info: i18n.str`Withdrawal confirmed!`, - }; - }); - }} - onError={(error) => { - pageStateSetter((prevState) => { - const { talerWithdrawUri, ...rest } = prevState; - // remove talerWithdrawUri and add error - return { - ...rest, - error, - }; - }); - }} - onAborted={clearCurrentWithdrawal} - onLoadNotOk={handleNotOkResult( - backend.state.username, - saveError, - i18n, - onRegister, - )} - /> - ); + if (pageState.currentWithdrawalOperationId) { + onPendingOperationFound(pageState.currentWithdrawalOperationId); + return <Loading />; } if (backend.state.isUserAdministrator) { - return ( - <AdminPage - onLoadNotOk={handleNotOkResult( - backend.state.username, - saveErrorAndLogout, - i18n, - onRegister, - )} - /> - ); + return <AdminPage onRegister={onRegister} />; } return ( <AccountPage account={backend.state.username} - onLoadNotOk={handleNotOkResult( - backend.state.username, - saveErrorAndLogout, - i18n, - onRegister, - )} + onLoadNotOk={handleNotOkResult(i18n, onRegister)} + /> + ); +} + +export function WithdrawalOperationPage({ + operationId, + onLoadNotOk, + onAbort, +}: { + operationId: string; + onLoadNotOk: () => void; + onAbort: () => void; +}): VNode { + const uri = stringifyWithdrawUri({ + bankIntegrationApiBaseUrl: getInitialBackendBaseURL(), + withdrawalOperationId: operationId, + }); + const parsedUri = parseWithdrawUri(uri); + const { i18n } = useTranslationContext(); + const { pageStateSetter } = usePageContext(); + function clearCurrentWithdrawal(): void { + pageStateSetter({}); + onAbort(); + } + + if (!parsedUri) { + notifyError({ + title: i18n.str`The Withdrawal URI is not valid: "${uri}"`, + }); + return <Loading />; + } + + return ( + <WithdrawalQRCode + withdrawUri={parsedUri} + onConfirmed={() => { + notifyInfo(i18n.str`Withdrawal confirmed!`); + }} + onAborted={clearCurrentWithdrawal} + onLoadNotOk={onLoadNotOk} /> ); } -function handleNotOkResult( - account: string, - onErrorHandler: (state: PageStateType["error"]) => void, +export function handleNotOkResult( i18n: ReturnType<typeof useTranslationContext>["i18n"], - onRegister: () => void, -): <T>(result: HttpResponsePaginated<T, SandboxBackend.SandboxError>) => VNode { + onRegister?: () => void, +): <T>( + result: + | HttpResponsePaginated<T, SandboxBackend.SandboxError> + | HttpResponse<T, SandboxBackend.SandboxError>, +) => VNode { return function handleNotOkResult2<T>( - result: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + result: + | HttpResponsePaginated<T, SandboxBackend.SandboxError> + | HttpResponse<T, SandboxBackend.SandboxError>, ): VNode { - if (result.clientError && result.isUnauthorized) { - onErrorHandler({ - title: i18n.str`Wrong credentials for "${account}"`, - }); - return <LoginForm onRegister={onRegister} />; - } - if (result.clientError && result.isNotfound) { - onErrorHandler({ - title: i18n.str`Username or account label "${account}" not found`, - }); - return <LoginForm onRegister={onRegister} />; - } if (result.loading) return <Loading />; if (!result.ok) { switch (result.type) { case ErrorType.TIMEOUT: { - onErrorHandler({ + notifyError({ title: i18n.str`Request timeout, try again later.`, }); break; } case ErrorType.CLIENT: { + if (result.status === HttpStatusCode.Unauthorized) { + notifyError({ + title: i18n.str`Wrong credentials`, + }); + return <LoginForm onRegister={onRegister} />; + } const errorData = result.payload; - onErrorHandler({ + notifyError({ title: i18n.str`Could not load due to a client error`, description: errorData.error.description, debug: JSON.stringify(result), @@ -175,19 +165,18 @@ function handleNotOkResult( break; } case ErrorType.SERVER: { - const errorData = result.error; - onErrorHandler({ + notifyError({ title: i18n.str`Server returned with error`, - description: errorData.error.description, - debug: JSON.stringify(result), + description: result.payload.error.description, + debug: JSON.stringify(result.payload), }); break; } case ErrorType.UNEXPECTED: { - onErrorHandler({ + notifyError({ title: i18n.str`Unexpected error.`, description: `Diagnostic from ${result.info?.url} is "${result.message}"`, - debug: JSON.stringify(result.exception), + debug: JSON.stringify(result), }); break; } @@ -196,7 +185,7 @@ function handleNotOkResult( } } - return <LoginForm onRegister={onRegister} />; + return <div>error</div>; } return <div />; }; diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index 16d2373da..7116e724e 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { HttpStatusCode } from "@gnu-taler/taler-util"; import { ErrorType, useTranslationContext, @@ -32,7 +33,7 @@ import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; /** * Collect and submit login data. */ -export function LoginForm({ onRegister }: { onRegister: () => void }): VNode { +export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode { const backend = useBackendContext(); const [username, setUsername] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>(); @@ -119,35 +120,60 @@ export function LoginForm({ onRegister }: { onRegister: () => void }): VNode { onClick={async (e) => { e.preventDefault(); if (!username || !password) return; - const { valid, cause } = await testLogin(username, password); - if (valid) { + const testResult = await testLogin(username, password); + if (testResult.valid) { backend.logIn({ username, password }); } else { - switch (cause) { - case ErrorType.CLIENT: { - saveError({ - title: i18n.str`Wrong credentials or username`, - }); - break; - } - case ErrorType.SERVER: { - saveError({ - title: i18n.str`Server had a problem, try again later or report.`, - }); - break; - } - case ErrorType.TIMEOUT: { - saveError({ - title: i18n.str`Could not reach the server, please report.`, - }); - break; - } - default: { - saveError({ - title: i18n.str`Unexpected error, please report.`, - }); - break; + if (testResult.requestError) { + const { cause } = testResult; + switch (cause.type) { + case ErrorType.CLIENT: { + if (cause.status === HttpStatusCode.Unauthorized) { + saveError({ + title: i18n.str`Wrong credentials for "${username}"`, + }); + } + if (cause.status === HttpStatusCode.NotFound) { + saveError({ + title: i18n.str`Account not found`, + }); + } else { + saveError({ + title: i18n.str`Could not load due to a client error`, + description: cause.payload.error.description, + 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; + } + default: { + saveError({ + title: i18n.str`Unexpected error, please report.`, + description: `Diagnostic from ${cause.info?.url} is "${cause.message}"`, + debug: JSON.stringify(cause), + }); + break; + } } + } else { + saveError({ + title: i18n.str`Unexpected error, please report.`, + debug: JSON.stringify(testResult.error), + }); } backend.logOut(); } @@ -158,7 +184,7 @@ export function LoginForm({ onRegister }: { onRegister: () => void }): VNode { {i18n.str`Login`} </button> - {bankUiSettings.allowRegistrations ? ( + {bankUiSettings.allowRegistrations && onRegister ? ( <button class="pure-button pure-button-secondary btn-cancel" onClick={(e) => { diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 291f2aa9e..e0ad64e64 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -17,8 +17,13 @@ import { AmountJson } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser"; import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { PageStateType, usePageContext } from "../context/pageState.js"; +import { StateUpdater, useState } from "preact/hooks"; +import { + notifyError, + notifyInfo, + PageStateType, + usePageContext, +} from "../context/pageState.js"; import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; import { WalletWithdrawForm } from "./WalletWithdrawForm.js"; @@ -33,9 +38,6 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">( "charge-wallet", ); - function saveError(error: PageStateType["error"]): void { - pageStateSetter((prev) => ({ ...prev, error })); - } return ( <article> @@ -64,15 +66,11 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { <WalletWithdrawForm focus limit={limit} - onSuccess={(data) => { - pageStateSetter((prevState: PageStateType) => ({ - ...prevState, - withdrawalInProgress: true, - talerWithdrawUri: data.taler_withdraw_uri, - withdrawalId: data.withdrawal_id, - })); + onSuccess={(currentWithdrawalOperationId) => { + pageStateSetter({ + currentWithdrawalOperationId, + }); }} - onError={saveError} /> </div> )} @@ -83,12 +81,8 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { focus limit={limit} onSuccess={() => { - pageStateSetter((prevState: PageStateType) => ({ - ...prevState, - info: i18n.str`Wire transfer created!`, - })); + notifyInfo(i18n.str`Wire transfer created!`); }} - onError={saveError} /> </div> )} diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index 027f8e25a..5f16fbf6b 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -29,7 +29,11 @@ import { } from "@gnu-taler/web-util/lib/index.browser"; import { h, VNode } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; -import { PageStateType } from "../context/pageState.js"; +import { + notifyError, + ObservedStateType, + PageStateType, +} from "../context/pageState.js"; import { useAccessAPI } from "../hooks/access.js"; import { buildRequestErrorMessage, @@ -42,20 +46,14 @@ const logger = new Logger("PaytoWireTransferForm"); export function PaytoWireTransferForm({ focus, - onError, onSuccess, limit, }: { focus?: boolean; - onError: (e: PageStateType["error"]) => void; onSuccess: () => void; limit: AmountJson; }): VNode { - // const backend = useBackendContext(); - // const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button? - const [isRawPayto, setIsRawPayto] = useState(false); - // const [submitData, submitDataSetter] = useWireTransferRequestType(); const [iban, setIban] = useState<string | undefined>(undefined); const [subject, setSubject] = useState<string | undefined>(undefined); const [amount, setAmount] = useState<string | undefined>(undefined); @@ -201,7 +199,7 @@ export function PaytoWireTransferForm({ setSubject(undefined); } catch (error) { if (error instanceof RequestError) { - onError( + notifyError( buildRequestErrorMessage(i18n, error.cause, { onClientError: (status) => status === HttpStatusCode.BadRequest @@ -210,7 +208,7 @@ export function PaytoWireTransferForm({ }), ); } else { - onError({ + notifyError({ title: i18n.str`Operation failed, please report`, description: error instanceof Error @@ -330,7 +328,7 @@ export function PaytoWireTransferForm({ rawPaytoInputSetter(undefined); } catch (error) { if (error instanceof RequestError) { - onError( + notifyError( buildRequestErrorMessage(i18n, error.cause, { onClientError: (status) => status === HttpStatusCode.BadRequest @@ -339,7 +337,7 @@ export function PaytoWireTransferForm({ }), ); } else { - onError({ + notifyError({ title: i18n.str`Operation failed, please report`, description: error instanceof Error diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx index 2b5f7e26c..290fd0a79 100644 --- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx +++ b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx @@ -24,6 +24,13 @@ import { Fragment, h, VNode } from "preact"; import { StateUpdater } from "preact/hooks"; import { Transactions } from "../components/Transactions/index.js"; import { usePublicAccounts } from "../hooks/access.js"; +import { + PageStateType, + notifyError, + usePageContext, +} from "../context/pageState.js"; +import { handleNotOkResult } from "./HomePage.js"; +import { Loading } from "../components/Loading.js"; const logger = new Logger("PublicHistoriesPage"); @@ -36,9 +43,7 @@ const logger = new Logger("PublicHistoriesPage"); // } interface Props { - onLoadNotOk: <T>( - error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, - ) => VNode; + onLoadNotOk: () => void; } /** @@ -49,7 +54,10 @@ export function PublicHistoriesPage({ onLoadNotOk }: Props): VNode { const { i18n } = useTranslationContext(); const result = usePublicAccounts(); - if (!result.ok) return onLoadNotOk(result); + if (!result.ok) { + onLoadNotOk(); + return handleNotOkResult(i18n)(result); + } const { data } = result; diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx index 8f85fff91..8613bfca7 100644 --- a/packages/demobank-ui/src/pages/QrCodeSection.tsx +++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx @@ -14,16 +14,17 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { stringifyWithdrawUri, WithdrawUriResult } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser"; import { h, VNode } from "preact"; import { useEffect } from "preact/hooks"; import { QR } from "../components/QR.js"; export function QrCodeSection({ - talerWithdrawUri, + withdrawUri, onAborted, }: { - talerWithdrawUri: string; + withdrawUri: WithdrawUriResult; onAborted: () => void; }): VNode { const { i18n } = useTranslationContext(); @@ -33,8 +34,9 @@ export function QrCodeSection({ //this hack manually triggers the tab update after the QR is in the DOM. // WebExtension will be using // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated - document.title = `${document.title} ${talerWithdrawUri}`; + document.title = `${document.title} ${withdrawUri.withdrawalOperationId}`; }, []); + const talerWithdrawUri = stringifyWithdrawUri(withdrawUri); return ( <section id="main" class="content"> diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx index 8554b1def..5b9584dde 100644 --- a/packages/demobank-ui/src/pages/RegistrationPage.tsx +++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx @@ -21,7 +21,11 @@ import { import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useBackendContext } from "../context/backend.js"; -import { PageStateType } from "../context/pageState.js"; +import { + PageStateType, + notifyError, + usePageContext, +} from "../context/pageState.js"; import { useTestingAPI } from "../hooks/access.js"; import { bankUiSettings } from "../settings.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; @@ -30,11 +34,9 @@ import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; const logger = new Logger("RegistrationPage"); export function RegistrationPage({ - onError, onComplete, }: { onComplete: () => void; - onError: (e: PageStateType["error"]) => void; }): VNode { const { i18n } = useTranslationContext(); if (!bankUiSettings.allowRegistrations) { @@ -42,7 +44,7 @@ export function RegistrationPage({ <p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p> ); } - return <RegistrationForm onComplete={onComplete} onError={onError} />; + return <RegistrationForm onComplete={onComplete} />; } export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/; @@ -50,13 +52,7 @@ export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/; /** * Collect and submit registration data. */ -function RegistrationForm({ - onComplete, - onError, -}: { - onComplete: () => void; - onError: (e: PageStateType["error"]) => void; -}): VNode { +function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode { const backend = useBackendContext(); const [username, setUsername] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>(); @@ -171,7 +167,7 @@ function RegistrationForm({ onComplete(); } catch (error) { if (error instanceof RequestError) { - onError( + notifyError( buildRequestErrorMessage(i18n, error.cause, { onClientError: (status) => status === HttpStatusCode.Conflict @@ -180,7 +176,7 @@ function RegistrationForm({ }), ); } else { - onError({ + notifyError({ title: i18n.str`Operation failed, please report`, description: error instanceof Error diff --git a/packages/demobank-ui/src/pages/Routing.tsx b/packages/demobank-ui/src/pages/Routing.tsx index 8234d8988..27aae69e9 100644 --- a/packages/demobank-ui/src/pages/Routing.tsx +++ b/packages/demobank-ui/src/pages/Routing.tsx @@ -14,140 +14,77 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - ErrorType, - HttpResponsePaginated, - useTranslationContext, -} from "@gnu-taler/web-util/lib/index.browser"; +import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser"; import { createHashHistory } from "history"; -import { h, VNode } from "preact"; -import { Router, route, Route } from "preact-router"; -import { useEffect } from "preact/hooks"; -import { Loading } from "../components/Loading.js"; -import { PageStateType, usePageContext } from "../context/pageState.js"; -import { HomePage } from "./HomePage.js"; +import { VNode, h } from "preact"; +import { Route, Router, route } from "preact-router"; +import { useEffect, useMemo, useState } from "preact/hooks"; import { BankFrame } from "./BankFrame.js"; +import { BusinessAccount } from "./BusinessAccount.js"; +import { HomePage, WithdrawalOperationPage } from "./HomePage.js"; import { PublicHistoriesPage } from "./PublicHistoriesPage.js"; import { RegistrationPage } from "./RegistrationPage.js"; -import { BusinessAccount } from "./BusinessAccount.js"; - -function handleNotOkResult( - safe: string, - saveError: (state: PageStateType["error"]) => void, - i18n: ReturnType<typeof useTranslationContext>["i18n"], -): <T>(result: HttpResponsePaginated<T, SandboxBackend.SandboxError>) => VNode { - return function handleNotOkResult2<T>( - result: HttpResponsePaginated<T, SandboxBackend.SandboxError>, - ): VNode { - if (result.clientError && result.isUnauthorized) { - route(safe); - return <Loading />; - } - if (result.clientError && result.isNotfound) { - route(safe); - return ( - <div>Page not found, you are going to be redirected to {safe}</div> - ); - } - if (result.loading) return <Loading />; - if (!result.ok) { - switch (result.type) { - case ErrorType.TIMEOUT: { - saveError({ - title: i18n.str`Request timeout, try again later.`, - }); - break; - } - case ErrorType.CLIENT: { - const errorData = result.error; - saveError({ - title: i18n.str`Could not load due to a client error`, - description: errorData.error.description, - debug: JSON.stringify(result), - }); - break; - } - case ErrorType.SERVER: { - const errorData = result.error; - saveError({ - title: i18n.str`Server returned with error`, - description: errorData.error.description, - debug: JSON.stringify(result), - }); - break; - } - case ErrorType.UNEXPECTED: { - saveError({ - title: i18n.str`Unexpected error.`, - description: `Diagnostic from ${result.info?.url} is "${result.message}"`, - debug: JSON.stringify(result.error), - }); - break; - } - default: - { - assertUnreachable(result); - } - route(safe); - } - } - return <div />; - }; -} export function Routing(): VNode { const history = createHashHistory(); - const { pageStateSetter } = usePageContext(); - function saveError(error: PageStateType["error"]): void { - pageStateSetter((prev) => ({ ...prev, error })); - } - const { i18n } = useTranslationContext(); return ( - <Router history={history}> - <Route - path="/public-accounts" - component={() => ( - <BankFrame> + <BankFrame + goToBusinessAccount={() => { + route("/business"); + }} + > + <Router history={history}> + <Route + path="/operation/:wopid" + component={({ wopid }: { wopid: string }) => ( + <WithdrawalOperationPage + operationId={wopid} + onAbort={() => { + route("/account"); + }} + onLoadNotOk={() => { + route("/account"); + }} + /> + )} + /> + <Route + path="/public-accounts" + component={() => ( <PublicHistoriesPage - onLoadNotOk={handleNotOkResult("/account", saveError, i18n)} + onLoadNotOk={() => { + route("/account"); + }} /> - </BankFrame> - )} - /> - <Route - path="/register" - component={() => ( - <BankFrame> + )} + /> + <Route + path="/register" + component={() => ( <RegistrationPage - onError={saveError} onComplete={() => { route("/account"); }} /> - </BankFrame> - )} - /> - <Route - path="/account" - component={() => ( - <BankFrame - goToBusinessAccount={() => { - route("/business"); - }} - > + )} + /> + <Route + path="/account" + component={() => ( <HomePage + onPendingOperationFound={(wopid) => { + route(`/operation/${wopid}`); + }} onRegister={() => { route("/register"); }} /> - </BankFrame> - )} - /> - <Route - path="/business" - component={() => ( - <BankFrame> + )} + /> + <Route + path="/business" + component={() => ( <BusinessAccount onClose={() => { route("/account"); @@ -155,13 +92,15 @@ export function Routing(): VNode { onRegister={() => { route("/register"); }} - onLoadNotOk={handleNotOkResult("/account", saveError, i18n)} + onLoadNotOk={() => { + route("/account"); + }} /> - </BankFrame> - )} - /> - <Route default component={Redirect} to="/account" /> - </Router> + )} + /> + <Route default component={Redirect} to="/account" /> + </Router> + </BankFrame> ); } diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index 8bbfe0713..7f3e207ac 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -19,6 +19,7 @@ import { Amounts, HttpStatusCode, Logger, + parseWithdrawUri, } from "@gnu-taler/taler-util"; import { RequestError, @@ -26,7 +27,11 @@ import { } from "@gnu-taler/web-util/lib/index.browser"; import { h, VNode } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; -import { PageStateType } from "../context/pageState.js"; +import { + ObservedStateType, + PageStateType, + notifyError, +} from "../context/pageState.js"; import { useAccessAPI } from "../hooks/access.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; @@ -36,18 +41,12 @@ const logger = new Logger("WalletWithdrawForm"); export function WalletWithdrawForm({ focus, limit, - onError, onSuccess, }: { limit: AmountJson; focus?: boolean; - onError: (e: PageStateType["error"]) => void; - onSuccess: ( - data: SandboxBackend.Access.BankAccountCreateWithdrawalResponse, - ) => void; + onSuccess: (operationId: string) => void; }): VNode { - // const backend = useBackendContext(); - // const { pageState, pageStateSetter } = usePageContext(); const { i18n } = useTranslationContext(); const { createWithdrawal } = useAccessAPI(); @@ -129,10 +128,18 @@ export function WalletWithdrawForm({ const result = await createWithdrawal({ amount: Amounts.stringify(parsedAmount), }); - onSuccess(result.data); + const uri = parseWithdrawUri(result.data.taler_withdraw_uri); + if (!uri) { + return notifyError({ + title: i18n.str`Server responded with an invalid withdraw URI`, + description: i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`, + }); + } else { + onSuccess(uri.withdrawalOperationId); + } } catch (error) { if (error instanceof RequestError) { - onError( + notifyError( buildRequestErrorMessage(i18n, error.cause, { onClientError: (status) => status === HttpStatusCode.Forbidden @@ -141,7 +148,7 @@ export function WalletWithdrawForm({ }), ); } else { - onError({ + notifyError({ title: i18n.str`Operation failed, please report`, description: error instanceof Error diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index d7ed215be..10a37cd88 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -14,35 +14,41 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { HttpStatusCode, Logger } from "@gnu-taler/taler-util"; +import { + HttpStatusCode, + Logger, + WithdrawUriResult, +} from "@gnu-taler/taler-util"; import { RequestError, useTranslationContext, } from "@gnu-taler/web-util/lib/index.browser"; import { Fragment, h, VNode } from "preact"; import { useMemo, useState } from "preact/hooks"; -import { PageStateType, usePageContext } from "../context/pageState.js"; -import { useAccessAPI } from "../hooks/access.js"; +import { + ObservedStateType, + PageStateType, + notifyError, +} from "../context/pageState.js"; +import { useAccessAnonAPI } from "../hooks/access.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; const logger = new Logger("WithdrawalConfirmationQuestion"); interface Props { - withdrawalId: string; - onError: (e: PageStateType["error"]) => void; onConfirmed: () => void; onAborted: () => void; + withdrawUri: WithdrawUriResult; } /** * Additional authentication required to complete the operation. * Not providing a back button, only abort. */ export function WithdrawalConfirmationQuestion({ - onError, onConfirmed, onAborted, - withdrawalId, + withdrawUri, }: Props): VNode { const { i18n } = useTranslationContext(); @@ -53,7 +59,7 @@ export function WithdrawalConfirmationQuestion({ }; }, []); - const { confirmWithdrawal, abortWithdrawal } = useAccessAPI(); + const { confirmWithdrawal, abortWithdrawal } = useAccessAnonAPI(); const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>(); const answer = parseInt(captchaAnswer ?? "", 10); const errors = undefinedIfEmpty({ @@ -114,11 +120,13 @@ export function WithdrawalConfirmationQuestion({ onClick={async (e) => { e.preventDefault(); try { - await confirmWithdrawal(withdrawalId); + await confirmWithdrawal( + withdrawUri.withdrawalOperationId, + ); onConfirmed(); } catch (error) { if (error instanceof RequestError) { - onError( + notifyError( buildRequestErrorMessage(i18n, error.cause, { onClientError: (status) => status === HttpStatusCode.Conflict @@ -129,7 +137,7 @@ export function WithdrawalConfirmationQuestion({ }), ); } else { - onError({ + notifyError({ title: i18n.str`Operation failed, please report`, description: error instanceof Error @@ -148,11 +156,11 @@ export function WithdrawalConfirmationQuestion({ onClick={async (e) => { e.preventDefault(); try { - await abortWithdrawal(withdrawalId); + await abortWithdrawal(withdrawUri.withdrawalOperationId); onAborted(); } catch (error) { if (error instanceof RequestError) { - onError( + notifyError( buildRequestErrorMessage(i18n, error.cause, { onClientError: (status) => status === HttpStatusCode.Conflict @@ -161,7 +169,7 @@ export function WithdrawalConfirmationQuestion({ }), ); } else { - onError({ + notifyError({ title: i18n.str`Operation failed, please report`, description: error instanceof Error diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 1a4157d06..9c5f83eca 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -14,30 +14,35 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Logger, parseWithdrawUri } from "@gnu-taler/taler-util"; import { + HttpStatusCode, + Logger, + WithdrawUriResult, +} from "@gnu-taler/taler-util"; +import { + ErrorType, HttpResponsePaginated, useTranslationContext, } from "@gnu-taler/web-util/lib/index.browser"; import { Fragment, h, VNode } from "preact"; import { Loading } from "../components/Loading.js"; -import { PageStateType } from "../context/pageState.js"; +import { + ObservedStateType, + notifyError, + notifyInfo, +} from "../context/pageState.js"; import { useWithdrawalDetails } from "../hooks/access.js"; import { QrCodeSection } from "./QrCodeSection.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; +import { handleNotOkResult } from "./HomePage.js"; const logger = new Logger("WithdrawalQRCode"); interface Props { - account: string; - withdrawalId: string; - talerWithdrawUri: string; - onError: (e: PageStateType["error"]) => void; + withdrawUri: WithdrawUriResult; onAborted: () => void; onConfirmed: () => void; - onLoadNotOk: <T>( - error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, - ) => VNode; + onLoadNotOk: () => void; } /** * Offer the QR code (and a clickable taler://-link) to @@ -45,43 +50,46 @@ interface Props { * the bank. Poll the backend until such operation is done. */ export function WithdrawalQRCode({ - account, - withdrawalId, - talerWithdrawUri, + withdrawUri, onConfirmed, onAborted, - onError, onLoadNotOk, }: Props): VNode { const { i18n } = useTranslationContext(); - - const result = useWithdrawalDetails(account, withdrawalId); + const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId); if (!result.ok) { - return onLoadNotOk(result); + if (result.loading) { + return <Loading />; + } + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) { + return <div>operation not found</div>; + } + console.log("result", result); + onLoadNotOk(); + return handleNotOkResult(i18n)(result); } const { data } = result; logger.trace("withdrawal status", data); - if (data.aborted) { + if (data.aborted || data.confirmation_done) { // signal that this withdrawal is aborted // will redirect to account info + notifyInfo(i18n.str`Operation was completed from other session`); onAborted(); return <Loading />; } - const parsedUri = parseWithdrawUri(talerWithdrawUri); - if (!parsedUri) { - onError({ - title: i18n.str`The Withdrawal URI is not valid: "${talerWithdrawUri}"`, - }); - return <Loading />; - } - if (!data.selection_done) { return ( <QrCodeSection - talerWithdrawUri={talerWithdrawUri} - onAborted={onAborted} + withdrawUri={withdrawUri} + onAborted={() => { + notifyInfo(i18n.str`Operation canceled`); + onAborted(); + }} /> ); } @@ -90,10 +98,15 @@ export function WithdrawalQRCode({ // user to authorize the operation (here CAPTCHA). return ( <WithdrawalConfirmationQuestion - withdrawalId={parsedUri.withdrawalOperationId} - onError={onError} - onConfirmed={onConfirmed} - onAborted={onAborted} + withdrawUri={withdrawUri} + onConfirmed={() => { + notifyInfo(i18n.str`Operation confirmed`); + onConfirmed(); + }} + onAborted={() => { + notifyInfo(i18n.str`Operation canceled`); + onAborted(); + }} /> ); } |