aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-04-07 17:30:01 -0300
committerSebastian <sebasjm@gmail.com>2023-04-07 17:30:01 -0300
commita3aa7d95d09c83794067c47df4a455c0e3f21806 (patch)
tree00837196305227fe6f7cbc7289f96b256d5de089
parent43ae414a55b84b1125c5e4377c6d485ca6c748e2 (diff)
anon withdrawal confirmation, and fix error with infinity loop
-rw-r--r--packages/demobank-ui/src/components/app.tsx24
-rw-r--r--packages/demobank-ui/src/context/pageState.ts32
-rw-r--r--packages/demobank-ui/src/hooks/access.ts64
-rw-r--r--packages/demobank-ui/src/hooks/backend.ts26
-rw-r--r--packages/demobank-ui/src/pages/AccountPage.tsx20
-rw-r--r--packages/demobank-ui/src/pages/AdminPage.tsx179
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx80
-rw-r--r--packages/demobank-ui/src/pages/BusinessAccount.tsx44
-rw-r--r--packages/demobank-ui/src/pages/HomePage.tsx197
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx82
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx30
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx20
-rw-r--r--packages/demobank-ui/src/pages/PublicHistoriesPage.tsx16
-rw-r--r--packages/demobank-ui/src/pages/QrCodeSection.tsx8
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx22
-rw-r--r--packages/demobank-ui/src/pages/Routing.tsx177
-rw-r--r--packages/demobank-ui/src/pages/WalletWithdrawForm.tsx29
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx36
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx75
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>
- &nbsp;
- <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>
+ &nbsp;
+ <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();
+ }}
/>
);
}