anon withdrawal confirmation, and fix error with infinity loop
This commit is contained in:
parent
43ae414a55
commit
a3aa7d95d0
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -14,13 +14,9 @@
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
|
||||
import {
|
||||
Amounts,
|
||||
HttpStatusCode,
|
||||
parsePaytoUri,
|
||||
TranslatedString,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
ErrorType,
|
||||
HttpResponsePaginated,
|
||||
RequestError,
|
||||
useTranslationContext,
|
||||
@ -29,11 +25,7 @@ import { Fragment, h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { Cashouts } from "../components/Cashouts/index.js";
|
||||
import { useBackendContext } from "../context/backend.js";
|
||||
import {
|
||||
ErrorMessage,
|
||||
PageStateType,
|
||||
usePageContext,
|
||||
} from "../context/pageState.js";
|
||||
import { ErrorMessage, notifyInfo } from "../context/pageState.js";
|
||||
import { useAccountDetails } from "../hooks/access.js";
|
||||
import {
|
||||
useAdminAccountAPI,
|
||||
@ -50,6 +42,7 @@ import {
|
||||
} from "../utils.js";
|
||||
import { ErrorBannerFloat } from "./BankFrame.js";
|
||||
import { ShowCashoutDetails } from "./BusinessAccount.js";
|
||||
import { handleNotOkResult } from "./HomePage.js";
|
||||
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
|
||||
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
||||
|
||||
@ -69,14 +62,12 @@ function randomPassword(): string {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onLoadNotOk: <T>(
|
||||
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
||||
) => VNode;
|
||||
onRegister: () => void;
|
||||
}
|
||||
/**
|
||||
* Query account information and show QR code if there is pending withdrawal
|
||||
*/
|
||||
export function AdminPage({ onLoadNotOk }: Props): VNode {
|
||||
export function AdminPage({ onRegister }: Props): VNode {
|
||||
const [account, setAccount] = useState<string | undefined>();
|
||||
const [showDetails, setShowDetails] = useState<string | undefined>();
|
||||
const [showCashouts, setShowCashouts] = useState<string | undefined>();
|
||||
@ -87,24 +78,13 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
||||
>();
|
||||
|
||||
const [createAccount, setCreateAccount] = useState(false);
|
||||
const { pageStateSetter } = usePageContext();
|
||||
|
||||
function showInfoMessage(info: TranslatedString): void {
|
||||
pageStateSetter((prev) => ({
|
||||
...prev,
|
||||
info,
|
||||
}));
|
||||
}
|
||||
function saveError(error: PageStateType["error"]): void {
|
||||
pageStateSetter((prev) => ({ ...prev, error }));
|
||||
}
|
||||
|
||||
const result = useBusinessAccounts({ account });
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
if (result.loading) return <div />;
|
||||
if (!result.ok) {
|
||||
return onLoadNotOk(result);
|
||||
return handleNotOkResult(i18n, onRegister)(result);
|
||||
}
|
||||
|
||||
const { customers } = result.data;
|
||||
@ -113,7 +93,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
||||
return (
|
||||
<ShowCashoutDetails
|
||||
id={showCashoutDetails}
|
||||
onLoadNotOk={onLoadNotOk}
|
||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||
onCancel={() => {
|
||||
setShowCashoutDetails(undefined);
|
||||
}}
|
||||
@ -155,13 +135,13 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
||||
return (
|
||||
<ShowAccountDetails
|
||||
account={showDetails}
|
||||
onLoadNotOk={onLoadNotOk}
|
||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||
onChangePassword={() => {
|
||||
setUpdatePassword(showDetails);
|
||||
setShowDetails(undefined);
|
||||
}}
|
||||
onUpdateSuccess={() => {
|
||||
showInfoMessage(i18n.str`Account updated`);
|
||||
notifyInfo(i18n.str`Account updated`);
|
||||
setShowDetails(undefined);
|
||||
}}
|
||||
onClear={() => {
|
||||
@ -174,9 +154,9 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
||||
return (
|
||||
<RemoveAccount
|
||||
account={removeAccount}
|
||||
onLoadNotOk={onLoadNotOk}
|
||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||
onUpdateSuccess={() => {
|
||||
showInfoMessage(i18n.str`Account removed`);
|
||||
notifyInfo(i18n.str`Account removed`);
|
||||
setRemoveAccount(undefined);
|
||||
}}
|
||||
onClear={() => {
|
||||
@ -189,9 +169,9 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
||||
return (
|
||||
<UpdateAccountPassword
|
||||
account={updatePassword}
|
||||
onLoadNotOk={onLoadNotOk}
|
||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||
onUpdateSuccess={() => {
|
||||
showInfoMessage(i18n.str`Password changed`);
|
||||
notifyInfo(i18n.str`Password changed`);
|
||||
setUpdatePassword(undefined);
|
||||
}}
|
||||
onClear={() => {
|
||||
@ -205,7 +185,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
||||
<CreateNewAccount
|
||||
onClose={() => setCreateAccount(false)}
|
||||
onCreateSuccess={(password) => {
|
||||
showInfoMessage(
|
||||
notifyInfo(
|
||||
i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
|
||||
);
|
||||
setCreateAccount(false);
|
||||
@ -214,59 +194,6 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
||||
);
|
||||
}
|
||||
|
||||
function AdminAccount(): VNode {
|
||||
const r = useBackendContext();
|
||||
const account = r.state.status === "loggedIn" ? r.state.username : "admin";
|
||||
const result = useAccountDetails(account);
|
||||
|
||||
if (!result.ok) {
|
||||
return onLoadNotOk(result);
|
||||
}
|
||||
const { data } = result;
|
||||
const balance = Amounts.parseOrThrow(data.balance.amount);
|
||||
const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
|
||||
const balanceIsDebit =
|
||||
result.data.balance.credit_debit_indicator == "debit";
|
||||
const limit = balanceIsDebit
|
||||
? Amounts.sub(debitThreshold, balance).amount
|
||||
: Amounts.add(balance, debitThreshold).amount;
|
||||
if (!balance) return <Fragment />;
|
||||
return (
|
||||
<Fragment>
|
||||
<section id="assets">
|
||||
<div class="asset-summary">
|
||||
<h2>{i18n.str`Bank account balance`}</h2>
|
||||
{!balance ? (
|
||||
<div class="large-amount" style={{ color: "gray" }}>
|
||||
Waiting server response...
|
||||
</div>
|
||||
) : (
|
||||
<div class="large-amount amount">
|
||||
{balanceIsDebit ? <b>-</b> : null}
|
||||
<span class="value">{`${Amounts.stringifyValue(
|
||||
balance,
|
||||
)}`}</span>
|
||||
|
||||
<span class="currency">{`${balance.currency}`}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<PaytoWireTransferForm
|
||||
focus
|
||||
limit={limit}
|
||||
onSuccess={() => {
|
||||
pageStateSetter((prevState: PageStateType) => ({
|
||||
...prevState,
|
||||
info: i18n.str`Wire transfer created!`,
|
||||
}));
|
||||
}}
|
||||
onError={saveError}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div>
|
||||
@ -293,7 +220,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<AdminAccount />
|
||||
<AdminAccount onRegister={onRegister} />
|
||||
<section
|
||||
id="main"
|
||||
style={{ width: 600, marginLeft: "auto", marginRight: "auto" }}
|
||||
@ -393,6 +320,53 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
||||
);
|
||||
}
|
||||
|
||||
function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const r = useBackendContext();
|
||||
const account = r.state.status === "loggedIn" ? r.state.username : "admin";
|
||||
const result = useAccountDetails(account);
|
||||
|
||||
if (!result.ok) {
|
||||
return handleNotOkResult(i18n, onRegister)(result);
|
||||
}
|
||||
const { data } = result;
|
||||
const balance = Amounts.parseOrThrow(data.balance.amount);
|
||||
const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
|
||||
const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
|
||||
const limit = balanceIsDebit
|
||||
? Amounts.sub(debitThreshold, balance).amount
|
||||
: Amounts.add(balance, debitThreshold).amount;
|
||||
if (!balance) return <Fragment />;
|
||||
return (
|
||||
<Fragment>
|
||||
<section id="assets">
|
||||
<div class="asset-summary">
|
||||
<h2>{i18n.str`Bank account balance`}</h2>
|
||||
{!balance ? (
|
||||
<div class="large-amount" style={{ color: "gray" }}>
|
||||
Waiting server response...
|
||||
</div>
|
||||
) : (
|
||||
<div class="large-amount amount">
|
||||
{balanceIsDebit ? <b>-</b> : null}
|
||||
<span class="value">{`${Amounts.stringifyValue(balance)}`}</span>
|
||||
|
||||
<span class="currency">{`${balance.currency}`}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<PaytoWireTransferForm
|
||||
focus
|
||||
limit={limit}
|
||||
onSuccess={() => {
|
||||
notifyInfo(i18n.str`Wire transfer created!`);
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
|
||||
const EMAIL_REGEX =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
@ -442,10 +416,13 @@ export function UpdateAccountPassword({
|
||||
const [repeat, setRepeat] = useState<string | undefined>();
|
||||
const [error, saveError] = useState<ErrorMessage | undefined>();
|
||||
|
||||
if (result.clientError) {
|
||||
if (result.isNotfound) return <div>account not found</div>;
|
||||
}
|
||||
if (!result.ok) {
|
||||
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||
return onLoadNotOk(result);
|
||||
}
|
||||
if (result.status === HttpStatusCode.NotFound) {
|
||||
return <div>account not found</div>;
|
||||
}
|
||||
return onLoadNotOk(result);
|
||||
}
|
||||
|
||||
@ -679,10 +656,13 @@ export function ShowAccountDetails({
|
||||
>();
|
||||
const [error, saveError] = useState<ErrorMessage | undefined>();
|
||||
|
||||
if (result.clientError) {
|
||||
if (result.isNotfound) return <div>account not found</div>;
|
||||
}
|
||||
if (!result.ok) {
|
||||
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||
return onLoadNotOk(result);
|
||||
}
|
||||
if (result.status === HttpStatusCode.NotFound) {
|
||||
return <div>account not found</div>;
|
||||
}
|
||||
return onLoadNotOk(result);
|
||||
}
|
||||
|
||||
@ -804,10 +784,13 @@ function RemoveAccount({
|
||||
const { deleteAccount } = useAdminAccountAPI();
|
||||
const [error, saveError] = useState<ErrorMessage | undefined>();
|
||||
|
||||
if (result.clientError) {
|
||||
if (result.isNotfound) return <div>account not found</div>;
|
||||
}
|
||||
if (!result.ok) {
|
||||
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||
return onLoadNotOk(result);
|
||||
}
|
||||
if (result.status === HttpStatusCode.NotFound) {
|
||||
return <div>account not found</div>;
|
||||
}
|
||||
return onLoadNotOk(result);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 />;
|
||||
};
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user