fix #7729
This commit is contained in:
parent
740849dd89
commit
9922192b0d
@ -32,7 +32,9 @@ describe("Transaction states", () => {
|
|||||||
|
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
account: "123",
|
account: "123",
|
||||||
onSelected: () => { null },
|
onSelected: () => {
|
||||||
|
null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
|
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
|
||||||
@ -117,7 +119,9 @@ describe("Transaction states", () => {
|
|||||||
|
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
account: "123",
|
account: "123",
|
||||||
onSelected: () => { null },
|
onSelected: () => {
|
||||||
|
null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {});
|
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {});
|
||||||
@ -151,7 +155,9 @@ describe("Transaction states", () => {
|
|||||||
|
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
account: "123",
|
account: "123",
|
||||||
onSelected: () => { null },
|
onSelected: () => {
|
||||||
|
null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {});
|
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {});
|
||||||
|
@ -279,7 +279,10 @@ export function useAuthenticatedBackend(): useBackendType {
|
|||||||
sandboxCashoutFetcher,
|
sandboxCashoutFetcher,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
export function useBackendConfig(): HttpResponse<
|
export function useBackendConfig(): HttpResponse<
|
||||||
SandboxBackend.Config,
|
SandboxBackend.Config,
|
||||||
SandboxBackend.SandboxError
|
SandboxBackend.SandboxError
|
||||||
|
@ -82,11 +82,11 @@ export function useAdminAccountAPI(): AdminAccountAPI {
|
|||||||
contentType: "json",
|
contentType: "json",
|
||||||
});
|
});
|
||||||
if (account === state.username) {
|
if (account === state.username) {
|
||||||
await mutateAll(/.*/)
|
await mutateAll(/.*/);
|
||||||
logIn({
|
logIn({
|
||||||
username: account,
|
username: account,
|
||||||
password: data.new_password
|
password: data.new_password,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
@ -284,7 +284,7 @@ export function useRatiosAndFeeConfig(): HttpResponse<
|
|||||||
HttpResponseOk<SandboxBackend.Circuit.Config>,
|
HttpResponseOk<SandboxBackend.Circuit.Config>,
|
||||||
RequestError<SandboxBackend.SandboxError>
|
RequestError<SandboxBackend.SandboxError>
|
||||||
>([`circuit-api/config`], fetcher, {
|
>([`circuit-api/config`], fetcher, {
|
||||||
refreshInterval: 0,
|
refreshInterval: 1000,
|
||||||
refreshWhenHidden: false,
|
refreshWhenHidden: false,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
revalidateOnReconnect: false,
|
revalidateOnReconnect: false,
|
||||||
@ -298,7 +298,7 @@ export function useRatiosAndFeeConfig(): HttpResponse<
|
|||||||
if (data) {
|
if (data) {
|
||||||
// data.data.ratios_and_fees.sell_out_fee = 2
|
// data.data.ratios_and_fees.sell_out_fee = 2
|
||||||
if (!data.data.ratios_and_fees.fiat_currency) {
|
if (!data.data.ratios_and_fees.fiat_currency) {
|
||||||
data.data.ratios_and_fees.fiat_currency = "FIAT"
|
data.data.ratios_and_fees.fiat_currency = "FIAT";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data) return data;
|
if (data) return data;
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Amounts,
|
Amounts,
|
||||||
|
HttpStatusCode,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
TranslatedString,
|
TranslatedString,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
@ -35,11 +36,13 @@ import {
|
|||||||
useAdminAccountAPI,
|
useAdminAccountAPI,
|
||||||
} from "../hooks/circuit.js";
|
} from "../hooks/circuit.js";
|
||||||
import {
|
import {
|
||||||
|
buildRequestErrorMessage,
|
||||||
PartialButDefined,
|
PartialButDefined,
|
||||||
|
RecursivePartial,
|
||||||
undefinedIfEmpty,
|
undefinedIfEmpty,
|
||||||
WithIntermediate,
|
WithIntermediate,
|
||||||
} from "../utils.js";
|
} from "../utils.js";
|
||||||
import { ErrorBanner } from "./BankFrame.js";
|
import { ErrorBannerFloat } from "./BankFrame.js";
|
||||||
import { ShowCashoutDetails } from "./BusinessAccount.js";
|
import { ShowCashoutDetails } from "./BusinessAccount.js";
|
||||||
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
||||||
|
|
||||||
@ -373,7 +376,7 @@ export function UpdateAccountPassword({
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<ErrorBanner error={error} onClear={() => saveError(undefined)} />
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form class="pure-form">
|
<form class="pure-form">
|
||||||
@ -435,7 +438,17 @@ export function UpdateAccountPassword({
|
|||||||
});
|
});
|
||||||
onUpdateSuccess();
|
onUpdateSuccess();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, saveError, i18n);
|
if (error instanceof RequestError) {
|
||||||
|
saveError(buildRequestErrorMessage(i18n, error.cause));
|
||||||
|
} else {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Operation failed, please report`,
|
||||||
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -467,13 +480,16 @@ function CreateNewAccount({
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<ErrorBanner error={error} onClear={() => saveError(undefined)} />
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AccountForm
|
<AccountForm
|
||||||
template={undefined}
|
template={undefined}
|
||||||
purpose="create"
|
purpose="create"
|
||||||
onChange={(a) => setSubmitAccount(a)}
|
onChange={(a) => {
|
||||||
|
console.log(a);
|
||||||
|
setSubmitAccount(a);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@ -514,7 +530,28 @@ function CreateNewAccount({
|
|||||||
await createAccount(account);
|
await createAccount(account);
|
||||||
onCreateSuccess(account.password);
|
onCreateSuccess(account.password);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, saveError, i18n);
|
if (error instanceof RequestError) {
|
||||||
|
saveError(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Forbidden
|
||||||
|
? i18n.str`The rights to perform the operation are not sufficient`
|
||||||
|
: status === HttpStatusCode.BadRequest
|
||||||
|
? i18n.str`Input data was invalid`
|
||||||
|
: status === HttpStatusCode.Conflict
|
||||||
|
? i18n.str`At least one registration detail was not available`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Operation failed, please report`,
|
||||||
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -564,7 +601,7 @@ export function ShowAccountDetails({
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<ErrorBanner error={error} onClear={() => saveError(undefined)} />
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
||||||
)}
|
)}
|
||||||
<AccountForm
|
<AccountForm
|
||||||
template={result.data}
|
template={result.data}
|
||||||
@ -622,7 +659,26 @@ export function ShowAccountDetails({
|
|||||||
});
|
});
|
||||||
onUpdateSuccess();
|
onUpdateSuccess();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, saveError, i18n);
|
if (error instanceof RequestError) {
|
||||||
|
saveError(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Forbidden
|
||||||
|
? i18n.str`The rights to change the account are not sufficient`
|
||||||
|
: status === HttpStatusCode.NotFound
|
||||||
|
? i18n.str`The username was not found`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Operation failed, please report`,
|
||||||
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -673,7 +729,7 @@ function RemoveAccount({
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{!isBalanceEmpty && (
|
{!isBalanceEmpty && (
|
||||||
<ErrorBanner
|
<ErrorBannerFloat
|
||||||
error={{
|
error={{
|
||||||
title: i18n.str`Can't delete the account`,
|
title: i18n.str`Can't delete the account`,
|
||||||
description: i18n.str`Balance is not empty`,
|
description: i18n.str`Balance is not empty`,
|
||||||
@ -681,7 +737,7 @@ function RemoveAccount({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<ErrorBanner error={error} onClear={() => saveError(undefined)} />
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@ -710,7 +766,28 @@ function RemoveAccount({
|
|||||||
const r = await deleteAccount(account);
|
const r = await deleteAccount(account);
|
||||||
onUpdateSuccess();
|
onUpdateSuccess();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, saveError, i18n);
|
if (error instanceof RequestError) {
|
||||||
|
saveError(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Forbidden
|
||||||
|
? i18n.str`The administrator specified a institutional username`
|
||||||
|
: status === HttpStatusCode.NotFound
|
||||||
|
? i18n.str`The username was not found`
|
||||||
|
: status === HttpStatusCode.PreconditionFailed
|
||||||
|
? i18n.str`Balance was not zero`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Operation failed, please report`,
|
||||||
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -720,7 +797,6 @@ function RemoveAccount({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create valid account object to update or create
|
* Create valid account object to update or create
|
||||||
* Take template as initial values for the form
|
* Take template as initial values for the form
|
||||||
@ -740,7 +816,9 @@ function AccountForm({
|
|||||||
}): VNode {
|
}): VNode {
|
||||||
const initial = initializeFromTemplate(template);
|
const initial = initializeFromTemplate(template);
|
||||||
const [form, setForm] = useState(initial);
|
const [form, setForm] = useState(initial);
|
||||||
const [errors, setErrors] = useState<typeof initial | undefined>(undefined);
|
const [errors, setErrors] = useState<
|
||||||
|
RecursivePartial<typeof initial> | undefined
|
||||||
|
>(undefined);
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
function updateForm(newForm: typeof initial): void {
|
function updateForm(newForm: typeof initial): void {
|
||||||
@ -748,7 +826,7 @@ function AccountForm({
|
|||||||
? undefined
|
? undefined
|
||||||
: parsePaytoUri(newForm.cashout_address);
|
: parsePaytoUri(newForm.cashout_address);
|
||||||
|
|
||||||
const validationResult = undefinedIfEmpty<typeof initial>({
|
const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
|
||||||
cashout_address: !newForm.cashout_address
|
cashout_address: !newForm.cashout_address
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: !parsed
|
: !parsed
|
||||||
@ -758,20 +836,20 @@ function AccountForm({
|
|||||||
: !IBAN_REGEX.test(parsed.iban)
|
: !IBAN_REGEX.test(parsed.iban)
|
||||||
? i18n.str`IBAN should have just uppercased letters and numbers`
|
? i18n.str`IBAN should have just uppercased letters and numbers`
|
||||||
: undefined,
|
: undefined,
|
||||||
contact_data: {
|
contact_data: undefinedIfEmpty({
|
||||||
email: !newForm.contact_data.email
|
email: !newForm.contact_data?.email
|
||||||
? undefined
|
? undefined
|
||||||
: !EMAIL_REGEX.test(newForm.contact_data.email)
|
: !EMAIL_REGEX.test(newForm.contact_data.email)
|
||||||
? i18n.str`it should be an email`
|
? i18n.str`it should be an email`
|
||||||
: undefined,
|
: undefined,
|
||||||
phone: !newForm.contact_data.phone
|
phone: !newForm.contact_data?.phone
|
||||||
? undefined
|
? undefined
|
||||||
: !newForm.contact_data.phone.startsWith("+")
|
: !newForm.contact_data.phone.startsWith("+")
|
||||||
? i18n.str`should start with +`
|
? i18n.str`should start with +`
|
||||||
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
|
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
|
||||||
? i18n.str`phone number can't have other than numbers`
|
? i18n.str`phone number can't have other than numbers`
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
}),
|
||||||
iban: !newForm.iban
|
iban: !newForm.iban
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: !IBAN_REGEX.test(newForm.iban)
|
: !IBAN_REGEX.test(newForm.iban)
|
||||||
@ -780,10 +858,9 @@ function AccountForm({
|
|||||||
name: !newForm.name ? i18n.str`required` : undefined,
|
name: !newForm.name ? i18n.str`required` : undefined,
|
||||||
username: !newForm.username ? i18n.str`required` : undefined,
|
username: !newForm.username ? i18n.str`required` : undefined,
|
||||||
});
|
});
|
||||||
|
setErrors(errors);
|
||||||
setErrors(validationResult);
|
|
||||||
setForm(newForm);
|
setForm(newForm);
|
||||||
onChange(validationResult === undefined ? undefined : (newForm as any));
|
onChange(errors === undefined ? (newForm as any) : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -846,7 +923,7 @@ function AccountForm({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ShowInputErrorLabel
|
<ShowInputErrorLabel
|
||||||
message={errors?.contact_data.email}
|
message={errors?.contact_data?.email}
|
||||||
isDirty={form.contact_data.email !== undefined}
|
isDirty={form.contact_data.email !== undefined}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -861,7 +938,7 @@ function AccountForm({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ShowInputErrorLabel
|
<ShowInputErrorLabel
|
||||||
message={errors?.contact_data.phone}
|
message={errors?.contact_data?.phone}
|
||||||
isDirty={form.contact_data?.phone !== undefined}
|
isDirty={form.contact_data?.phone !== undefined}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -883,29 +960,3 @@ function AccountForm({
|
|||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleError(
|
|
||||||
error: unknown,
|
|
||||||
saveError: (e: ErrorMessage) => void,
|
|
||||||
i18n: ReturnType<typeof useTranslationContext>["i18n"],
|
|
||||||
): void {
|
|
||||||
if (error instanceof RequestError) {
|
|
||||||
const payload = error.info.error as SandboxBackend.SandboxError;
|
|
||||||
saveError({
|
|
||||||
title: error.info.serverError
|
|
||||||
? i18n.str`Server had an error`
|
|
||||||
: i18n.str`Server didn't accept the request`,
|
|
||||||
description: payload.error.description,
|
|
||||||
});
|
|
||||||
} else if (error instanceof Error) {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Could not update account`,
|
|
||||||
description: error.message,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Error, please report`,
|
|
||||||
debug: JSON.stringify(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -126,14 +126,6 @@ export function BankFrame({
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<section id="main" class="content">
|
<section id="main" class="content">
|
||||||
{pageState.error && (
|
|
||||||
<ErrorBanner
|
|
||||||
error={pageState.error}
|
|
||||||
onClear={() => {
|
|
||||||
pageStateSetter((prev) => ({ ...prev, error: undefined }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<StatusBanner />
|
<StatusBanner />
|
||||||
{backend.state.status === "loggedIn" ? (
|
{backend.state.status === "loggedIn" ? (
|
||||||
<div class="top-right">
|
<div class="top-right">
|
||||||
@ -191,20 +183,48 @@ function maybeDemoContent(content: VNode): VNode {
|
|||||||
return <Fragment />;
|
return <Fragment />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBanner({
|
export function ErrorBannerFloat({
|
||||||
error,
|
error,
|
||||||
onClear,
|
onClear,
|
||||||
}: {
|
}: {
|
||||||
error: ErrorMessage;
|
error: ErrorMessage;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
}): VNode | null {
|
}): VNode {
|
||||||
return (
|
return (
|
||||||
<div class="informational informational-fail" style={{ marginTop: 8 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
zIndex: 200,
|
||||||
|
width: "90%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorBanner error={error} onClear={onClear} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ErrorBanner({
|
||||||
|
error,
|
||||||
|
onClear,
|
||||||
|
}: {
|
||||||
|
error: ErrorMessage;
|
||||||
|
onClear?: () => void;
|
||||||
|
}): VNode {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="informational informational-fail"
|
||||||
|
style={{
|
||||||
|
marginTop: 8,
|
||||||
|
paddingLeft: 16,
|
||||||
|
paddingRight: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<p>
|
<p>
|
||||||
<b>{error.title}</b>
|
<b>{error.title}</b>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div style={{ marginTop: "auto", marginBottom: "auto" }}>
|
||||||
{onClear && (
|
{onClear && (
|
||||||
<input
|
<input
|
||||||
type="button"
|
type="button"
|
||||||
@ -225,10 +245,21 @@ export function ErrorBanner({
|
|||||||
|
|
||||||
function StatusBanner(): VNode | null {
|
function StatusBanner(): VNode | null {
|
||||||
const { pageState, pageStateSetter } = usePageContext();
|
const { pageState, pageStateSetter } = usePageContext();
|
||||||
if (!pageState.info) return null;
|
|
||||||
|
|
||||||
const rval = (
|
return (
|
||||||
<div class="informational informational-ok" style={{ marginTop: 8 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
zIndex: 200,
|
||||||
|
width: "90%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!pageState.info ? undefined : (
|
||||||
|
<div
|
||||||
|
class="informational informational-ok"
|
||||||
|
style={{ marginTop: 8, paddingLeft: 16, paddingRight: 16 }}
|
||||||
|
>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<p>
|
<p>
|
||||||
<b>{pageState.info}</b>
|
<b>{pageState.info}</b>
|
||||||
@ -245,6 +276,15 @@ function StatusBanner(): VNode | null {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{!pageState.error ? undefined : (
|
||||||
|
<ErrorBanner
|
||||||
|
error={pageState.error}
|
||||||
|
onClear={() => {
|
||||||
|
pageStateSetter((prev) => ({ ...prev, error: undefined }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
return rval;
|
|
||||||
}
|
}
|
||||||
|
@ -20,13 +20,13 @@ import {
|
|||||||
TranslatedString,
|
TranslatedString,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
ErrorType,
|
HttpResponse,
|
||||||
HttpResponsePaginated,
|
HttpResponsePaginated,
|
||||||
RequestError,
|
RequestError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/lib/index.browser";
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { Cashouts } from "../components/Cashouts/index.js";
|
import { Cashouts } from "../components/Cashouts/index.js";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
import { ErrorMessage, usePageContext } from "../context/pageState.js";
|
import { ErrorMessage, usePageContext } from "../context/pageState.js";
|
||||||
@ -36,9 +36,13 @@ import {
|
|||||||
useCircuitAccountAPI,
|
useCircuitAccountAPI,
|
||||||
useRatiosAndFeeConfig,
|
useRatiosAndFeeConfig,
|
||||||
} from "../hooks/circuit.js";
|
} from "../hooks/circuit.js";
|
||||||
import { TanChannel, undefinedIfEmpty } from "../utils.js";
|
import {
|
||||||
|
buildRequestErrorMessage,
|
||||||
|
TanChannel,
|
||||||
|
undefinedIfEmpty,
|
||||||
|
} from "../utils.js";
|
||||||
import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
|
import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
|
||||||
import { ErrorBanner } from "./BankFrame.js";
|
import { ErrorBannerFloat } from "./BankFrame.js";
|
||||||
import { LoginForm } from "./LoginForm.js";
|
import { LoginForm } from "./LoginForm.js";
|
||||||
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
||||||
|
|
||||||
@ -177,6 +181,46 @@ type ErrorFrom<T> = {
|
|||||||
[P in keyof T]+?: string;
|
[P in keyof T]+?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// check #7719
|
||||||
|
function useRatiosAndFeeConfigWithChangeDetection(): HttpResponse<
|
||||||
|
SandboxBackend.Circuit.Config & { hasChanged?: boolean },
|
||||||
|
SandboxBackend.SandboxError
|
||||||
|
> {
|
||||||
|
const result = useRatiosAndFeeConfig();
|
||||||
|
const [oldResult, setOldResult] = useState<
|
||||||
|
SandboxBackend.Circuit.Config | undefined
|
||||||
|
>(undefined);
|
||||||
|
const dataFromBackend = result.ok ? result.data : undefined;
|
||||||
|
useEffect(() => {
|
||||||
|
// save only the first result of /config to the backend
|
||||||
|
if (!dataFromBackend || oldResult !== undefined) return;
|
||||||
|
setOldResult(dataFromBackend);
|
||||||
|
}, [dataFromBackend]);
|
||||||
|
|
||||||
|
if (!result.ok) return result;
|
||||||
|
|
||||||
|
const data = !oldResult ? result.data : oldResult;
|
||||||
|
const hasChanged =
|
||||||
|
oldResult &&
|
||||||
|
(result.data.name !== oldResult.name ||
|
||||||
|
result.data.version !== oldResult.version ||
|
||||||
|
result.data.ratios_and_fees.buy_at_ratio !==
|
||||||
|
oldResult.ratios_and_fees.buy_at_ratio ||
|
||||||
|
result.data.ratios_and_fees.buy_in_fee !==
|
||||||
|
oldResult.ratios_and_fees.buy_in_fee ||
|
||||||
|
result.data.ratios_and_fees.sell_at_ratio !==
|
||||||
|
oldResult.ratios_and_fees.sell_at_ratio ||
|
||||||
|
result.data.ratios_and_fees.sell_out_fee !==
|
||||||
|
oldResult.ratios_and_fees.sell_out_fee ||
|
||||||
|
result.data.ratios_and_fees.fiat_currency !==
|
||||||
|
oldResult.ratios_and_fees.fiat_currency);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
data: { ...data, hasChanged },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function CreateCashout({
|
function CreateCashout({
|
||||||
account,
|
account,
|
||||||
onComplete,
|
onComplete,
|
||||||
@ -207,15 +251,6 @@ function CreateCashout({
|
|||||||
|
|
||||||
if (!sellRate || sellRate < 0) return <div>error rate</div>;
|
if (!sellRate || sellRate < 0) return <div>error rate</div>;
|
||||||
|
|
||||||
function truncate(a: AmountJson): AmountJson {
|
|
||||||
const str = Amounts.stringify(a);
|
|
||||||
const idx = str.indexOf(".");
|
|
||||||
if (idx === -1) return a;
|
|
||||||
const truncated = str.substring(0, idx + 3);
|
|
||||||
console.log(str, truncated);
|
|
||||||
return Amounts.parseOrThrow(truncated);
|
|
||||||
}
|
|
||||||
|
|
||||||
const amount = Amounts.parse(`${balance.currency}:${form.amount}`);
|
const amount = Amounts.parse(`${balance.currency}:${form.amount}`);
|
||||||
const amount_debit = !amount
|
const amount_debit = !amount
|
||||||
? zero
|
? zero
|
||||||
@ -256,7 +291,7 @@ function CreateCashout({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{error && (
|
{error && (
|
||||||
<ErrorBanner error={error} onClear={() => saveError(undefined)} />
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
||||||
)}
|
)}
|
||||||
<h1>New cashout</h1>
|
<h1>New cashout</h1>
|
||||||
<form class="pure-form">
|
<form class="pure-form">
|
||||||
@ -555,74 +590,31 @@ function CreateCashout({
|
|||||||
onComplete(res.data.uuid);
|
onComplete(res.data.uuid);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
const e = error as RequestError<SandboxBackend.SandboxError>;
|
saveError(
|
||||||
switch (e.cause.type) {
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
case ErrorType.TIMEOUT: {
|
onClientError: (status) =>
|
||||||
saveError({
|
status === HttpStatusCode.BadRequest
|
||||||
title: i18n.str`Request timeout, try again later.`,
|
? i18n.str`The exchange rate was incorrectly applied`
|
||||||
});
|
: status === HttpStatusCode.Forbidden
|
||||||
break;
|
? i18n.str`A institutional user tried the operation`
|
||||||
}
|
: status === HttpStatusCode.Conflict
|
||||||
case ErrorType.CLIENT: {
|
? i18n.str`Need a contact data where to send the TAN`
|
||||||
const errorData = e.cause.error;
|
: status === HttpStatusCode.PreconditionFailed
|
||||||
|
? i18n.str`The account does not have sufficient funds`
|
||||||
if (
|
: undefined,
|
||||||
e.cause.status === HttpStatusCode.PreconditionFailed
|
onServerError: (status) =>
|
||||||
) {
|
status === HttpStatusCode.ServiceUnavailable
|
||||||
saveError({
|
? i18n.str`The bank does not support the TAN channel for this operation`
|
||||||
title: i18n.str`The account does not have sufficient funds`,
|
: undefined,
|
||||||
description: errorData.error.description,
|
}),
|
||||||
debug: JSON.stringify(error.info),
|
);
|
||||||
});
|
|
||||||
} else if (e.cause.status === HttpStatusCode.Conflict) {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`No contact information for this channel`,
|
|
||||||
description: errorData.error.description,
|
|
||||||
debug: JSON.stringify(error.info),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
saveError({
|
saveError({
|
||||||
title: i18n.str`New cashout gave response error`,
|
title: i18n.str`Operation failed, please report`,
|
||||||
description: errorData.error.description,
|
description:
|
||||||
debug: JSON.stringify(error.info),
|
error instanceof Error
|
||||||
});
|
? error.message
|
||||||
}
|
: JSON.stringify(error),
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorType.SERVER: {
|
|
||||||
const errorData = e.cause.error;
|
|
||||||
if (
|
|
||||||
e.cause.status === HttpStatusCode.ServiceUnavailable
|
|
||||||
) {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`The bank does not support the TAN channel for this operation`,
|
|
||||||
description: errorData.error.description,
|
|
||||||
debug: JSON.stringify(error.info),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Creating cashout returned with a server error`,
|
|
||||||
description: errorData.error.description,
|
|
||||||
debug: JSON.stringify(error.cause),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorType.UNEXPECTED: {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Unexpected error trying to create cashout.`,
|
|
||||||
debug: JSON.stringify(error.cause),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
assertUnreachable(e.cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (error instanceof Error) {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Cashout failed, please report`,
|
|
||||||
description: error.message,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -636,6 +628,25 @@ function CreateCashout({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_AMOUNT_DIGIT = 2;
|
||||||
|
/**
|
||||||
|
* Truncate the amount of digits to display
|
||||||
|
* in the form based on the fee calculations
|
||||||
|
*
|
||||||
|
* Backend must have the same truncation
|
||||||
|
* @param a
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function truncate(a: AmountJson): AmountJson {
|
||||||
|
const str = Amounts.stringify(a);
|
||||||
|
const idx = str.indexOf(".");
|
||||||
|
if (idx === -1) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
const truncated = str.substring(0, idx + 1 + MAX_AMOUNT_DIGIT);
|
||||||
|
return Amounts.parseOrThrow(truncated);
|
||||||
|
}
|
||||||
|
|
||||||
interface ShowCashoutProps {
|
interface ShowCashoutProps {
|
||||||
id: string;
|
id: string;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@ -662,7 +673,7 @@ export function ShowCashoutDetails({
|
|||||||
<div>
|
<div>
|
||||||
<h1>Cashout details {id}</h1>
|
<h1>Cashout details {id}</h1>
|
||||||
{error && (
|
{error && (
|
||||||
<ErrorBanner error={error} onClear={() => saveError(undefined)} />
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
||||||
)}
|
)}
|
||||||
<form class="pure-form">
|
<form class="pure-form">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@ -744,68 +755,27 @@ export function ShowCashoutDetails({
|
|||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const rest = await abortCashout(id);
|
await abortCashout(id);
|
||||||
onCancel();
|
onCancel();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
const e =
|
saveError(
|
||||||
error as RequestError<SandboxBackend.SandboxError>;
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
switch (e.cause.type) {
|
onClientError: (status) =>
|
||||||
case ErrorType.TIMEOUT: {
|
status === HttpStatusCode.NotFound
|
||||||
saveError({
|
? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
|
||||||
title: i18n.str`Request timeout, try again later.`,
|
: status === HttpStatusCode.PreconditionFailed
|
||||||
});
|
? i18n.str`Cashout was already confimed`
|
||||||
break;
|
: undefined,
|
||||||
}
|
}),
|
||||||
case ErrorType.CLIENT: {
|
);
|
||||||
const errorData = e.cause.error;
|
|
||||||
if (
|
|
||||||
e.cause.status === HttpStatusCode.PreconditionFailed
|
|
||||||
) {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Cashout was already aborted`,
|
|
||||||
description: errorData.error.description,
|
|
||||||
debug: JSON.stringify(error.info),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
saveError({
|
saveError({
|
||||||
title: i18n.str`Aborting cashout gave response error`,
|
title: i18n.str`Operation failed, please report`,
|
||||||
description: errorData.error.description,
|
description:
|
||||||
debug: JSON.stringify(error.info),
|
error instanceof Error
|
||||||
});
|
? error.message
|
||||||
}
|
: JSON.stringify(error),
|
||||||
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Aborting cashout gave response error`,
|
|
||||||
description: errorData.error.description,
|
|
||||||
debug: JSON.stringify(error.cause),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorType.SERVER: {
|
|
||||||
const errorData = e.cause.error;
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Aborting cashout returned with a server error`,
|
|
||||||
description: errorData.error.description,
|
|
||||||
debug: JSON.stringify(error.cause),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorType.UNEXPECTED: {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Unexpected error trying to abort cashout.`,
|
|
||||||
debug: JSON.stringify(error.cause),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
assertUnreachable(e.cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (error instanceof Error) {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Aborting failed, please report`,
|
|
||||||
description: error.message,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -827,48 +797,27 @@ export function ShowCashoutDetails({
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
const e =
|
saveError(
|
||||||
error as RequestError<SandboxBackend.SandboxError>;
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
switch (e.cause.type) {
|
onClientError: (status) =>
|
||||||
case ErrorType.TIMEOUT: {
|
status === HttpStatusCode.NotFound
|
||||||
|
? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
|
||||||
|
: status === HttpStatusCode.PreconditionFailed
|
||||||
|
? i18n.str`Cashout was already confimed`
|
||||||
|
: status === HttpStatusCode.Conflict
|
||||||
|
? i18n.str`Confirmation failed. Maybe the user changed their cash-out address between the creation and the confirmation`
|
||||||
|
: status === HttpStatusCode.Forbidden
|
||||||
|
? i18n.str`Invalid code`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
saveError({
|
saveError({
|
||||||
title: i18n.str`Request timeout, try again later.`,
|
title: i18n.str`Operation failed, please report`,
|
||||||
});
|
description:
|
||||||
break;
|
error instanceof Error
|
||||||
}
|
? error.message
|
||||||
case ErrorType.CLIENT: {
|
: JSON.stringify(error),
|
||||||
const errorData = e.cause.error;
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Confirmation of cashout gave response error`,
|
|
||||||
description: errorData.error.description,
|
|
||||||
debug: JSON.stringify(error.cause),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorType.SERVER: {
|
|
||||||
const errorData = e.cause.error;
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Confirmation of cashout gave response error`,
|
|
||||||
description: errorData.error.description,
|
|
||||||
debug: JSON.stringify(error.cause),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorType.UNEXPECTED: {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Unexpected error trying to cashout.`,
|
|
||||||
debug: JSON.stringify(error.cause),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
assertUnreachable(e.cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (error instanceof Error) {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Confirmation failed, please report`,
|
|
||||||
description: error.message,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,10 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
ErrorType,
|
ErrorType,
|
||||||
HttpResponsePaginated,
|
HttpResponsePaginated,
|
||||||
RequestError,
|
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/lib/index.browser";
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
@ -79,7 +78,27 @@ export function HomePage({ onRegister }: { onRegister: () => void }): VNode {
|
|||||||
account={backend.state.username}
|
account={backend.state.username}
|
||||||
withdrawalId={withdrawalId}
|
withdrawalId={withdrawalId}
|
||||||
talerWithdrawUri={talerWithdrawUri}
|
talerWithdrawUri={talerWithdrawUri}
|
||||||
onAbort={clearCurrentWithdrawal}
|
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(
|
onLoadNotOk={handleNotOkResult(
|
||||||
backend.state.username,
|
backend.state.username,
|
||||||
saveError,
|
saveError,
|
||||||
@ -147,7 +166,7 @@ function handleNotOkResult(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ErrorType.CLIENT: {
|
case ErrorType.CLIENT: {
|
||||||
const errorData = result.error;
|
const errorData = result.payload;
|
||||||
onErrorHandler({
|
onErrorHandler({
|
||||||
title: i18n.str`Could not load due to a client error`,
|
title: i18n.str`Could not load due to a client error`,
|
||||||
description: errorData.error.description,
|
description: errorData.error.description,
|
||||||
@ -168,7 +187,7 @@ function handleNotOkResult(
|
|||||||
onErrorHandler({
|
onErrorHandler({
|
||||||
title: i18n.str`Unexpected error.`,
|
title: i18n.str`Unexpected error.`,
|
||||||
description: `Diagnostic from ${result.info?.url} is "${result.message}"`,
|
description: `Diagnostic from ${result.info?.url} is "${result.message}"`,
|
||||||
debug: JSON.stringify(result.error),
|
debug: JSON.stringify(result.exception),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,12 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
|
import { PageStateType, usePageContext } from "../context/pageState.js";
|
||||||
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
|
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
|
||||||
import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
|
import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
|
||||||
import { PageStateType, usePageContext } from "../context/pageState.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Let the user choose a payment option,
|
* Let the user choose a payment option,
|
||||||
|
@ -17,22 +17,20 @@
|
|||||||
import {
|
import {
|
||||||
Amounts,
|
Amounts,
|
||||||
buildPayto,
|
buildPayto,
|
||||||
|
HttpStatusCode,
|
||||||
Logger,
|
Logger,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
stringifyPaytoUri,
|
stringifyPaytoUri,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
InternationalizationAPI,
|
|
||||||
RequestError,
|
RequestError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/lib/index.browser";
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { PageStateType } from "../context/pageState.js";
|
||||||
import { PageStateType, usePageContext } from "../context/pageState.js";
|
|
||||||
import { useAccessAPI } from "../hooks/access.js";
|
import { useAccessAPI } from "../hooks/access.js";
|
||||||
import { BackendState } from "../hooks/backend.js";
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
||||||
import { undefinedIfEmpty } from "../utils.js";
|
|
||||||
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
||||||
|
|
||||||
const logger = new Logger("PaytoWireTransferForm");
|
const logger = new Logger("PaytoWireTransferForm");
|
||||||
@ -184,11 +182,35 @@ export function PaytoWireTransferForm({
|
|||||||
ibanPayto.params.message = encodeURIComponent(subject);
|
ibanPayto.params.message = encodeURIComponent(subject);
|
||||||
const paytoUri = stringifyPaytoUri(ibanPayto);
|
const paytoUri = stringifyPaytoUri(ibanPayto);
|
||||||
|
|
||||||
|
try {
|
||||||
await createTransaction({
|
await createTransaction({
|
||||||
paytoUri,
|
paytoUri,
|
||||||
amount: `${currency}:${amount}`,
|
amount: `${currency}:${amount}`,
|
||||||
});
|
});
|
||||||
onSuccess();
|
onSuccess();
|
||||||
|
setAmount(undefined);
|
||||||
|
setIban(undefined);
|
||||||
|
setSubject(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
onError(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.BadRequest
|
||||||
|
? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onError({
|
||||||
|
title: i18n.str`Operation failed, please report`,
|
||||||
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@ -298,13 +320,21 @@ export function PaytoWireTransferForm({
|
|||||||
rawPaytoInputSetter(undefined);
|
rawPaytoInputSetter(undefined);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
const errorData: SandboxBackend.SandboxError =
|
onError(
|
||||||
error.info.error;
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.BadRequest
|
||||||
|
? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
onError({
|
onError({
|
||||||
title: i18n.str`Transfer creation gave response error`,
|
title: i18n.str`Operation failed, please report`,
|
||||||
description: errorData.error.description,
|
description:
|
||||||
debug: JSON.stringify(errorData),
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,17 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
import { QR } from "../components/QR.js";
|
import { QR } from "../components/QR.js";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
|
|
||||||
|
|
||||||
export function QrCodeSection({
|
export function QrCodeSection({
|
||||||
talerWithdrawUri,
|
talerWithdrawUri,
|
||||||
onAbort,
|
onAborted,
|
||||||
}: {
|
}: {
|
||||||
talerWithdrawUri: string;
|
talerWithdrawUri: string;
|
||||||
onAbort: () => void;
|
onAborted: () => void;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -64,7 +64,7 @@ export function QrCodeSection({
|
|||||||
<br />
|
<br />
|
||||||
<a
|
<a
|
||||||
class="pure-button btn-cancel"
|
class="pure-button btn-cancel"
|
||||||
onClick={onAbort}
|
onClick={onAborted}
|
||||||
>{i18n.str`Abort`}</a>
|
>{i18n.str`Abort`}</a>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
|
import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
ErrorType,
|
|
||||||
RequestError,
|
RequestError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/lib/index.browser";
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
@ -25,7 +24,7 @@ import { useBackendContext } from "../context/backend.js";
|
|||||||
import { PageStateType } from "../context/pageState.js";
|
import { PageStateType } from "../context/pageState.js";
|
||||||
import { useTestingAPI } from "../hooks/access.js";
|
import { useTestingAPI } from "../hooks/access.js";
|
||||||
import { bankUiSettings } from "../settings.js";
|
import { bankUiSettings } from "../settings.js";
|
||||||
import { undefinedIfEmpty } from "../utils.js";
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
||||||
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
||||||
|
|
||||||
const logger = new Logger("RegistrationPage");
|
const logger = new Logger("RegistrationPage");
|
||||||
@ -177,53 +176,23 @@ function RegistrationForm({
|
|||||||
onComplete();
|
onComplete();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
const e =
|
onError(
|
||||||
error as RequestError<SandboxBackend.SandboxError>;
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
switch (e.cause.type) {
|
onClientError: (status) =>
|
||||||
case ErrorType.TIMEOUT: {
|
status === HttpStatusCode.Conflict
|
||||||
onError({
|
? i18n.str`That username is already taken`
|
||||||
title: i18n.str`Request timeout, try again later.`,
|
: undefined,
|
||||||
});
|
}),
|
||||||
break;
|
);
|
||||||
}
|
|
||||||
case ErrorType.CLIENT: {
|
|
||||||
const errorData = e.cause.error;
|
|
||||||
if (e.cause.status === HttpStatusCode.Conflict) {
|
|
||||||
onError({
|
|
||||||
title: i18n.str`That username is already taken`,
|
|
||||||
description: errorData.error.description,
|
|
||||||
debug: JSON.stringify(error.cause),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
onError({
|
onError({
|
||||||
title: i18n.str`New registration gave response error`,
|
title: i18n.str`Operation failed, please report`,
|
||||||
description: errorData.error.description,
|
description:
|
||||||
debug: JSON.stringify(error.cause),
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorType.SERVER: {
|
|
||||||
const errorData = e.cause.error;
|
|
||||||
onError({
|
|
||||||
title: i18n.str`New registration gave response error`,
|
|
||||||
description: errorData?.error?.description,
|
|
||||||
debug: JSON.stringify(error.cause),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorType.UNEXPECTED: {
|
|
||||||
onError({
|
|
||||||
title: i18n.str`Unexpected error doing the registration.`,
|
|
||||||
debug: JSON.stringify(error.cause),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
assertUnreachable(e.cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -14,16 +14,16 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Amounts, Logger } from "@gnu-taler/taler-util";
|
import { Amounts, HttpStatusCode, Logger } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
RequestError,
|
RequestError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/lib/index.browser";
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { PageStateType, usePageContext } from "../context/pageState.js";
|
import { PageStateType } from "../context/pageState.js";
|
||||||
import { useAccessAPI } from "../hooks/access.js";
|
import { useAccessAPI } from "../hooks/access.js";
|
||||||
import { undefinedIfEmpty } from "../utils.js";
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
||||||
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
||||||
|
|
||||||
const logger = new Logger("WalletWithdrawForm");
|
const logger = new Logger("WalletWithdrawForm");
|
||||||
@ -127,16 +127,21 @@ export function WalletWithdrawForm({
|
|||||||
onSuccess(result.data);
|
onSuccess(result.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
|
onError(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Forbidden
|
||||||
|
? i18n.str`The operation was rejected due to insufficient funds`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
onError({
|
onError({
|
||||||
title: i18n.str`Could not create withdrawal operation`,
|
title: i18n.str`Operation failed, please report`,
|
||||||
description: (error as any).error.description,
|
description:
|
||||||
debug: JSON.stringify(error),
|
error instanceof Error
|
||||||
});
|
? error.message
|
||||||
}
|
: JSON.stringify(error),
|
||||||
if (error instanceof Error) {
|
|
||||||
onError({
|
|
||||||
title: i18n.str`Something when wrong trying to start the withdrawal`,
|
|
||||||
description: error.message,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,85 +152,3 @@ export function WalletWithdrawForm({
|
|||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
|
||||||
// * This function creates a withdrawal operation via the Access API.
|
|
||||||
// *
|
|
||||||
// * After having successfully created the withdrawal operation, the
|
|
||||||
// * user should receive a QR code of the "taler://withdraw/" type and
|
|
||||||
// * supposed to scan it with their phone.
|
|
||||||
// *
|
|
||||||
// * TODO: (1) after the scan, the page should refresh itself and inform
|
|
||||||
// * the user about the operation's outcome. (2) use POST helper. */
|
|
||||||
// async function createWithdrawalCall(
|
|
||||||
// amount: string,
|
|
||||||
// backendState: BackendState,
|
|
||||||
// pageStateSetter: StateUpdater<PageStateType>,
|
|
||||||
// i18n: InternationalizationAPI,
|
|
||||||
// ): Promise<void> {
|
|
||||||
// if (backendState?.status === "loggedOut") {
|
|
||||||
// logger.error("Page has a problem: no credentials found in the state.");
|
|
||||||
// pageStateSetter((prevState) => ({
|
|
||||||
// ...prevState,
|
|
||||||
|
|
||||||
// error: {
|
|
||||||
// title: i18n.str`No credentials given.`,
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let res: Response;
|
|
||||||
// try {
|
|
||||||
// const { username, password } = backendState;
|
|
||||||
// const headers = prepareHeaders(username, password);
|
|
||||||
|
|
||||||
// // Let bank generate withdraw URI:
|
|
||||||
// const url = new URL(
|
|
||||||
// `access-api/accounts/${backendState.username}/withdrawals`,
|
|
||||||
// backendState.url,
|
|
||||||
// );
|
|
||||||
// res = await fetch(url.href, {
|
|
||||||
// method: "POST",
|
|
||||||
// headers,
|
|
||||||
// body: JSON.stringify({ amount }),
|
|
||||||
// });
|
|
||||||
// } catch (error) {
|
|
||||||
// logger.trace("Could not POST withdrawal request to the bank", error);
|
|
||||||
// pageStateSetter((prevState) => ({
|
|
||||||
// ...prevState,
|
|
||||||
|
|
||||||
// error: {
|
|
||||||
// title: i18n.str`Could not create withdrawal operation`,
|
|
||||||
// description: (error as any).error.description,
|
|
||||||
// debug: JSON.stringify(error),
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (!res.ok) {
|
|
||||||
// const response = await res.json();
|
|
||||||
// logger.error(
|
|
||||||
// `Withdrawal creation gave response error: ${response} (${res.status})`,
|
|
||||||
// );
|
|
||||||
// pageStateSetter((prevState) => ({
|
|
||||||
// ...prevState,
|
|
||||||
|
|
||||||
// error: {
|
|
||||||
// title: i18n.str`Withdrawal creation gave response error`,
|
|
||||||
// description: response.error.description,
|
|
||||||
// debug: JSON.stringify(response),
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// logger.trace("Withdrawal operation created!");
|
|
||||||
// const resp = await res.json();
|
|
||||||
// pageStateSetter((prevState: PageStateType) => ({
|
|
||||||
// ...prevState,
|
|
||||||
// withdrawalInProgress: true,
|
|
||||||
// talerWithdrawUri: resp.taler_withdraw_uri,
|
|
||||||
// withdrawalId: resp.withdrawal_id,
|
|
||||||
// }));
|
|
||||||
// }
|
|
||||||
|
@ -14,32 +14,36 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
|
import {
|
||||||
|
RequestError,
|
||||||
|
useTranslationContext,
|
||||||
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useMemo, useState } from "preact/hooks";
|
import { useMemo, useState } from "preact/hooks";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { PageStateType, usePageContext } from "../context/pageState.js";
|
||||||
import { usePageContext } from "../context/pageState.js";
|
|
||||||
import { useAccessAPI } from "../hooks/access.js";
|
import { useAccessAPI } from "../hooks/access.js";
|
||||||
import { undefinedIfEmpty } from "../utils.js";
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
||||||
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
||||||
|
|
||||||
const logger = new Logger("WithdrawalConfirmationQuestion");
|
const logger = new Logger("WithdrawalConfirmationQuestion");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: string;
|
|
||||||
withdrawalId: string;
|
withdrawalId: string;
|
||||||
|
onError: (e: PageStateType["error"]) => void;
|
||||||
|
onConfirmed: () => void;
|
||||||
|
onAborted: () => void;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Additional authentication required to complete the operation.
|
* Additional authentication required to complete the operation.
|
||||||
* Not providing a back button, only abort.
|
* Not providing a back button, only abort.
|
||||||
*/
|
*/
|
||||||
export function WithdrawalConfirmationQuestion({
|
export function WithdrawalConfirmationQuestion({
|
||||||
account,
|
onError,
|
||||||
|
onConfirmed,
|
||||||
|
onAborted,
|
||||||
withdrawalId,
|
withdrawalId,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const { pageState, pageStateSetter } = usePageContext();
|
|
||||||
const backend = useBackendContext();
|
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
const captchaNumbers = useMemo(() => {
|
const captchaNumbers = useMemo(() => {
|
||||||
@ -111,35 +115,29 @@ export function WithdrawalConfirmationQuestion({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
await confirmWithdrawal(withdrawalId);
|
await confirmWithdrawal(withdrawalId);
|
||||||
pageStateSetter((prevState) => {
|
onConfirmed();
|
||||||
const { talerWithdrawUri, ...rest } = prevState;
|
|
||||||
return {
|
|
||||||
...rest,
|
|
||||||
info: i18n.str`Withdrawal confirmed!`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
pageStateSetter((prevState) => ({
|
if (error instanceof RequestError) {
|
||||||
...prevState,
|
onError(
|
||||||
error: {
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
title: i18n.str`Could not confirm the withdrawal`,
|
onClientError: (status) =>
|
||||||
description: (error as any).error.description,
|
status === HttpStatusCode.Conflict
|
||||||
debug: JSON.stringify(error),
|
? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
|
||||||
},
|
: status === HttpStatusCode.UnprocessableEntity
|
||||||
}));
|
? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onError({
|
||||||
|
title: i18n.str`Operation failed, please report`,
|
||||||
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// if (
|
|
||||||
// captchaAnswer ==
|
|
||||||
// (captchaNumbers.a + captchaNumbers.b).toString()
|
|
||||||
// ) {
|
|
||||||
// await confirmWithdrawalCall(
|
|
||||||
// backend.state,
|
|
||||||
// pageState.withdrawalId,
|
|
||||||
// pageStateSetter,
|
|
||||||
// i18n,
|
|
||||||
// );
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{i18n.str`Confirm`}
|
{i18n.str`Confirm`}
|
||||||
@ -151,29 +149,27 @@ export function WithdrawalConfirmationQuestion({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
await abortWithdrawal(withdrawalId);
|
await abortWithdrawal(withdrawalId);
|
||||||
pageStateSetter((prevState) => {
|
onAborted();
|
||||||
const { talerWithdrawUri, ...rest } = prevState;
|
|
||||||
return {
|
|
||||||
...rest,
|
|
||||||
info: i18n.str`Withdrawal confirmed!`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
pageStateSetter((prevState) => ({
|
if (error instanceof RequestError) {
|
||||||
...prevState,
|
onError(
|
||||||
error: {
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
title: i18n.str`Could not confirm the withdrawal`,
|
onClientError: (status) =>
|
||||||
description: (error as any).error.description,
|
status === HttpStatusCode.Conflict
|
||||||
debug: JSON.stringify(error),
|
? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
|
||||||
},
|
: undefined,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onError({
|
||||||
|
title: i18n.str`Operation failed, please report`,
|
||||||
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// await abortWithdrawalCall(
|
|
||||||
// backend.state,
|
|
||||||
// pageState.withdrawalId,
|
|
||||||
// pageStateSetter,
|
|
||||||
// i18n,
|
|
||||||
// );
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{i18n.str`Cancel`}
|
{i18n.str`Cancel`}
|
||||||
@ -195,199 +191,3 @@ export function WithdrawalConfirmationQuestion({
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function confirms a withdrawal operation AFTER
|
|
||||||
* the wallet has given the exchange's payment details
|
|
||||||
* to the bank (via the Integration API). Such details
|
|
||||||
* can be given by scanning a QR code or by passing the
|
|
||||||
* raw taler://withdraw-URI to the CLI wallet.
|
|
||||||
*
|
|
||||||
* This function will set the confirmation status in the
|
|
||||||
* 'page state' and let the related components refresh.
|
|
||||||
*/
|
|
||||||
// async function confirmWithdrawalCall(
|
|
||||||
// backendState: BackendState,
|
|
||||||
// withdrawalId: string | undefined,
|
|
||||||
// pageStateSetter: StateUpdater<PageStateType>,
|
|
||||||
// i18n: InternationalizationAPI,
|
|
||||||
// ): Promise<void> {
|
|
||||||
// if (backendState.status === "loggedOut") {
|
|
||||||
// logger.error("No credentials found.");
|
|
||||||
// pageStateSetter((prevState) => ({
|
|
||||||
// ...prevState,
|
|
||||||
|
|
||||||
// error: {
|
|
||||||
// title: i18n.str`No credentials found.`,
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (typeof withdrawalId === "undefined") {
|
|
||||||
// logger.error("No withdrawal ID found.");
|
|
||||||
// pageStateSetter((prevState) => ({
|
|
||||||
// ...prevState,
|
|
||||||
|
|
||||||
// error: {
|
|
||||||
// title: i18n.str`No withdrawal ID found.`,
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// let res: Response;
|
|
||||||
// try {
|
|
||||||
// const { username, password } = backendState;
|
|
||||||
// const headers = prepareHeaders(username, password);
|
|
||||||
// /**
|
|
||||||
// * NOTE: tests show that when a same object is being
|
|
||||||
// * POSTed, caching might prevent same requests from being
|
|
||||||
// * made. Hence, trying to POST twice the same amount might
|
|
||||||
// * get silently ignored.
|
|
||||||
// *
|
|
||||||
// * headers.append("cache-control", "no-store");
|
|
||||||
// * headers.append("cache-control", "no-cache");
|
|
||||||
// * headers.append("pragma", "no-cache");
|
|
||||||
// * */
|
|
||||||
|
|
||||||
// // Backend URL must have been stored _with_ a final slash.
|
|
||||||
// const url = new URL(
|
|
||||||
// `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/confirm`,
|
|
||||||
// backendState.url,
|
|
||||||
// );
|
|
||||||
// res = await fetch(url.href, {
|
|
||||||
// method: "POST",
|
|
||||||
// headers,
|
|
||||||
// });
|
|
||||||
// } catch (error) {
|
|
||||||
// logger.error("Could not POST withdrawal confirmation to the bank", error);
|
|
||||||
// pageStateSetter((prevState) => ({
|
|
||||||
// ...prevState,
|
|
||||||
|
|
||||||
// error: {
|
|
||||||
// title: i18n.str`Could not confirm the withdrawal`,
|
|
||||||
// description: (error as any).error.description,
|
|
||||||
// debug: JSON.stringify(error),
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (!res || !res.ok) {
|
|
||||||
// const response = await res.json();
|
|
||||||
// // assume not ok if res is null
|
|
||||||
// logger.error(
|
|
||||||
// `Withdrawal confirmation gave response error (${res.status})`,
|
|
||||||
// res.statusText,
|
|
||||||
// );
|
|
||||||
// pageStateSetter((prevState) => ({
|
|
||||||
// ...prevState,
|
|
||||||
|
|
||||||
// error: {
|
|
||||||
// title: i18n.str`Withdrawal confirmation gave response error`,
|
|
||||||
// debug: JSON.stringify(response),
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// logger.trace("Withdrawal operation confirmed!");
|
|
||||||
// pageStateSetter((prevState) => {
|
|
||||||
// const { talerWithdrawUri, ...rest } = prevState;
|
|
||||||
// return {
|
|
||||||
// ...rest,
|
|
||||||
|
|
||||||
// info: i18n.str`Withdrawal confirmed!`,
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Abort a withdrawal operation via the Access API's /abort.
|
|
||||||
// */
|
|
||||||
// async function abortWithdrawalCall(
|
|
||||||
// backendState: BackendState,
|
|
||||||
// withdrawalId: string | undefined,
|
|
||||||
// pageStateSetter: StateUpdater<PageStateType>,
|
|
||||||
// i18n: InternationalizationAPI,
|
|
||||||
// ): Promise<void> {
|
|
||||||
// if (backendState.status === "loggedOut") {
|
|
||||||
// logger.error("No credentials found.");
|
|
||||||
// pageStateSetter((prevState) => ({
|
|
||||||
// ...prevState,
|
|
||||||
|
|
||||||
// error: {
|
|
||||||
// title: i18n.str`No credentials found.`,
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (typeof withdrawalId === "undefined") {
|
|
||||||
// logger.error("No withdrawal ID found.");
|
|
||||||
// pageStateSetter((prevState) => ({
|
|
||||||
// ...prevState,
|
|
||||||
|
|
||||||
// error: {
|
|
||||||
// title: i18n.str`No withdrawal ID found.`,
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// let res: Response;
|
|
||||||
// try {
|
|
||||||
// const { username, password } = backendState;
|
|
||||||
// const headers = prepareHeaders(username, password);
|
|
||||||
// /**
|
|
||||||
// * NOTE: tests show that when a same object is being
|
|
||||||
// * POSTed, caching might prevent same requests from being
|
|
||||||
// * made. Hence, trying to POST twice the same amount might
|
|
||||||
// * get silently ignored. Needs more observation!
|
|
||||||
// *
|
|
||||||
// * headers.append("cache-control", "no-store");
|
|
||||||
// * headers.append("cache-control", "no-cache");
|
|
||||||
// * headers.append("pragma", "no-cache");
|
|
||||||
// * */
|
|
||||||
|
|
||||||
// // Backend URL must have been stored _with_ a final slash.
|
|
||||||
// const url = new URL(
|
|
||||||
// `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/abort`,
|
|
||||||
// backendState.url,
|
|
||||||
// );
|
|
||||||
// res = await fetch(url.href, { method: "POST", headers });
|
|
||||||
// } catch (error) {
|
|
||||||
// logger.error("Could not abort the withdrawal", error);
|
|
||||||
// pageStateSetter((prevState) => ({
|
|
||||||
// ...prevState,
|
|
||||||
|
|
||||||
// error: {
|
|
||||||
// title: i18n.str`Could not abort the withdrawal.`,
|
|
||||||
// description: (error as any).error.description,
|
|
||||||
// debug: JSON.stringify(error),
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (!res.ok) {
|
|
||||||
// const response = await res.json();
|
|
||||||
// logger.error(
|
|
||||||
// `Withdrawal abort gave response error (${res.status})`,
|
|
||||||
// res.statusText,
|
|
||||||
// );
|
|
||||||
// pageStateSetter((prevState) => ({
|
|
||||||
// ...prevState,
|
|
||||||
|
|
||||||
// error: {
|
|
||||||
// title: i18n.str`Withdrawal abortion failed.`,
|
|
||||||
// description: response.error.description,
|
|
||||||
// debug: JSON.stringify(response),
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// logger.trace("Withdrawal operation aborted!");
|
|
||||||
// pageStateSetter((prevState) => {
|
|
||||||
// const { ...rest } = prevState;
|
|
||||||
// return {
|
|
||||||
// ...rest,
|
|
||||||
|
|
||||||
// info: i18n.str`Withdrawal aborted!`,
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
} from "@gnu-taler/web-util/lib/index.browser";
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { Loading } from "../components/Loading.js";
|
import { Loading } from "../components/Loading.js";
|
||||||
import { usePageContext } from "../context/pageState.js";
|
import { PageStateType } from "../context/pageState.js";
|
||||||
import { useWithdrawalDetails } from "../hooks/access.js";
|
import { useWithdrawalDetails } from "../hooks/access.js";
|
||||||
import { QrCodeSection } from "./QrCodeSection.js";
|
import { QrCodeSection } from "./QrCodeSection.js";
|
||||||
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
|
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
|
||||||
@ -32,7 +32,9 @@ interface Props {
|
|||||||
account: string;
|
account: string;
|
||||||
withdrawalId: string;
|
withdrawalId: string;
|
||||||
talerWithdrawUri: string;
|
talerWithdrawUri: string;
|
||||||
onAbort: () => void;
|
onError: (e: PageStateType["error"]) => void;
|
||||||
|
onAborted: () => void;
|
||||||
|
onConfirmed: () => void;
|
||||||
onLoadNotOk: <T>(
|
onLoadNotOk: <T>(
|
||||||
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
||||||
) => VNode;
|
) => VNode;
|
||||||
@ -46,10 +48,12 @@ export function WithdrawalQRCode({
|
|||||||
account,
|
account,
|
||||||
withdrawalId,
|
withdrawalId,
|
||||||
talerWithdrawUri,
|
talerWithdrawUri,
|
||||||
onAbort,
|
onConfirmed,
|
||||||
|
onAborted,
|
||||||
|
onError,
|
||||||
onLoadNotOk,
|
onLoadNotOk,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
logger.trace(`Showing withdraw URI: ${talerWithdrawUri}`);
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
const result = useWithdrawalDetails(account, withdrawalId);
|
const result = useWithdrawalDetails(account, withdrawalId);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
@ -61,18 +65,24 @@ export function WithdrawalQRCode({
|
|||||||
if (data.aborted) {
|
if (data.aborted) {
|
||||||
// signal that this withdrawal is aborted
|
// signal that this withdrawal is aborted
|
||||||
// will redirect to account info
|
// will redirect to account info
|
||||||
onAbort();
|
onAborted();
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedUri = parseWithdrawUri(talerWithdrawUri);
|
const parsedUri = parseWithdrawUri(talerWithdrawUri);
|
||||||
if (!parsedUri) {
|
if (!parsedUri) {
|
||||||
throw Error("can't parse withdrawal URI");
|
onError({
|
||||||
|
title: i18n.str`The Withdrawal URI is not valid: "${talerWithdrawUri}"`,
|
||||||
|
});
|
||||||
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.selection_done) {
|
if (!data.selection_done) {
|
||||||
return (
|
return (
|
||||||
<QrCodeSection talerWithdrawUri={talerWithdrawUri} onAbort={onAbort} />
|
<QrCodeSection
|
||||||
|
talerWithdrawUri={talerWithdrawUri}
|
||||||
|
onAborted={onAborted}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,8 +90,10 @@ export function WithdrawalQRCode({
|
|||||||
// user to authorize the operation (here CAPTCHA).
|
// user to authorize the operation (here CAPTCHA).
|
||||||
return (
|
return (
|
||||||
<WithdrawalConfirmationQuestion
|
<WithdrawalConfirmationQuestion
|
||||||
account={account}
|
|
||||||
withdrawalId={parsedUri.withdrawalOperationId}
|
withdrawalId={parsedUri.withdrawalOperationId}
|
||||||
|
onError={onError}
|
||||||
|
onConfirmed={onConfirmed}
|
||||||
|
onAborted={onAborted}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -66,14 +66,14 @@ body {
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
z-index: 10000;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
left: 1vw;
|
left: 1vw;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #0042b2;
|
background: #0042b2;
|
||||||
z-index: 10000;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a,
|
nav a,
|
||||||
|
@ -14,7 +14,13 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
|
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
|
import {
|
||||||
|
ErrorType,
|
||||||
|
HttpError,
|
||||||
|
useTranslationContext,
|
||||||
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
|
import { ErrorMessage } from "./context/pageState.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate (the number part of) an amount. If needed,
|
* Validate (the number part of) an amount. If needed,
|
||||||
@ -58,6 +64,13 @@ export type WithIntermediate<Type extends object> = {
|
|||||||
? WithIntermediate<Type[prop]>
|
? WithIntermediate<Type[prop]>
|
||||||
: Type[prop] | undefined;
|
: Type[prop] | undefined;
|
||||||
};
|
};
|
||||||
|
export type RecursivePartial<T> = {
|
||||||
|
[P in keyof T]?: T[P] extends (infer U)[]
|
||||||
|
? RecursivePartial<U>[]
|
||||||
|
: T[P] extends object
|
||||||
|
? RecursivePartial<T[P]>
|
||||||
|
: T[P];
|
||||||
|
};
|
||||||
|
|
||||||
export enum TanChannel {
|
export enum TanChannel {
|
||||||
SMS = "sms",
|
SMS = "sms",
|
||||||
@ -99,3 +112,52 @@ export enum CashoutStatus {
|
|||||||
|
|
||||||
export const PAGE_SIZE = 20;
|
export const PAGE_SIZE = 20;
|
||||||
export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
|
export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
|
||||||
|
|
||||||
|
export function buildRequestErrorMessage(
|
||||||
|
i18n: ReturnType<typeof useTranslationContext>["i18n"],
|
||||||
|
cause: HttpError<SandboxBackend.SandboxError>,
|
||||||
|
specialCases: {
|
||||||
|
onClientError?: (status: HttpStatusCode) => TranslatedString | undefined;
|
||||||
|
onServerError?: (status: HttpStatusCode) => TranslatedString | undefined;
|
||||||
|
} = {},
|
||||||
|
): ErrorMessage {
|
||||||
|
let result: ErrorMessage;
|
||||||
|
switch (cause.type) {
|
||||||
|
case ErrorType.TIMEOUT: {
|
||||||
|
result = {
|
||||||
|
title: i18n.str`Request timeout`,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ErrorType.CLIENT: {
|
||||||
|
const title =
|
||||||
|
specialCases.onClientError && specialCases.onClientError(cause.status);
|
||||||
|
result = {
|
||||||
|
title: title ? title : i18n.str`The server didn't accept the request`,
|
||||||
|
description: cause.payload.error.description,
|
||||||
|
debug: JSON.stringify(cause),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ErrorType.SERVER: {
|
||||||
|
const title =
|
||||||
|
specialCases.onServerError && specialCases.onServerError(cause.status);
|
||||||
|
result = {
|
||||||
|
title: title
|
||||||
|
? title
|
||||||
|
: i18n.str`The server had problems processing the request`,
|
||||||
|
description: cause.payload.error.description,
|
||||||
|
debug: JSON.stringify(cause),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ErrorType.UNEXPECTED: {
|
||||||
|
result = {
|
||||||
|
title: i18n.str`Unexpected error`,
|
||||||
|
debug: JSON.stringify(cause),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user