anon withdrawal confirmation, and fix error with infinity loop

This commit is contained in:
Sebastian 2023-04-07 17:30:01 -03:00
parent 43ae414a55
commit a3aa7d95d0
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
19 changed files with 597 additions and 566 deletions

View File

@ -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>
);
};

View File

@ -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));
}

View File

@ -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,

View File

@ -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 };
}
};
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}}
/>
)}

View File

@ -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;
}

View File

@ -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)}
/>
);
}
function handleNotOkResult(
account: string,
onErrorHandler: (state: PageStateType["error"]) => void,
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}
/>
);
}
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 />;
};

View File

@ -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) => {

View File

@ -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>
)}

View File

@ -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

View File

@ -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;

View File

@ -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">

View File

@ -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

View File

@ -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>
<PublicHistoriesPage
onLoadNotOk={handleNotOkResult("/account", saveError, i18n)}
<BankFrame
goToBusinessAccount={() => {
route("/business");
}}
>
<Router history={history}>
<Route
path="/operation/:wopid"
component={({ wopid }: { wopid: string }) => (
<WithdrawalOperationPage
operationId={wopid}
onAbort={() => {
route("/account");
}}
onLoadNotOk={() => {
route("/account");
}}
/>
</BankFrame>
)}
/>
<Route
path="/register"
component={() => (
<BankFrame>
)}
/>
<Route
path="/public-accounts"
component={() => (
<PublicHistoriesPage
onLoadNotOk={() => {
route("/account");
}}
/>
)}
/>
<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>
);
}

View File

@ -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

View File

@ -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

View File

@ -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();
}}
/>
);
}